Schönere Formulare mit Crispy Forms
Ein fast unverzichtbares Package in Django ist Crispy Forms
, vor allem, wenn
man an schönem Formular Layout interessiert ist. Die Installation hatten wir
schon in einem früheren Kapitel beschrieben. In diesem Kapitel wollen wir das
Package näher beleuchten und uns ein paar Features ansehen.
Template Packs
Vorab: Wir hatten Crispy Forms bereits mit dem Template Pack für das Bootstrap-Framework
genutzt, da wir in diesem Projekt auch auf Bootstrap5
setzen. Aber es werden von Haus aus noch viele andere CSS-Frameworks
unterstützt, zum Beispiel das modernere Bulma
oder TailwindCSS
. django-crispy-forms
lässt sich also in fast jedes CSS-Framework einbinden.
Eine Übersicht der Template Packs, die Crispy Forms bietet, findet sich in der Doku: https://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs
Der Crispy Template-Filter
Bisher hatten wir Crispy Forms in einer Variante kennengelernt, die dem Package
eigentlich nicht gerecht wird, aber trotzdem schon mächtig genug war, die Darstellung des
Formulars deutlich zu verbessern: der Crispy Template Filter
.
Zur Erinnerung, so in etwa sah das aus:
{% load crispy_forms_tags %}
<form method="post">
{{ form|crispy }}
</form>
Wir nutzen bisher das Pipe
-Symbol, um die form
-Variable an den Filter crispy
zu übergeben.
Crisyp Forms kann natürlich deutlich mehr.
Der Crispy Formhelper
Die FormHelper
-Klasse gibt uns eine pythonische Möglichkeit an die Hand,
das Layout und die Eigenschaften unserer Formularer so anzupassen, wie wir das
wünschen. Und zwar ohne viel HTML-Code direkt in die Templates schreiben zu müssen.
Öffnen wir nun die event_manager/events/forms.py
und passen das Formular
für das Event-Model etwas an.
from django import forms
from crispy_forms.helper import FormHelper
class EventForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
class Meta:
model = Event
exclude = ("category", "author")
Den FormHelper
haben wir aus crispy_forms.helper
importiert.
Der FormHelper in Action
Um nun eine Instanz der FormHelper
-Klasse zu erstellen, mussten wir die __init__
Methode überschrieben, damit erstmal die __init__
Methode der Elternklasse
ausgeführt werden kann. In unserer überschriebenen __init__
erstellen wir mit self.helper = FormHelper()
eine Instanz
des FormHelpers.
Mit dem FormHelper
können wir zum Beispiel die CSS-Klasse des Formulars
definieren, welches später im Template gerendert wird, ohne in das HTML händisch eingreifen zu müssen. Den Submit-Button müssen wir nicht -wie vorher- im
Formular angeben, sondern machen das bequem mit add_input
von hier aus.
Zusätzlich haben wir noch die HTTP-Methode im Formular auf POST
gesetzt, wie das für Web-Formulare üblich ist:
[..]
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
class EventForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
[..]
self.helper = FormHelper()
self.helper.form_method = 'POST'
self.helper.form_id = 'event_form'
self.helper.add_input(Submit('submit', 'Formular absenden'))
Darüberhinaus wird uns auch der HTML form
-Tag gerendert. Um den Form-Helper
nun zu nutzen, müssen wir das Template für das Event-Formular anpassen.
Den FormHelper im Formular nutzen
Um den Formhelper jetzt zu nutzen, öffnen wir die Template-Datei
events/templates/events/event_form.html
und tragen den folgenden Code ein:
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}
{% if object %}
Edit {{object.name}}
{% else %}
Event eintragen
{% endif %}
{%endblock%}
{% block head %}
{% if object %}
<p>
<a href="{% url 'events:event_detail' object.pk %}">zurück zum Event</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">{{object.name}}</h1>
{% else %}
<p>
<a href="{% url 'events:events' %}">zurück zur Übersicht</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">Event eintragen</h1>
{% endif %}
{%endblock%}
{% block content %}
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-12 mb-5">
{% crispy form form.helper %}
<div>
</div>
</div>
{% endblock %}
Wie wir sehen können, wurde das Template deutlich entschlackt. Alle HTML-Tags,
die wir vorher manuell in das Template geschrieben haben wie zum Beispiel
<form>
oder <button>
werden jetzt von Crispy Forms bzw. dem Formhelper
erstellt.
Das Formular sollte jetzt schön gerendert sein. Wenn wir uns den Quelltext des Formulars im Browser ansehen (rechter Mausklick, Quelltext ansehen),
sehen wir wunderschön das Formular mit CSRF-Feld
, Submit-Button
und den öffnenden und schließenden Formular-Tags
.
Die Einstellungen für Crispy Forms sind vielfältig. Wer mehr wissen möchte, sehe sich bitte zuerst die Doku an:
https://django-crispy-forms.readthedocs.io/en/latest/form_helper.html
Form Layouts
Mit dem FormHelper
ist es möglich, das Layout von Formularen einfach zu verändern, ohne
mühselig im HTML alles neu zu stylen. Wenn wir zum Beispiel ein zweispaltiges horizontales Layout wollten,
in dem links das Label und rechts die Felder stehen, würden wir unseren FormHelper um
ein Layout
erweitern:
[..]
from crispy_forms.layout import Submit, Layout
class EventForm(forms.ModelForm):
[..]
def __init__(self, *args, **kwargs):
[..]
# horizontales Formular mit zwei Spalten definieren.
# Einer Label-Spalte und einer Feld-Spalte
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-lg-2'
self.helper.field_class = 'col-lg-8'
self.helper.layout = Layout(
'name',
'description',
)
Nun wird ein zweispaltiges Formular mit der Bootstrap-Klasse
form-horizontal
gerendert, welches sich im Fall von sehr kleinen Endgeräten
responsive verhält:
Zum Thema Layouts gibt es noch vieles mehr zu sagen und viele Beispiele finden sich auch in der Doku.
https://django-crispy-forms.readthedocs.io/en/latest/layouts.html
Fieldsets in der Admin-Oberfläche
Bei komplexen Formularen kann es sinnvoll sein, diese in verschiedene
Abschnitte einzuteilen. HTML bietet ein fieldset
- Element an, mit dem sich
Formulare strukturieren lassen.
Django macht das schon von Haus aus in der Adminoberfläche.
Dort gibt es die admin.fieldsets
, die dazu dienen, Formulare semantisch zu strukturieren.
Hier mal ein Beispiel, wie Django das User-Model in der Admin-Oberfläche
darstellt. Die Einteilung der Formularbereiche sind die fieldsets
.
Wenn wir unser Event-Model etwas anders strukturieren wollten, könnten wir das
in der event_manager/events/admin.py
wie folgt festlegen:
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
search_fields = ["name"]
list_filter = ("category",)
list_display = "author", "name", "category", "is_active"
fieldsets = (
("Standard info", {"fields": ("name", "date", "category")}),
(
"Detail Infos",
{"fields": ("description", "min_group", "sub_title", "is_active")},
),
)
Wir haben zwei Fieldsets angelegt, die das Formular in zwei Bereiche unterteilt: die Standard Infos und die Detail-Infos.
Fieldsets in gewöhnlichen Formularen
Leider bietet Django aber für normale Formulare keine Möglichkeit an, Formulare mit fieldsets unkompliziert zu strukturieren. Man müsste sich die einzelnen Formularfelder rendern lassen und das Formular selber nachbauen. Eine aufwändige und fehleranfällige Arbeit.
Hier kommt uns django-crispy-forms
zur Rettung!
Crispy-Forms ermöglicht beim Definieren eines Layouts sogenannte Fieldset
.
Damit lassen sich zusammengehörige Bereiche auch in normalen Formularen sinnvoll abtrennen. Das erste Argument für die Fieldset-Klasse ist der Name des Bereichs, der als Label gerendert wird. Im Beispiel steht hier dann Standard Infos oder Detail Infos.
Die weiteren Argumente wie name
oder date
beziehen sich hier im
Beispiel auf die Attribute des Event-Models.
[..]
from crispy_forms.layout import Submit, Layout, Fieldset, HTML
class EventForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
[..]
self.helper.layout = Layout(
Fieldset(
"Standard Infos",
"name",
"date",
"category",
css_class="form-group",
),
Fieldset(
"Detail Infos",
"description",
"min_group",
"sub_title",
"is_active",
HTML(
"""
<div class='mb-3 row'>
<div class='col-lg-2'></div>
<div class='col-lg-8'>Danke für die Mitarbeit, <strong>Wir
finden das ganz toll!</strong></div>
</div>
"""
),
css_class="form-group",
),
)
Wir haben jetzt auch hier das Formular in zwei Bereich geteilt und zudem
jedem Formset noch die Bootstrap-Klasse form-group
zugeteilt.
Hier wurde jetzt noch das mächtige HTML
-Element genutzt, dass es uns erlaubt,
beliebigen HTML-Code in das Formular zu schleusen.
Unser fertiges Event-Form sieht nun so aus:
from django import forms
from django.core.exceptions import ValidationError
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Fieldset, HTML
from .models import Category, Event
class CategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = "__all__"
class EventForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = "POST"
self.helper.form_id = "event_form"
self.helper.form_class = "form-horizontal"
self.helper.label_class = "col-lg-2"
self.helper.field_class = "col-lg-8"
self.helper.add_input(Submit("submit", "Submit"))
self.helper.layout = Layout(
Fieldset(
"Standard Infos",
"name",
"date",
"category",
css_class="form-group",
),
Fieldset(
"Detail Infos",
"description",
"min_group",
"sub_title",
"is_active",
HTML(
"""
<div class='mb-3 row'>
<div class='col-lg-2'></div>
<div class='col-lg-8'>Danke für die Mitarbeit, <strong>Wir
finden das ganz toll!</strong></div>
</div>
"""
),
css_class="form-group",
),
)
class Meta:
model = Event
fields = "__all__"
exclude = ("category", "author")
widgets = {
"date": forms.DateInput(
format=("%Y-%m-%d %H:%M"), attrs={"type": "datetime-local"}
),
}
labels = {
"name": "Name des Events",
"sub_title": "Slogan",
"description": "Kurzbeschreibung",
"date": "Datum des Events",
}
def clean_sub_title(self) -> str:
sub_title = self.cleaned_data["sub_title"]
illegal = ("*", "-", ":")
if isinstance(sub_title, str) and sub_title.startswith(illegal):
raise ValidationError("Dieses Zeichen ist nicht erlaubt!")
return sub_title
Den Kategorienamen ausgeben
Einen kleinen Schönheitsfehler hat unser Event-Eintragen Formular aber noch: der Name der Kategorie, für die das Event eingetragen wird, wird an keiner Stelle im Formular erwähnt. Für den User ist das ungünstig, weil er hier im Unklaren gelassen wird, für welche Kategorie er einen Event einträgt.
Um diese Aufgabe zu Bewerkstelligen, muss unsere EventCreateView angepasst werden. Sie muss im Kontext das Category-Objekt mit an das Template übergeben, damit diese Information beim Eintragen darsgestellt werden kann.
Dafür überschreiben wir zuerst die get_context_data
- Methode der
EventCreateView-Klasse. Mit ihr ist es möglich, bei klassenbasierten Views den
Kontext, der an das Template zum Rendern weitergereicht wird, zu manipulieren.
Wir machen hier nichts weiter, als dem Context-Dictionary das category
- Objekt, dass wir uns in
der Methode get_initial
geholt hatten, zu übergeben.
class EventCreateView(LoginRequiredMixin, CreateView):
"""
create an event for a certain category
events/event/create/3
"""
model = Event
form_class = EventForm
def form_valid(self, form):
form.instance.category = self.category
form.instance.author = self.request.user
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["category"] = self.category
return context
def get_initial(self):
self.category = get_object_or_404(Category, pk=self.kwargs["category_id"])
Um diese Information noch im Template zur Verfügung zu stellen, passen wir unser Template unter
event_manager/events/event_form.html
an. Im head-Bereich tragen wir
folgendes ein:
{% block head %}
{% if object %}
<p>
<a href="{% url 'events:event_detail' object.pk %}">zurück zum Event</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">{{object.name}}</h1>
{% else %}
<p>
<a href="{% url 'events:events' %}">zurück zur Übersicht</a><br>
</p>
<h1 class="display-5 fw-bold lh-1">Event in {{category}} eintragen</h1>
{% endif %}
{%endblock%}
In den geschweiften Klammern geben wir jetzt die category
- Variable, die wir
über get_context_data
dem Kontext übergeben haben wieder aus. Unser
Form-Template in event_manager/events/event_form.html
sieht jetzt so aus:
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}
{% if object %}
Edit {{object.name}}
{% else %}
Event in {{category}} eintragen
{% endif %}
{%endblock%}
{% block head %}
{% if object %}
<p>
<a href="{% url 'events:event_detail' object.pk %}">zurück zum Event</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">{{object.name}}</h1>
{% else %}
<p>
<a href="{% url 'events:events' %}">zurück zur Übersicht</a><br>
</p>
<h1 class="display-5 fw-bold lh-1">Event in {{category}} eintragen</h1>
{% endif %}
{%endblock%}
{% block content %}
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-12 mb-5">
{% crispy form form.helper %}
<div>
</div>
</div>
{% endblock %}
Im Formular sehen wir jetzt, dass die Kategorie angegeben wird, in der das Event eingetragen wird:
Optional: Das Projekt in der Version v0.4 kopieren
Wie immer kann man sich den Zwischenstand klonen, falls beim Mitschreiben was schiefgegangen ist.
Version v0.4 via github clonen
git clone -b v0.4 git@github.com:realcaptainsolaris/event_project.git
und dann nicht vergessen, ein virtuelles Environment anzulegen und zu migrieren und danach noch ein paar Testdaten zu generieren:
python -m venv .envs/eventenv
pip install pip-tools
pip-sync requirements.txt requirements-dev.txt
python manage.py migrate
python manage.py createsuperuser
python manage.py create_user -n 10
python manage.py create_events --events 20 --categories 5
python manage.py runserver
Version v0.4 als zip-Datei runterladen
https://github.com/realcaptainsolaris/event_project/archive/refs/tags/v0.4.zip
Version v0.4 als tar-Datei runterladen
https://github.com/realcaptainsolaris/event_project/archive/refs/tags/v0.4.tar.gz