trouble showing desired checkbox validation state w/ bootstrap5 for django model form w/ m2m field and checkboxselectmultiple widget - django

I have a checkboxselectmultiple on an m2m model field in an ModelForm that is required - meaning at least one of the choices must be selected. I am using the boostrap5 was-validated class on my form:
<form method="POST" action="{{ request.path }}" {% if attempt_submit %}class="was-validated"{% endif %}>
This question is about how the validation shows up on my form with bootstrap5. Should be red border and red ! if not validated, green border and checkmark if so. However, for my checkboxes, if I don't have any selected (and everything else on the form validates), the form will show each checkbox option as green instead of red. Yet, it does know that it's invalid because the page focus will come back up the checkbox area to show the user what to correct (and it doesn't pass form.is_valid() in views.py.
Why are these labels and boxes still showing green and how can I show them as red until I select one and it's now valid?
Along the lines of this post, I have tried adding
{% if form.sales_location.field.required %}required{% else %}form.sales_location.field.required=""{% endif %}
to the checkbox <input>, but then each field is required and if I select one, the other remaining options still remain red - as if every option would have to be selected for the form to validate. Am I supposed to do this anyway and then add something else (JS?) to disable that?
Not sure exactly what code would be helpful to see...
in models.py, this is the field:
sales_location = models.ManyToManyField(SalesLocation, verbose_name="Where do you sell your products? (select all that apply)" )
in forms.py
model = AssessmentProfile
fields = [
'sales_location',
...
]
widgets = {
'sales_location': forms.CheckboxSelectMultiple(attrs={
'class': 'form-check',}),
}
I add this because I read this post about making sure that I use a `ModelMultipleChoiceField' - but I assume that is already happening because it's a model form.(?)
Probably most important, in the template thisform.html, here's how I'm manually adding this form element:
<div class="field-wrapper">
{{ form.sales_location.label_tag }}
<ul id="id_sales_location" class="form-check">
{% for pk, choice in form.sales_location.field.widget.choices %}
<li>
<input {% for location in location_qs %}{% if location == pk %}checked='checked'{% endif %}{% endfor %}
name="sales_location" class="form-check-input" type="checkbox" value="{{ pk }}" id="id_sales_location_{{forloop.counter0}}"
{% if already_submitted %}disabled="disabled"{% endif %}>
<label class="form-check-label" for="id_sales_location_{{forloop.counter0}}">
{{ choice }}
</label>
</li>
{% endfor %}
</ul>
</div>
Also, I tried updating css to manually format red, but think that doesn't address the root of the problem, plus, I wasn't able to do it successfully anyway.
Thanks for taking a look and for any suggestions.

In the end, I used javascript to solve this problem.
I updated the form template
<div class="field-wrapper">
{{ form.sales_location.label_tag }}
<ul id="id_sales_location" class="form-check">
{% for pk, choice in form.sales_location.field.widget.choices %}
<li>
<input {% for location in location_qs %}{% if location == pk %}checked='checked'{% endif %}{% endfor %}
name="sales_location" class="form-check-input" type="checkbox" value="{{ pk }}" id="id_sales_location_{{forloop.counter0}}"
{% if not form.sales_location.field.required %} {% else %} required {% endif %}
{% if already_submitted %}disabled="disabled"{% endif %}>
<label class="form-check-label" for="id_sales_location_{{forloop.counter0}}">
{{ choice }}
</label>
</li>
{% endfor %}
</ul>
</div>
to add required to the input if the checkbox is required. This allows all the checkboxes to come up red when validating, if the field is empty.
Then, I added this javascript to remove 'required' if it's checked.
<script>
// Select all checkboxes using querySelectorAll.
var checkboxes = document.querySelectorAll("input[type=checkbox][name=sales_location]");
checkboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
for (var cb of checkboxes) {
cb.removeAttribute('required');
}
})
});
</script>
If the field is not required, nothing changes. But if it is, then the required attribute on the <input>is gone and all the checkboxes show up green, which is what I wanted.
It's not perfect because if the checkboxes become unchecked, they don't change back to red. So I am making a dirty assumption that if someone checked a box, they wouldn't go back and uncheck it and try to submit. In which case, the validation would show green (and unchecked) until Submit was pressed again, but then it would take them back to this field which would be red again. If you know how to improve my code by adding the different case for the change function (only if the field is required), please feel free to add that. Cheers.

Related

Django ModelMultipleChoiceField bullet style

I have a form that offers options for signing up to mailing lists.
field = forms.ModelMultipleChoiceField(
queryset=MailingList.objects.filter(
list_active=True),
widget=forms.CheckboxSelectMultiple(
{'class': 'no-bullet-list',
'style': 'list-style: none;'}))
# Specifying class and style both is excessive, I'm still exploring.
Meanwhile, I've put this in my CSS sheet:
.no-bullet-list {
list-style-type: none;
}
This does what I want, rendering a correct set of choices as checkboxes, but it also puts bullets before them. This is because the rendered HTML looks like this:
<ul id="id_newsletters" class="no-bullet-list">
<li><label for="id_newsletters_0">
<input type="checkbox" name="newsletters" value="1"
class="no-bullet-list" style="list-style: none;"
id="id_newsletters_0">
Annonces</label>
</li>
That snippet comes from django/forms/templates/django/forms/widgets/multiple_input.html, which is the result of the definition of CheckboxSelectMultiple in django/forms/widgets.py.
class CheckboxSelectMultiple(ChoiceWidget):
allow_multiple_selected = True
input_type = 'checkbox'
template_name = 'django/forms/widgets/checkbox_select.html'
option_template_name = 'django/forms/widgets/checkbox_option.html'
def use_required_attribute(self, initial):
# Don't use the 'required' attribute because browser validation would
# require all checkboxes to be checked instead of at least one.
return False
def value_omitted_from_data(self, data, files, name):
# HTML checkboxes don't appear in POST data if not checked, so it's
# never known if the value is actually omitted.
return False
def id_for_label(self, id_, index=None):
""""
Don't include for="field_0" in <label> because clicking such a label
would toggle the first checkbox.
"""
if index is None:
return ''
return super().id_for_label(id_, index)
My attempt to add CSS attributes to the <li> only succeeded in adding to the <ul> and to the <input ...>, so it doesn't do what I want (remove the bullets). In addition, it appears there's no customisation hook to style the <li>.
The only way I see to do this is to copy the full widget definition and its template to my app and modify them. This is icky. Is there a portable way to style the checkbox list items?
FWIW, my own template does this:
<form method="post">{% csrf_token %}
<p>{{ form.non_field_errors }}</p>
{% for field in form %}
<p>
{{ field.label }}<br>
{{ field }}
{% if field.help_text %}
<small style="color: grey">{{ field.help_text }}</small>
{% endif %}
</p>
{% for error in field.error_messages %}
<p style="color: red">{{ error }}</p>
{% endfor %}
{% endfor %}
<button type="submit" class="btn btn-outline-primary">Je m'inscris</button>
</form>
Your forms.py should work. A couple steps to take to debug:
clear your browser history. I remember I was extremely frustrated with css, and it turns out that they were caching problems.
If step 1) not working, open your chrome develop tools, and see if your css is loading. If you get something like below:
GET http://127.0.0.1:8000/static/soforms/css/soforms.css
net:: ERR_ABORTED 404 (Not Found).
2-1) then try to make sure you have
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR.joinpath('static_files')
2-2) see if your css file is located in the right place.
2-3) in a rare case, it might be due to you forgot migrate after you generate your app. This actually happened to me today.

How to make a selection button inside a multistep form wizard in Django that renders an output without proceeding to the next step?

I am new to Django and I am making a project with a multistep form using django-formtools. The problem is, in my step 2 form, I have selection fields that I need to pass in the backend to perform some calculations and then render the output. The user can make changes anytime based on the output. I made an apply changes button which should trigger the backend process and a proceed to next step button if the user decides to finalize the selected changes. However, when I click the apply changes button, it leads me to the next step instead.
Here's my HTML code:
<form action="" method="POST">
{% csrf_token %}
{{ wizard.management_form }}
{% if wizard.form.forms %}
{{ wizard.form.management_form }}
{% for form in wizard.form.forms %}
{{ form }}
{% endfor %}
{% else %}
{{ form }} # three selection fields
<button name="apply_changes">Apply Changes</button>
{% endif %}
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}">{% trans '‹ Previous Step' %}</button>
{% endif %}
<input type="submit" value="{% trans 'Finish' %}">
</form>
Here's my SessionWizardView method code snippet:
def get_context_data(self, form, **kwargs):
context = super(StepWizard, self).get_context_data(form=form, **kwargs)
if self.steps.current == 'step_1':
# save step 1 data to sessions
if self.steps.current == 'step_2':
step1_data = self.get_all_cleaned_data()
# if apply changes button is clicked
data = self.request.POST.get('apply_changes')
# process data
# add output to context
return context
I need help on how can it rightly be done. Thanks in advance!
So for future django developers who encountered the same problem as me, here's the answer to my question:
1) validate the data in step 2 which is temporarily the default values of my selection fields; and
2) override the post method to load the current page using the goto_step wizard function and embed it in the apply changes button
You can find the guide here :)
And then there 'ya go! Once the user clicks the apply changes button, the page reloads and the output is rendered in the form.
Still needs to optimize it though :D

How to render individual radio button choices in Django?

If I have a model that contains a ChoiceField with a RadioSelect widget, how can I render the radio buttons separately in a template?
Let's say I'm building a web app that allows new employees at a company to choose what kind of computer they want on their desktop. This is the relevant model:
class ComputerOrder(forms.Form):
name = forms.CharField(max_length=50)
office_address = forms.Charfield(max_length=75)
pc_type = forms.ChoiceField(widget=RadioSelect(), choices=[(1, 'Mac'), (2, 'PC')])
On the template, how do I render just the Mac choice button? If I do this, it renders all the choices:
{{ form.pc_type }}
Somewhat naively I tried this, but it produced no output:
{{ form.pc_type.0 }}
(I found a few similar questions here on SO:
In a Django form, how do I render a radio button so that the choices are separated on the page?
Django Forms: How to iterate over a Choices of a field in Django form
But I didn't feel like they had good answers. Is there a way to resurrect old questions?)
Django 1.4+ allows you to iterate over the choices in a RadioSelect, along with the lines of
{% for choice in form.pc_type %}
{{ choice.choice_label }}
<span class="radio">{{ choice.tag }}</span>
{% endfor %}
I'm not sure if this change allows you to use the syntax you describe ({{ form.pc_type.0 }}) — if not, you could work around this limitation with the for loop above and a tag like {% if forloop.counter0 == 0 %}.
If you're tied to Django < 1.4, you can either override the render() method as suggested or go with the slightly-more-verbose-but-less-complicated option of building up the form field yourself in the template:
{% for choice in form.pc_type.field.choices %}
<input name='{{ form.pc_type.name }}'
id='{{ form.pc_type.auto_id }}_{{ forloop.counter0 }}' type='radio' value='{{ choice.0 }}'
{% if not form.is_bound %}{% ifequal form.pc_type.field.initial choice.0 %} checked='checked' {% endifequal %}
{% else %}{% ifequal form.pc_type.data choice.0 %} checked='checked' {% endifequal %}{% endif %}/>
<label for='{{ form.pc_type.auto_id }}_{{ forloop.counter0 }}'>{{ choice.1 }}</label>
{% endfor %}
(choice.0 and choice.1 are the first and second items in your choices two-tuple)
The rendering of the individual radio inputs is handled by the RadioSelect widget's render method. If you want a different rendering, subclass RadioSelect, change the render method accordingly, and then use your subclass as the field's widget.
I think the simply looking at what's available inside the for loop of a choice field will tell one what they need to know. For example, I needed the value to set a class surrounding the span of the option (for colors and such):
<div>
{% for radio_input in form.role %}
{# Skip the empty value #}
{% if radio_input.choice_value %}
<span class="user-level {{ radio_input.choice_value }}">{{ radio_input }}</span>
{% endif %}
{% endfor %}
</div>
There are several attributes as you can see that keep you from having to use the ordinal.
In Django 2.0+ you can subclass forms.RadioSelect and "simply" specify a template for rendering the radio fields:
class SlimRadioSelect(forms.RadioSelect):
template_name = 'includes/slim_radio.html'
where slim_radio.html contains a revised version of the combined template_name and option_template_name used by the default RadioSelect widget.
Note, the default RadioSelect widget template is low-level rendering and consists of heavily layered templates: include, conditional and loop logic tags abound.
You'll know you've arrived when you're digging around in packages/django/forms/templates/django/forms/widgets/input.html to get what you need.
One other oddity for overriding the default widget's template is that you must invoke the TemplatesSetting renderer or your subclass won't be able to find slim_radio.html in your project's normally accessible template paths.
To override RadioSelect's local-only template path lookup:
Add 'django.forms' to your INSTALLED_APPS;
Add FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' to your settings.py.
This all seems harder than it should be, but that's frameworks. Good luck.

Django template get custom attribute from request OR "refresh" radio buttons

I currently have a template containing an html form, with the lines:
{% for r in q1.responseoption_set.all %}
<span class="r"><input type="{{ q1.answer_type }}" name="r{{ r.id }}" id="r{{ forloop.counter }}"/>
<label {% if q1.answer_type == "text" %}class="textanswer"{% endif %}for="r{{ forloop.counter }}">{{ r.text }}</label></span><br>
{% endfor %}
problem is, because they don't all have the same name (that's why, right?), if I pick a radio button, and then switch to another one, the first one still shows as selected.
However, at the moment, I need them to all have different names because I need to be able to identify the choices within my view, and as far as I can tell, all I can get from the request is [name, value], e.g. [r200, "on"]
The only way around this that I can think of is to insert a script that assigns a check event to each button, and then, once checked, inserts a hidden input with the name I want, but that seems messy.
SO, is there a way for me to either:
get the button id from the request OR have the buttons "refresh" somehow as they are.
Keep the name the same, and set the value for each input choice to the answer id.
{% for r in q1.responseoption_set.all %}
<span class="r"><input type="{{ q1.answer_type }}" name="{% questionId %}" value="r{{ r.id }}" id="r{{ forloop.counter }}"/>
<label {% if q1.answer_type == "text" %}class="textanswer"{% endif %}for="r{{ forloop.counter }}">{{ r.text }}</label></span><br>
{% endfor %}

Get the ID of a field widget in a formset

I want to customize the layout of forms in a formset (that is, I don't want to use .as_table() or .as_p() and the like). I'm trying to get the name of a form field for use in its label's for attribute, but I'm not sure how to go about it. I'm hoping that I won't need to construct a new name/ID for the field from scratch. Here's an example of what I'm working with right now:
{% for form in formset.forms %}
<!-- The field for the "path" form field -->
<label for="{{what do I put here?}}">{{form.fields.path.label}}:</label><input type="text" id="{{django creates this one; do I have to do my own with the for loop counter or something?}}" name="{{probably the same as id}}" />
{% endfor %}
Is there any sort of "create ID for formset field" sort of method?
This is likely what you want.
for="{{ form.your_field.html_name }}"
First, you want to use the form element's id, instead of name.
I tried Django 1.3 Alpha-1 and the following worked:
{% for form in formset.forms %}
<label for="{{ form.my_field.auto_id }}">{{ form.my_field.label }}</label>
{{ form.my_field }}
{% endfor %}
Enjoy!