I have a SurveyForm where I create dynamically my fields. Here's the most basic code I could do and still have my problem:
class SurveyForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields = []
self.fields_alone = []
new_field = forms.CharField(label=f'1 - 2: This is the question',
widget=widgets.Input(attrs={}))
self.fields.append(('id_1', new_field))
self.fields_alone.append(new_field)
self.fields = OrderedDict(self.fields)
In my template, when I do a "classical" loop I can display the fields and it works, but the second loop, which is supposed to access to the same fields, doesn't work:
<form action="" method="post">
{% for field in form %}
{{ field.label }} :{{ field }} <br>
{% endfor %}
{% for field in form.fields_alone %}
{{ field.label }} :{{ field }} <br>
{% endfor %}
</form>
The second loop display the field as a string like <django.forms.fields.CharField object at 0x0000012C00EF60D0>
What am I missing to display is like the "classical" loop?
I found the problem. It's in the source code of Django.
Django can only convert fields to HTML if they are BoundField. The problem is that you can access to the BoundField "version" of the fields of your form through iteration on the form itself.
In the django/forms/forms.py:
def __getitem__(self, name):
# blabla code
if name not in self._bound_fields_cache:
self._bound_fields_cache[name] = field.get_bound_field(self, name)
So you can get HTML code only through an iteration on the form or direct access to a field via form['myfield'].
So in my form I did:
class SurveyForm(forms.Form):
def field_by_id(self, field_id):
return self[field_id]
Then I've made a template tag in my application which is:
#register.filter(name='field_by_id')
def field_by_id(arg1, arg2):
"""
Returns the result of field_by_id() (= method has to exist in arg1!)
Usage: {{ form|field_by_id:XXX }} (XXX = field id string)
:param arg1: object of class Form
:param arg2: field id string
:returns corresponding field
"""
return arg1.field_by_id(arg2)
and then in my template, I use it like this:
{% for question_group, questions in form.question_groups.items %}
{% for question, answers in questions.items %}
{% for answer in answers %}
{% with form|field_by_id:answer as field %}
{{ field.label }} :{{ field }} <br>
{% endwith %}
{% endfor %}
{% endfor %}
{% endfor %}
And it works. Tricky solution, but I have many sub-groups (I could have used FormSet's for a single sub-group).
The fields list is meant to store the form fields. Whereas fields_alone isn't.
That is also the reason why you are able to loop over the form itself and not over form.fields.
Since I cant figure out why you need to have this other list I suggest that you add all the fields to the actual fields list.
Comment if there are any further questions to this problem
Here:
self.fields = []
You're overwriting the form's own fields ordereddict. You just want to add to it:
self.fields["whatevernameyouwant"] = new_field
(works on django 1.11, not tested on 2.x / 3.x)
EDIT:
the problem is when I try to use the fields through another property
You mean the form.fields_alone thing ? Well, you can always make it a property instead:
class YourForm(...):
def __init__(...)
# ....
self._fields_alone = []
self.fields["whatevernameyouwant"] = new_field
self.fields_alone.append("whatevernameyouwant")
#property
def fields_alone(self):
for name in self._fields_alone:
yield self[name]
But it won't remove this field from the "normal" form iterator (for field in form) so you'll have the field twice, so you'd also need to redefine __iter__ too:
def __iter__(self):
for name in self.fields:
if name not in self._fields_alone:
yield self[name]
Related
I am trying to raise ValidationError if a duplicate entry found in an inlineformset, just like in the docs: Custom formset validation I'm using a debugging tool and stepping through the code. It successfully reaches the line: raise ValidationError("Check Duplicate Entry"), and subsequently, in the view it takes the else statement instead proceeding with if formset.is_valid. I print(formset.non_form_errors) just as in the docs. However, the example in the docs returns a list at this stage with the error message inside it. In my case, I get <bound method BaseFormSet.non_form_errors of <django.forms.formsets.MyInlineFormSet object at 0x7fcc22e89610>> The template renders with the form data but the validation error does not appear to say what is wrong. I'm missing something for sure. Any help appreciated.
class MyInlineFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(MyInlineFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.empty_permitted = False
def clean(self):
if any(self.errors):
return
mylist = []
for form in self.forms:
myfield_data = form.cleaned_data.get("myfield")
if myfield_data in mylist:
raise ValidationError("Check Duplicate Entry")
mylist.append(myfield_data)
My tempate just has this:
{% block content %}
<section class="container-fluid">
{% crispy form %}
</section>
{% endblock content %}
Side Note: Any field validation errors against specific fields do show up. But there I am writing the errors like so: self._errors["fieldname"] = self.error_class(["Error in field, please double check"]) Not sure why raise ValidationError is not working for the formset
SOLVED! My formset is a crsipy form within a crispy form. The overall form is rendered in the template linked to the view. But I should have mentioned, I have a separate template containing the markup for the formset.
<div class="row justify-content-center">
<div class="col-7">
{{ formset.management_form | crispy }}
{% for form in formset.forms %}
<h3>Form {{forloop.counter}}</h3>
{% crispy form %}
{% endfor %}
</div>
<br>
</div>
So I just needed to add {{ formset.non_form_errors }} below formset.management_form.
I have a database of television channels that I am displaying using a Django form with a ModelMultipleChoiceField and a CheckboxSelectMultiple widget.
The template renders fine, but how do I access fields of the individual objects that are part of the ModelMultipleChoiceField field of the form?
Channel model
class Channel(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
This is the form
class ChannelForm(forms.Form):
channels_list = forms.ModelMultipleChoiceField(queryset=Channel.objects.all().order_by('name'),widget=forms.CheckboxSelectMultiple)
This is how I was trying to access the 'name' field of each channel in the template by doing {{channel.name}}.
{% for channel in form.channels_list %}
{{ channel }} accessing: {{ channel.name }}<br>
{% endfor %}
When it renders it simply says "accessing: " without the channel name
Here is my view.py as requested:
def generage_recommendation(request):
if request.method == 'POST':
# POST code ...
else:
form = RecommendationRequestForm()
return render(request, 'recommendation_home.html', {'form':form})
in your views.py,
use ChannelForm instead of RecommendationRequestForm.
in your template, you can access field values of ModelMultipleChoiceField like this:
{% for key, value in form.channels_list.field.choices %}
Channel ID = {{ key }}
Channel Name = {{ value }}
{% endfor %}
(notice .field. code)
Can you post your views.py file.
You don't have to use
form.channels in the template.
Try using this instead.
{% for channel in channels_list %}
{{ channel }} accessing: {{ channel.name }}<br>
{% endfor %}
I have a list of questions and a form that has a corresponding number of answer fields.
Example:
questions = ['What is your favorite color?', 'What is your first car?']
How do I display in template, alternating question and answer pair?
Desired results:
What is your favorite color?
Answer: [answer_field1]
What is your first car?
Answer: [answer_field2]
Currently, my template has this code but it is not displaying correctly.
{% load i18n %}
{% block head %}
{{ wizard.form.media }}
{% endblock %}
{% block content %}
Please answer these questions:
<p>
<p>
<form action="" method="post">{% csrf_token %}
{% for question in questions %}
{{ question }}
{{ form.as_p }}
{% endfor %}
I don't have control over looping over 'form' because Django does that automatically. I'm not sure how to zip 'form' and 'questions' together to for loop them together.
you might need to write a custom templatetag to achieve such task. but before you would do that, you might need to consider these questions
Does the index of a question in the questionsList correspond with the index of its answer field in the form?
Does the length of the questionsList equals the number of fields present in the form?
Try this
templatetags/mytag.py
from django import template
register = template.Library()
def do_question_n_ans(parser, token):
"""
Usage:
{% do_question_n_ans_for question in questionsList and ans_field in form.fields %}
"""
bits = token.contents.split()
if len(bits) != 8:
raise template.TemplateSyntaxError("'%s' takes exactly seven arguments" % bits[0])
if len(bits[3]) != len(bits[7]):
raise template.TemplateSyntaxError("Question list and anwser fields must b of the same length")
if bits[2] != 'in':
raise template.TemplateSyntaxError("Second argument to '%s' must be 'in'" % bits[0])
if bits[4] != 'and':
raise template.TemplateSyntaxError("Third argument to '%s' must be 'and'" % bits[0])
if bits[7] != 'in':
raise template.TemplateSyntaxError("Sixth argument to '%s' must be 'in'" % bits[0])
return QuestionAnswerFormatNode(bits[1], bits[3], bits[5], bits[7]
class QuestionAnswerFormatNode(template.Node):
def __init__(self, question, questionsList, ans_field, fields):
self.questionsList = questionsList
self.fields = fields
self.question = question
self.ans_field = ans_field
def render(self, context):
try:
questionsList = template.resolve_variable(self.questionsList, context)
except template.VariableDoesNotExist:
raise template.VariableDoesNotExist
question_index = questionsList.index(question)
ans_field = fields[question_index]
context[self.ans_field] = ans_field
return ''
register.tag('do_question_n_ans_for', do_question_n_ans)
you could then use the tag within your html code
{% load mytag %}
{% for question in questionList %}
{% do_question_n_ans_for question in questionsList and ans_field in form.fields %}
<p>Question: {{ question }}</p>
<p>Answer: {{ ans_field }}</p>
{% endfor %}
Note:
I presumed you already have the questions and the answer fields arranged in this format in their respective list
['question1', 'question2', 'question3'] and ['answerFieldToQuestion1', 'answerFieldToQuestion2', 'answerFieldToQuestion3'].
you should be able to check out the order of your form fields by printing yourForm.fields.
Please let me know if there are any errors as i didn't test the above code, but i guarantee you it would work just fine
My model formset without even defining "extra" parameters in modelformset_factory is rendering one extra field in the template. I have tried many variations but it didn't work. If I print the form (the model form) on the command line it just prints a single form field as required but on model formset it prints 2 by default.
Here is my code.
models.py
class Direction(models.Model):
text = models.TextField(blank=True, verbose_name='Direction|text')
forms.py
class DirectionForm(forms.ModelForm):
class Meta:
model = Direction
fields = ['text',]
views.py
def myview(request):
Dirset = modelformset_factory(Direction, form=DirectionForm)
if request.method == "POST":
dir_formset = Dirset(request.POST or None)
if dir_formset.is_valid():
for direction in dir_formset:
text = direction.cleaned_data.get('text')
Direction.objects.create(text=text)
return render(request, "test/test.html", {'DirFormSet':Dirset})
template
{% block content %}
<form method="POST">{% csrf_token %}
<div id="forms">
{{DirFormSet.management_form}}
{% for form in DirFormSet %}
{{form.text}}
{% if error in form.text.errors %}
{{error|escape}
{% endif %}
{% endfor %}
</div>
<button id="add-another">add another</button>
<input type="submit" />
</form>
{% endblock %}
As a side note, if I submit data on this form it gives the following error.
Error
Exception Type: MultiValueDictKeyError
Exception Value:"u'form-0-id'"
By default, modelformset_factory creates one extra form. If you don't want any extra forms, set extra=0.
Dirset = modelformset_factory(Direction, form=DirectionForm, extra=0)
The KeyError is because you have not included the form's id field in your template. You should have something like:
{% for form in dir_formset %}
{{ form.id }}
{{ form.text }}
...
{% endfor %}
Note that you should be passing the formset instance dir_formset when rendering the template, not the class DirFormSet. Your view should be something like
return render(request, "test/test.html", {'dir_formset': dir_formset})
then the template should be updated to use dir_formset instead of DirFormSet.
I would like to display 2 groups of fields. The group is defined based on the kind of field.
I added 2 methods in my form:
def get_group_a(self):
return [obj for obj in self.fields.values() if isinstance(obj, GroupAField)]
def get_group_b(self):
return [obj for obj in self.fields.values() if isinstance(obj, GroupBField)]
Then in the template I tried to display the form:
<h1>Group A:</h1>
{% for f in form.get_group_a %}
<div class="fieldWrapper">
{{ f.errors }}
<label> {{ f.label }}:</label>
{{ f }}
</div>
{% endfor %}
<h1>Group B:</h1>
{% for f in form.get_group_b %}
<div class="fieldWrapper">
{{ f.error }}
<label> {{ f.label }}:</label>
{{ f }}
</div>
{% endfor %}
This is working partially. I have the good label of the field but I don't have the text input displayed.
How can I get the good field object?
Don't iterate through self.fields in your get_group methods, but through self directly. self.fields contains the raw field instances: for display, Django creates BoundField instances which wrap those fields, and which you access directly via self['fieldname'].
For representing form fields in template Django uses BoundField. BoundField used to display HTML or access attributes for a single field of a Form instance. So in your case, you should wrap grouped fields with BoundField, like this:
def get_group_a(self):
return [BoundField(self, field, name) for name, field in self.fields.items() if isinstance(field, GroupAField)]
def get_group_b(self):
return [BoundField(self, field, name) for name, field in self.fields.items() if isinstance(field, GroupbField)]