I developed a Django application in which i have a form with some fields. Depending on the input additional fields are displayed are hidden. Now everything worked quit fine in Django 3.2.14 since the update in Django 4.0.6 it didn't worked anymore.
I first build a form, where if a "field_rule_display" exists the field widget is set as "HiddenInput".
class AnalysisForm(forms.Form):
def __init__(self, analysis_form_template: AnalysisFormTemplate, disable_required: bool, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout()
self.helper.add_input(Submit("submit", _("Evaluate"), css_class="btn-primary btn-lg"))
analysis_field_queryset = analysis_form_template.analysis_fields
analysis_form_url = reverse("analysis_form", args=(analysis_form_template.id,))
for field in analysis_field_queryset.all():
htmx_dictionary = _htmx_dictionary(analysis_form_url, field)
self.fields[field.name_for_formula] = _get_field_by_type(
field, htmx_dictionary, analysis_form_template, self.data
)
self.fields[field.name_for_formula].empty_values = empty_values()
self.helper.layout.fields.append(
Div(Field(field.name_for_formula), css_class=AnalysisFieldKind(field.kind).name)
)
if field.field_rule_display is not None and disable_required is False:
self.fields[field.name_for_formula].widget = forms.HiddenInput()
self.fields[field.name_for_formula].widget.attrs["disabled"] = True
if disable_required:
self.fields[field.name_for_formula].required = False
After the user enters a specific input into the form, htmx will send a request and i rebuild the form with the new fields. And here the problem starts, even if i update my field in the "self.fields" Django does not render the update and my form field is still hidden.
if field.field_rule_display is not None:
evaluated_result_display = self._evaluated_formula(
field,
analysis_form_template,
field.field_rule_display,
field.field_rule_display.formula,
cleaned_data,
)
if evaluated_result_display:
field_type = _get_field_by_type(
field, htmx_dictionary, analysis_form_template, cleaned_data
)
self.fields[field.name_for_formula] = field_type
self.fields[field.name_for_formula].initial = cleaned_data[field.name_for_formula]
Here the second field should be displayed, but only my border is shown as a result of changing the crispy layout helper.
if (
field.field_rule_display is None or (field.field_rule_display is not None and evaluated_result_display)
) and field.field_rule_highlight is not None:
evaluated_result_highlight = self._evaluated_formula(
field.name_for_formula,
analysis_form_template,
field.field_rule_highlight,
field.field_rule_highlight.formula,
cleaned_data,
)
if evaluated_result_highlight:
field_layout = Div(
Field(field.name_for_formula),
css_class=f"{AnalysisFieldKind(field.kind).name} border border-primary mb-2 p-2",
)
self.fields[field.name_for_formula].empty_values = empty_values()
field_layout_list.append(field_layout)
self.helper.layout.fields = field_layout_list
I would appreciate some help, why this does not work anymore in Django 4 but in Django 3 it worked without a problem.
After some debugging i found a possible solution to my problem.
It seems that Django caches my form fields and their widgets in "_bound_fields_cache". Now there my fields widget is still set to "HiddenInput", even after updating the widget to a "TextInput".
So i tried updating the field in "_bound_fields_cache" and to my suprise it worked.
if field.field_rule_display is not None:
evaluated_result_display = self._evaluated_formula(
field,
analysis_form_template,
field.field_rule_display,
field.field_rule_display.formula,
cleaned_data,
)
if evaluated_result_display:
field_type = _get_field_by_type(
field, htmx_dictionary, analysis_form_template, cleaned_data
)
self.fields[field.name_for_formula] = field_type
self.fields[field.name_for_formula].initial = cleaned_data[field.name_for_formula]
if field.name_for_formula in self._bound_fields_cache:
self._bound_fields_cache[field.name_for_formula].field = field_type
But i am not quite satisfied with this kind of solution. I can't really point my finger why Django doesn't change the "_bound_fields_cache" after updating my form fields. Changing the "_bound_fields_cache" seems for me like an ugly hack... Is there perhaps a better solution?
Related
Apologies if the title is poorly worded, I couldn't figure out a succinct way to describe this issue.
I've been working with a client who uses the Django form library to allow users to create/edit model instances. I've recently added a many to many relationship between model A and model B, and I want there to be an input on the model A form that allows users to connect instances of model B to it. The SelectMultiple widget used by Django by default is unintuitive, and the alternative CheckboxSelectMultiple widget seems like it wouldn't be great once there are 100s of options (which is a likely scenario). What complicates this further is that there's also a M2M relationship between model C and D, connected by a through table that determines a D instance's order on a C instance, and neither of those widgets work well with through tables.
My initial thought was to use CheckboxSelectMultiple and just order the already selected options at the top, wrapping the widget in a scroll overflow container. I unfortunately don't know how to order options in that way though, since the accepted queryset parameter is inherently unordered. For the case with the through table, ideally each checked option would have an input next to it where users could set the order of the respective instance, but I'm not sure how to fundamentally alter the widget in that way.
So, how would you advise I go about making a form field for a M2M relationship with many options? Is there a way to make my modified CheckboxSelectMultiple idea work, or is there an alternative, more efficient way to do this (possibly involving libraries)? I can't imagine I'm the first person to encounter this problem.
Here's "one I wrote earlier" Sorry, this is a bit of an info-dump. Has worked, but not fully tested. Check that it's csrf-safe if you care about that (it doesn't use a form, just picks things out of request.POST)
class GenericEditM2MView( DetailView):
#model = Model # required as per DetailView
# template_name = # as per DetailView
#m2m_fieldname = None # no longer required if unique: the name of the model's m2m field to operate on
remove_qs = None # defaults to .all() of the m2m field
success_url = '.' # defaults to displaying the modified m2m relationship unless done
done_url = None # where to go if submit="Done", defaults to success_url
#
"""template_name must define a form full of checkboxes, obtained from
{% for p in add_qs %}
<input type="checkbox" name="add" value="{{p.pk}}" > {% endfor}
{% for p in remove_qs %}
<input type="checkbox" name="remove " value="{{p.pk}}" > {% endfor}
default is to return to this same view after submit, to show that the changes
have been made. You can supply <input type="submit" name="submit" value="done" />
which will go to done_url instead of success_url
example use:
class PenStockM2MView( GenericEditM2MView):
template_name = 'playpen/edit_m2m.html'
model = PenStock
# m2m_fieldname = 'name' # works it out for itself if ony one M2M field on the model
done_url = '/playpen/OK'
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# everything works without this __init__ provided self.m2m_fieldname is present and correct.
# if model has only one m2m field, locate it via _meta as default.
# Also check m2m_fieldname is m2m because very confusing errors later if it's not!
f = getattr(self, 'm2m_fieldname', None)
m2m_fieldnames = [
field.name for field in self.model._meta.get_fields() if field.many_to_many ]
model_name = self.model.__name__
if f and not f in m2m_fieldnames:
raise AttributeError( f'field "{f}" is not a many-to-many field in {model_name}')
if not f:
if len( m2m_fieldnames ) == 1:
self.m2m_fieldname = m2m_fieldnames[0]
else:
raise AttributeError( f'Cannot identify a unique many-to-many field in {model_name}' )
def get_add_queryset(self):
field = getattr( self.object, self.m2m_fieldname)
remove_qs = self.get_remove_queryset()
already_there = remove_qs.values_list('pk', flat=True)
return remove_qs.model.objects.exclude( pk__in = already_there) # is qs.model documented?
def get_remove_queryset(self):
if hasattr( self, 'remove_queryset'):
return self.remove_qs
remove_qs = getattr( self.object, self.m2m_fieldname)
return remove_qs.all()
def get_context_data( self, **kwargs):
context = super().get_context_data( **kwargs)
context['add_qs'] = self.get_add_queryset()
context['remove_qs'] = self.get_remove_queryset()
return context
def post( self, request, *args, **kwargs):
self.object = self.get_object()
add = request.POST.getlist('add')
remove = request.POST.getlist('remove')
add_objs = list( self.get_add_queryset().filter(pk__in=add) )
remove_objs = list( self.get_remove_queryset().filter(pk__in=remove) )
field = getattr( self.object, self.m2m_fieldname )
field.add( *add_objs)
field.remove( *remove_objs)
return HttpResponseRedirect( self.get_done_url() or self.get_success_url() )
def get_success_url(self):
return self.success_url
def get_done_url( self):
done = self.request.POST.get("submit", None)
if done == "done" and hasattr(self, 'done_url'):
return self.done_url
return None
Alright, so I'm following a tutorial on creating a small questionnaire with Django.
A User can create a survey with different multiple choice questions. Since they're multiple choice questions, users can also set the options for the questions.
A "survey taker" can then start a survey, select his or her preferred questions and submit the form.
I would like to only show a single question per page so I'm trying to work with modelformset_factory and implement pagination using the build in paginator.
The form is rendered correctly and a user can submit an answer, however, the form fails before "formset.is_valid()", I just can't figure out why;
ValueError at /surveys/1/submit/4/
Cannot assign "'2'": "Answer.option" must be a "Option" instance.
So I can not save an integer and I somehow have to relate the integer with the id from the option model... But I can't access cleaned_data yet so I guess I'm missing something here. Did I forget something within the formset?
I've been staring at this for a while now so any help is appreciated.
#Views
def submit(request, survey_pk, sub_pk):
# Let's retrieve the survey which is created by the survey-taker
try:
survey = Survey.objects.prefetch_related("question_set__option_set").get(
pk=survey_pk, is_active=True
)
except Survey.DoesNotExist:
raise Http404()
try:
submission = survey.submission_set.get(pk=sub_pk, is_complete=False)
except Submission.DoesNotExist:
raise Http404()
# Retrieve all question associated with this survey
questions = survey.question_set.all()
# Get all related options from the questions
options = [q.option_set.all() for q in questions]
form_kwargs = {"empty_permitted": False, "options": options,}
# Setup a formset utilizing a ModelFormSet
AnswerFormset = modelformset_factory(
Answer,
form=AnswerModelForm,
formset=BaseAnswerFormSet,
exclude=(),
extra=len(questions),
)
if request.method == "POST":
print("Request.POST", request.POST)
# This is where the problem starts
formset = AnswerFormset(request.POST, form_kwargs=form_kwargs)
print("Formset: ", formset)
if formset.is_valid():
print("Formset is valid")
with transaction.atomic():
for form in formset:
Answer.objects.create(
option_id=form.cleaned_data["option"],
submission_id=sub_pk,
)
submission.is_complete = True
submission.save
return redirect("survey-thanks", pk=survey_pk)
else:
formset = AnswerFormset(form_kwargs=form_kwargs)
print(form_kwargs)
question_forms = zip(questions, formset)
return render(
request,
"survey/submit.html",
{
"survey": survey,
"question_forms": question_forms,
"formset": formset
}
)
#forms
# Setup a Modelformset
class AnswerModelForm(forms.ModelForm):
class Meta:
model = Answer
fields = "option",
def __init__(self, *args, **kwargs):
options = kwargs.pop("options")
# Options must be a list of Option objects
choices = {(o.pk, o.text) for o in options}
super().__init__(*args, **kwargs)
option_field = forms.ChoiceField(choices=choices, widget=forms.RadioSelect, required=True)
self.fields["option"] = option_field
class BaseAnswerFormSet(forms.BaseFormSet):
def get_form_kwargs(self, index):
kwargs = super().get_form_kwargs(index)
kwargs["options"] = kwargs["options"][index]
return kwargs
#models
class Answer(models.Model):
"""An answer a survey's questions."""
submission = models.ForeignKey(Submission, on_delete=models.CASCADE)
option = models.ForeignKey(Option, on_delete=models.CASCADE)
option_id=form.cleaned_data["option"],
is the suspect, it may require an option instance, even though you are trying to pass the id. Try changing option_id to option__id and explicitly setting the form.cleaned_data["option"] to int. Another way to to is to pull the option manually and pass it directly:
option = Option.object.get(id=int(form.cleaned_data["option"]))
I've got the following django crispy form:
class ConsultForm(forms.ModelForm):
class Meta:
model = Consults # Your User model
fields = [ 'TEMPLATE','EMAIL', 'DATE']
labels = {
'EMAIL' : 'Your Email',
'DATE' : 'Todays date',
# 'captcha': "Enter captcha"
}
helper = FormHelper()
helper.form_method = 'POST'
helper.form_action = "/contact/"
helper.form_id = 'form' # SET THIS OR BOOTSTRAP JS AND VAL.JS WILL NOT WORK
helper.add_input(Submit('Submit', 'Submit', css_class='btn-primary'))
helper.layout = Layout(
Field('TEMPLATE', type="hidden"),
Field('DATE', type="hidden"))
I want to pass a value with the hidden field TEMPLATE. I've read https://django-crispy-forms.readthedocs.io/en/latest/api_helpers.html , but can't see how to do this. How can I get this done?
You can set Form field initial values like this:
class ConsultForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.initial['TEMPLATE'] = 'my_initial_value'
You can also change the value of the field at other places in your code like:
form = ConsultForm(instance=instance)
form.initial['TEMPLATE'] = 'new_value'
With formhelper (with crispy Universal Layout Objects like Field) you set attributes as you already did, like:
Field('TEMPLATE', id="template", value="something" template="my-template.html")
If that's what you were asking for.
Or if the above does not work easy then there is a layout object called Hidden in crispy. You can create hidden input fields with that:
Hidden('name', 'value')
You use it as Hidden('TEMPLATE', 'mysomethingvalue')
Like:
Button('name', 'value')
To make it fully clear:
helper.layout = Layout(
Hidden('TEMPLATE', 'myvalue'),
Hidden('DATE', 'anydate'))
I have a form with a search box that uses jQuery to fill a multipleChoiceField. I need to use a custom multipleChoiceField so I can control the validation and only check if the choice exists, not if it was one of the original choices as a modelMultipleChoiceField with a queryset would. However, the custom multipleChoiceField renders on the page as empty until you enter something in the search box to fill it with choices via jQuery. I would like it to render with a few choices to begin with instead.
class ArticleMultipleChoiceField(forms.MultipleChoiceField):
def __init__(self, *args, **kwargs):
super(ArticleMultipleChoiceField, self).__init__(*args, **kwargs)
include_articles = [article.id for article in Article.objects.order_by('-sub_date')[:5]]
self.choices = Article.objects.filter(id__in=include_articles).order_by('-sub_date')
In this form, I get the error "Article object is not iterable". I have also tried changing that self.choices to self.data, self.queryset, and self.initial, and in all those 3 cases, I keep getting an empty multiple choice field instead.
How can I use a queryset to provide the initial set of choices here?
Here is the form it is used in:
class StorylineAddArticleForm(forms.Form):
articleSearchBox = forms.CharField(label="Search to narrow list below:")
include_articles = [article.id for article in Article.objects.order_by('-sub_date')[:5]]
articles = ArticleMultipleChoiceField()
def __init__(self, *args, **kwargs):
super(StorylineAddArticleForm, self).__init__(*args, **kwargs)
self.fields['articleSearchBox'].required = False
self.helper = FormHelper(self)
self.helper.layout = Layout(
Field('articleSearchBox'),
Field('articles'),
ButtonHolder(
Submit('submit', 'Add', css_class='button white')
)
)
Also, this is being rendered by Crispy Forms.
choices doesn't accept a QuerySet as an argument, it needs a list or tuple of two-tuples with acceptable values. See the documentation on choices here: https://docs.djangoproject.com/en/2.0/ref/models/fields/#field-choices .
In this case you need to turn your Article queryset into a list or tuple of the above format.
I have a field in one of my models like the following:
PAYROLL_CHOICES = (
('C1', 'Choice1'),
('C2', 'Choice2')
etc.....
)
payrollProvider = models.CharField(max_length=2, choices=PAYROLL_CHOICES)
When I create a model form for this field, Django correctly generates an HTML select box, but includes a default blank value of "---------".
I would like to know how to change this default value to some other text, such as "please choose value".
I believe I should be able to set this in my model form's init via the following, as documented in this answer and several others:
self.fields['payrollProvider'].empty_label = "please choose value"
However, this isn't working for me. When I include that line in my form's init, "--------" still shows up as the initial choice in the select box. I'm pasting the relevant forms.py below, but it seems that others have also been unable to access / modify empty_label. At this link, the questioner describes a way to delete the default empty_label value (which I was able to do successfully via his method) but what I really want to do is to modify the empty_label that is displayed.
Any ideas?
Here's the code for the form in forms.py, with the empty_label code that isn't successful at changing the default "----------":
class PayrollCredentialForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PayrollCredentialForm, self).__init__(*args, **kwargs)
self.fields['payrollUsername'].widget.attrs.update({'class' : 'yp-signup'})
self.fields['payrollPassword'].widget.attrs.update({'class' : 'yp-signup'})
self.fields['payrollProvider'].widget.attrs.update({'class' : 'yp-signup'})
self.fields['payrollUsername'].widget.attrs.update({'placeholder' : ' Payroll Username'})
self.fields['payrollPassword'].widget.attrs.update({'placeholder' : ' Payroll Password'})
self.fields['payrollProvider'].empty_label = "please choose value"
class Meta:
model = Company
fields = ('payrollProvider', 'payrollUsername', 'payrollPassword')
widgets = {
'payrollPassword': forms.PasswordInput(),
}
dokkaebi, that won't work properly. You'll receive the following select code:
<select name="payrollProvider" id="id_payrollProvider">
<option value="" selected="selected">---------</option>
<option value="" selected="selected">please choose value</option>
<option value="C1">Choice1</option>
<option value="C2">Choice2</option>
</select>
The only relatively convenient way that came to my mind is to do something like this in the form:
class PayrollCredentialForm(forms.ModelForm):
class Meta:
model = Company
def __init__(self, *args, **kwargs):
super(PayrollCredentialForm, self).__init__(*args, **kwargs)
self.fields["payrollProvider"].choices = [("", "please choose value"),] + list(self.fields["payrollProvider"].choices)[1:]
Actually, now (as of Django 1.8 and higher) override of an empty_label works:
class PayrollCredentialForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PayrollCredentialForm, self).__init__(*args, **kwargs)
self.fields['payrollProvider'].empty_label = 'Please, choose value'
Also, if you working with Django Admin, there is an option to set empty value for a list view:
class PayrollCredentialAdmin(admin.ModelAdmin):
list_display = ('payrollProvider_value', )
def payrollProvider_value(self, instance):
return instance.payrollProvider
payrollProvider_value.empty_value_display = 'Empty value'
What if field should be readonly?
There is a catch if field modified in such way should be readonly.
If overridden form field will be specified in readonly_fields attribute inside PayrollCredentialAdmin class, it would result in KeyError exception in PayrollCredentialForm (because readonly field won't be included in form's self.fields). To handle that, it's required to override formfield_for_dbfield instead of using readonly_fields:
def formfield_for_dbfield(self, db_field, **kwargs):
field = super(PayrollCredentialAdmin, self).formfield_for_dbfield(
db_field, **kwargs
)
db_fieldname = canonical_fieldname(db_field)
if db_fieldname == 'payrollProvider':
field.widget = forms.Select(attrs={
'readonly': True, 'disabled': 'disabled',
})
return field
Might be useful.
Update for Django 1.11:
Comments below brought assumption that such override is no longer valid for newer version of Django.
The problem is that you are trying to specify something that is not available for the type of Select field.
The empty_label option is for forms.ModelChoiceField, which happens to use a Select widget, but is not the same kind of field as your CharField that you are providing options for.
https://docs.djangoproject.com/en/dev/ref/forms/fields/#modelchoicefield
You can see this also in a previous question here: https://stackoverflow.com/a/740011/1406860
You could try and override the html of the modelform to add the first option as "please choose value". Alternatively, you could use a template filter to do the same thing. Lastly, you could and ("", "please choose value") to PAYROLL_CHOICES, and if you don't want it to be submitted without a payrollProvider just set blank=False for the field in the model.
JD
In your forms.py file,
This would definitely work.. Try this...
class Meta:
model = StaffDetails
fields =['photo','email', 'first_name','school','department', 'middle_name','last_name','gender', 'is_active']
def __init__(self, *args, **kwargs):
super(StaffDetailsForm, self).__init__(*args, **kwargs)
self.fields['Field_name'].empty_label = 'Please Select'
It worked for me.. just replace the field names with yours...
Only ModelChoiceField (generated for ForeignKey fields) supports the empty_label parameter, and in that case it's tricky to get at as those fields are usually generated by django.forms.models.ModelFormMetaclass within a call to django.forms.models.modelform_factory.
ModelFormMetaclass uses the empty_label param to add another choice to the list, with empty_label as the display and '' as its value.
The simplest way to do what you want is just to add an empty choice to your choices list:
PAYROLL_CHOICES = (
('', 'please choose value'),
('C1', 'Choice1'),
('C2', 'Choice2'),
etc.....
)
Another simple way worked for me is:
country = forms.ModelChoiceField(queryset=Country.objects.filter(), empty_label='--Select--')
However, my Django version is 2.2.7
Just add a tuple to your model.field.choices with a value of None:
payrollProvider = models.CharField(max_length=2, choices=PAYROLL_CHOICES)
PAYROLL_CHOICES = (
(None, 'please choose'),
('C1', 'Choice1'),
('C2', 'Choice2')
etc.....
)
From the docs (v4.0):
Unless blank=False is set on the field along with a default then a label containing "---------" will be rendered with the select box. To override this behavior, add a tuple to choices containing None; e.g. (None, 'Your String For Display'). Alternatively, you can use an empty string instead of None where this makes sense - such as on a CharField.
Adapted from Javed answer. Since I have tons of fields in my form I just want to replace all labels in the html by placeholders so for select tags I use their field label.
class PacienteForm(forms.ModelForm):
class Meta:
model=Paciente
fields=('__all__')
def __init__(self, *args, **kwargs):
super(PacienteForm, self).__init__(*args, **kwargs)
for f in self.fields:
if hasattr(self.fields[f], 'choices'):
choices=self.fields[f].choices
if type(choices) == list:
choices[0]=('',self.fields[f].label)
self.fields[f].choices=choices
Add Empty String with "Please Select" to choices as shown below:
class DateForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class Months(models.TextChoices):
EMPTY_LABEL = '', 'Please Select' # Here
JANUARY = 'JAN', 'January'
FEBRUARY = 'FEB', 'February'
MARCH = 'MAR', 'March'
self.fields['month'].choices = Months.choices