To render an indented list of categories with checkboxes, we will create and use a new MultipleChoiceTreeField form field and also create an HTML template for this field. The specific template will be passed to the crispy_forms layout in the form. To do this, perform the following steps:
- In the utils app, add a fields.py file (or update it if one already exists) and create a MultipleChoiceTreeField form field that extends ModelMultipleChoiceField, as follows:
# utils/fields.py
# ...other imports... from django import forms
# ...
class MultipleChoiceTreeField(forms.ModelMultipleChoiceField):
widget = forms.CheckboxSelectMultiple
def label_from_instance(self, obj):
return obj
- Use the new field with the categories to choose from in a new form for movie creation. Also, in the form layout, pass a custom template to the categories field, as shown in the following:
# movies/forms.py from django import forms from django.utils.translation import ugettext_lazy as _ from crispy_forms.helper import FormHelper from crispy_forms import layout, bootstrap
from utils.fields import MultipleChoiceTreeField from .models import Movie, Category
class MovieForm(forms.ModelForm):
class Meta:
model = Movie
categories = MultipleChoiceTreeField(
label=_("Categories"),
required=False,
queryset=Category.objects.all())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_action = ""
self.helper.form_method = "POST"
self.helper.layout = layout.Layout(
layout.Field("title"),
layout.Field(
"categories",
template="utils/checkbox_multi_select_tree.html"),
bootstrap.FormActions(
layout.Submit("submit", _("Save")),
)
)
- Create a template for a Bootstrap-style checkbox list, as shown in the following:
{# templates/utils/checkbox_multi_select_tree.html #}
{% load crispy_forms_filters %}
{% load l10n %}
<div id="div_{{ field.auto_id }}"
class="form-group{% if wrapper_class %}
{{ wrapper_class }}{% endif %}
{% if form_show_errors and field.errors %}
has-error{% endif %}
{% if field.css_classes %}
{{ field.css_classes }}{% endif %}">
{% if field.label and form_show_labels %}
<label for="{{ field.id_for_label }}"
class="control-label {{ label_class }}
{% if field.field.required %}
requiredField{% endif %}">
{{ field.label|safe }}{% if field.field.required %}
<span class="asteriskField">*</span>{% endif %}
</label>
{% endif %}
<div class="controls {{ field_class }}"{% if flat_attrs %}
{{ flat_attrs|safe }}{% endif %}>
{% include 'bootstrap3/layout/field_errors_block.html' %}
{% for choice_value, choice_instance
in field.field.choices %}
<label class="form-check checkbox{% if inline_class
%}-{{ inline_class }}{% endif %}
level-{{ choice_instance.level }}">
<input type="checkbox" class="form-check-input"
{% if choice_value in field.value
or choice_value|stringformat:'s'
in field.value
or choice_value|stringformat:'s' ==
field.value|stringformat:'s'
%} checked{% endif %}
name="{{ field.html_name }}"
id="id_{{field.html_name}}_{{forloop.counter}}"
value="{{ choice_value|unlocalize }}"
{{ field.field.widget.attrs|flatatt }}>
<span>{{ choice_instance }}</span>
</label>
{% endfor %}
{% include "bootstrap3/layout/help_text.html" %}
</div>
</div>
Template tags in the snippet above have been split across lines for legibility, but in practice template tags must be on a single line, and so cannot be split in this manner.
- Create a new view for adding a movie, using the form we just created:
# movies/views.py
# ...other imports...
from django.views.generic import FormView
from .forms import MovieForm
# ...
class MovieAdd(FormView):
template_name = 'movies/add_form.html'
form_class = MovieForm
success_url = '/'
- Add the associated template to show the Add Movie form with the {% crispy %} template tag, whose usage you can learn more about in the Creating a form layout with django-crispy-forms recipe in Chapter 3, Forms and Views:
{# templates/movies/add_form.html #}
{% extends "base.html" %}
{% load i18n static crispy_forms_tags %}
{% block stylesheet %}
<link rel="stylesheet" type="text/css"
href="{% static 'site/css/movie_add.css' %}">
{% endblock %}
{% block content %}
<h2>{% trans "Add Movie" %}</h2>
<div id="form_add_movie">
{% crispy form %}
</div>
{% endblock %}
- We also need a URL rule pointing to the new view, as follows:
# movies/urls.py
# ...other imports...
from django.urls import path
from .views import MovieAdd
# ...
urlpatterns = [
# ...
path('add/', views.MovieAdd.as_view(),
name="add_movie"),
]
- Add rules to your CSS file to indent the labels using the classes generated in the checkbox tree field template, such as .level-0, .level-1, and .level-2, by setting the margin-left parameter. Make sure that you have a reasonable amount of these CSS classes for the expected maximum depth of trees in your context, as follows:
/* static/site/movie_add.css */ .level-0 { margin-left: 0; } .level-1 { margin-left: 20px; } .level-2 { margin-left: 40px; }