Django-Formulare
Als Letztes möchten wir auf unserer Website noch die Möglichkeit haben, Blogposts hinzuzufügen und zu editieren. Die Django admin
-Oberfläche ist cool, aber eher schwierig anzupassen und hübsch zu machen. Mit Formularen, forms
, haben wir die absolute Kontrolle über unser Interface - wir können fast alles machen, was man sich vorstellen kann!
Das Gute an Django-Forms ist, dass man sie entweder vollständig selbst definieren oder eine ModelForm
erstellen kann, welche den Inhalt des Formulars in das Model speichert.
Genau das wollen wir jetzt machen: Wir erstellen ein Formular für unser Post
-Model.
So wie die anderen wichtigen Django-Komponenten haben auch die Forms ihre eigene Datei: forms.py
.
Wir erstellen nun eine Datei mit diesem Namen im blog
-Verzeichnis.
blog
└── forms.py
So, jetzt lass uns diese im Code-Editor öffnen und folgenden Code hinzufügen:
blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'text',)
Zuerst müssen wir die Django-Forms importieren (from django import forms
) und auch unser Post
-Model (from .models import Post
).
Wie du wahrscheinlich schon vermutet hast, PostForm
ist der Name unseres Formulars. Wir müssen Django mitteilen, dass unser Formular ein ModelForm
ist (so kann Django ein bisschen für uns zaubern) - forms.ModelForm
ist dafür verantwortlich.
Als Nächstes sehen wir uns class Meta
an, damit sagen wir Django, welches Model benutzt werden soll, um das Formular zu erstellen (model = Post
).
Nun können wir bestimmen, welche(s) Feld(er) unser Formular besitzen soll. Wir wollen hier nur den title
und text
sichtbar machen - der author
sollte die Person sein, die gerade eingeloggt ist (Du!) und created_date
sollte automatisch generiert werden, wenn der Post erstellt wird (also im Code). Stimmt's?
Und das war's schon! Jetzt müssen wir das Formular nur noch in einem view benutzen und im Template darstellen.
Also erstellen wir hier auch wieder einen Link auf die Seite, eine URL, eine View und ein Template.
Link auf eine Seite mit dem Formular
Bevor wir den Link hinzufügen, benötigen wir einige Icons als Buttons für den Link. Lade für dieses Tutorial file-earmark-plus.svg herunter und speicher es im Ordner blog/templates/blog/blog/icons/
Hinweis: Um das SVG-Bild herunterzuladen, öffne das Kontextmenü auf dem Link (normalerweise durch einen Rechtsklick darauf) und wähle "Link speichern unter". Im Dialog, in dem du gefragt wirst, wo du die Datei speichern willst, navigiere zum
djangogirls
-Verzeichnis deines Django-Projekts und innerhalb davon in das Unterverzeichnisblog/templates/blog/icons/
und speicher die Datei dort.
Es ist an der Zeit, blog/templates/blog/base.html
im Code-Editor zu öffnen. Jetzt können wir diese Icon-Datei im Basis-Template wie folgt verwenden. Im div
-Element innerhalb des header
-Abschnitts werden wir einen Link vor dem h1
-Element hinzufügen:
blog/templates/blog/base.html
<a href="{% url 'post_new' %}" class="top-menu">
{% include './icons/file-earmark-plus.svg' %}
</a>
Beachte, dass wir unsere neue View post_new
nennen wollen. Das SVG-Icon wird von Bootstrap Icons zur Verfügung gestellt und zeigt ein Seitensymbol mit Pluszeichen an. Wir verwenden eine Django-Template-Direktive namens include
. Dadurch wird der Inhalt der Datei in das Django-Template eingefügt. Der Web-Browser weiß, wie man diese Art von Inhalt ohne weitere Verarbeitung handhabt.
Alle Bootstrap-Icons kannst du hier herunterladen. Entpacke die Datei und kopiere alle SVG-Bilddateien in einen neuen Ordner namens
icons
innerhalb vonblog/templates/blog/
. So kannst du auf ein Symbol wiepencil-fill.svg
mit dem Dateipfadblog/templates/blog/icons/pencil-fill.svg
zugreifen
Nach dem Bearbeiten der Zeile sieht deine HTML-Datei so aus:
blog/templates/blog/base.html
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>Django Girls blog</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<link href='//fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{% static 'css/blog.css' %}">
</head>
<body>
<header class="page-header">
<div class="container">
<a href="{% url 'post_new' %}" class="top-menu">
{% include './icons/file-earmark-plus.svg' %}
</a>
<h1><a href="/">Django Girls Blog</a></h1>
</div>
</header>
<main class="content container">
<div class="row">
<div class="col">
{% block content %}
{% endblock %}
</div>
</div>
</main>
</body>
</html>
Nach dem Speichern und Neuladen von http://127.0.0.1:8000 solltest du den bereits bekannten NoReverseMatch
-Fehler sehen. Ist dem so? Gut!
URL
Wir öffnen blog/urls.py
im Code-Editor und fügen eine Zeile hinzu:
blog/urls.py
path('post/new/', views.post_new, name='post_new'),
Der finale Code sieht dann so aus:
blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.post_list, name='post_list'),
path('post/<int:pk>/', views.post_detail, name='post_detail'),
path('post/new/', views.post_new, name='post_new'),
]
Nach dem Neuladen der Site sehen wir einen AttributeError
, weil wir noch keine post_new
-View eingefügt haben. Fügen wir sie gleich hinzu!
Die post_new-View
Jetzt wird es Zeit, die Datei blog/views.py
im Code-Editor zu öffnen und die folgenden Zeilen zu den anderen from
Zeilen hinzuzufügen:
blog/views.py
from .forms import PostForm
Und dann unsere View:
blog/views.py
def post_new(request):
form = PostForm()
return render(request, 'blog/post_edit.html', {'form': form})
Um ein neues PostForm
zu erstellen, rufen wir PostForm()
auf und übergeben es an das Template. Wir kommen gleich nochmal zu dem View zurück, aber jetzt erstellen wir schnell ein Template für das Form.
Template
Wir müssen eine Datei post_edit.html
im Verzeichnis blog/templates/blog
erstellen und im Code-Editor öffnen. Damit ein Formular funktioniert, benötigen wir einige Dinge:
- Wir müssen das Formular anzeigen. Wir können das zum Beispiel mit einem einfachen `` tun.
- Die Zeile oben muss von einem HTML-Formular-Element eingeschlossen werden
<form method="POST">...</form>
. - Wir benötigen einen
Save
-Button. Wir erstellen diesen mit einem HTML-Button:<button type="submit">Save</button>
. - Und schließlich fügen wir nach dem öffnenden
<form ...>
Tag{% csrf_token %}
hinzu. Das ist sehr wichtig, da es deine Formulare sicher macht! Wenn du diesen Teil vergisst, wird sich Django beim Speichern des Formulars beschweren.
Ok, also schauen wir mal, wie der HTML-Code in post_edit.html
aussehen sollte:
blog/templates/blog/post_edit.html
{% extends 'blog/base.html' %}
{% block content %}
<h2>New post</h2>
<form method="POST" class="post-form">{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="save btn btn-default">Save</button>
</form>
{% endblock %}
So, jetzt aktualisieren wir die Seite! Yay! Das Formular wird angezeigt!
Aber Moment! Wenn du in das title
- oder text
-Feld etwas eintippst und versuchst es zu speichern - was wird wohl passieren?
Nichts! Wir landen wieder auf der selben Seite und unser Text ist verschwunden... und kein neuer Post wurde hinzugefügt. Was lief denn hier schief?
Die Antwort ist: nichts. Wir müssen einfach noch etwas mehr Arbeit in unsere View stecken.
Speichern des Formulars
Öffne blog/views.py
erneut im Code-Editor. Derzeit ist alles, was wir in der View post_new
haben, das hier:
blog/views.py
def post_new(request):
form = PostForm()
return render(request, 'blog/post_edit.html', {'form': form})
Wenn wir das Formular übermitteln, werden wir zur selben Ansicht weitergeleitet, aber dieses Mal haben wir mehr Daten in request
, genauer in request.POST
(der Name hat nichts zu tun mit einem "Blogpost", sondern damit, dass wir Daten "posten"). Erinnerst du dich daran, dass in der HTML-Datei unsere <form>
Definition die Variable method="POST"
hatte? Alle Felder aus dem Formular sind jetzt in request.POST
. Du solltest POST
nicht umbenennen (der einzige andere gültige Wert für method
ist GET
, wir wollen hier jetzt aber nicht auf den Unterschied eingehen).
Somit müssen wir uns in unserer View mit zwei verschiedenen Situationen befassen: erstens, wenn wir das erste Mal auf die Seite zugreifen und ein leeres Formular wollen und zweitens, wenn wir zur View mit allen soeben ausgefüllten Formular-Daten zurück wollen. Wir müssen also eine Bedingung hinzufügen (dafür verwenden wir if
):
blog/views.py
if request.method == "POST":
[...]
else:
form = PostForm()
Es wird Zeit, die Lücken zu füllen [...]
. Falls die Methode
POST
ist, wollen wir das PostForm
mit Daten vom Formular erstellen. Oder? Das machen wir folgendermaßen:
blog/views.py
form = PostForm(request.POST)
Als Nächstes müssen wir testen, ob das Formular korrekt ist (alle benötigten Felder sind ausgefüllt und keine ungültigen Werte werden gespeichert). Wir tun das mit form.is_valid()
.
Wir überprüfen also, ob das Formular gültig ist und wenn ja, können wir es speichern!
blog/views.py
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.published_date = timezone.now()
post.save()
Im Grunde passieren hier zwei Dinge: Wir speichern das Formular mit form.save
und wir fügen einen Autor hinzu (da es bislang kein author
Feld in der PostForm
gab und dieses Feld notwendig ist). commit=False
bedeutet, dass wir das Post
Model noch nicht speichern wollen - wir wollen erst noch den Autor hinzufügen. Meistens wirst du form.save()
ohne commit=False
benutzen, aber in diesem Fall müssen wir es so tun. post.save()
wird die Änderungen sichern (den Autor hinzufügen) und ein neuer Blogpost wurde erstellt!
Wäre es nicht grossartig, wenn wir direkt zu der post_detail
Seite des neu erzeugten Blogposts gehen könnten? Um dies zu tun, benötigen wir noch einen zusätzlichen Import:
blog/views.py
from django.shortcuts import redirect
Füge dies direkt am Anfang der Datei hinzu. Jetzt können wir endlich sagen: "Gehe zu der post_detail
Seite unseres neu erstellten Posts":
blog/views.py
return redirect('post_detail', pk=post.pk)
post_detail
ist der Name der View, zu der wir springen wollen. Erinnerst du dich daran, dass diese view einen pk
benötigt? Um diesen an die View weiterzugeben, benutzen wir pk=post.pk
, wobei post
unser neu erstellter Blogpost ist!
Ok, wir haben jetzt eine ganze Menge geredet, aber du willst bestimmt sehen, wie die gesamte View aussieht, richtig?
blog/views.py
def post_new(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.published_date = timezone.now()
post.save()
return redirect('post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_edit.html', {'form': form})
Schauen wir mal, ob es funktioniert. Gehe zur Seite http://127.0.0.1:8000/post/new/, füge einen title
und text
hinzu und speichere es...voilà! Der neue Blogpost wird hinzugefügt und wir werden auf die post_detail
Seite umgeleitet!
Du hast vielleicht bemerkt, dass wir das Veröffentlichungsdatum festlegen, bevor wir den Post veröffentlichen. Später werden wir einen publish button in Django Girls Tutorial: Extensions einführen.
Das ist genial!
Da wir vor Kurzem das Django-Admin-Interface benutzt haben, denkt das System, dass wir noch angemeldet sind. Es gibt einige Situationen, welche dazu führen können, dass wir ausgeloggt werden (Schließen des Browsers, Neustarten der Datenbank etc). Wenn du feststellst, dass du bei dem Erstellen von Posts Fehlermeldungen bekommst, die auf nicht angemeldete Nutzer zurückzuführen sind, dann gehe zur Admin Seite http://127.0.0.1:8000/admin und logge dich erneut ein. Dies wird das Problem vorübergehend lösen. Es gibt eine permanente Lösung dafür, die im Kapitel Homework: add security to your website! nach dem Haupttutorial auf dich wartet.
Formularvalidierung
Jetzt zeigen wir dir, wie cool Django-Formulare sind. Ein Blogpost muss title
- und text
-Felder besitzen. In unserem Post
-Model haben wir (im Gegensatz zu dem published_date
) nicht festgelegt, dass diese Felder nicht benötigt werden, also nimmt Django standardmäßig an, dass sie definiert werden.
Versuch, das Formular ohne title
und text
zu speichern. Rate, was passieren wird!
Django kümmert sich darum sicherzustellen, dass alle Felder in unserem Formular richtig sind. Ist das nicht großartig?
"Bearbeiten"-Formular
Jetzt wissen wir, wie ein neuer Blogpost hinzugefügt wird. Aber was ist, wenn wir einen bereits bestehenden bearbeiten wollen? Das funktioniert so ähnlich wie das, was wir gerade getan haben. Lass uns schnell ein paar wichtige Dinge erstellen. (Falls du etwas nicht verstehst, solltest du deinen Coach fragen oder in den vorherigen Kapiteln nachschlagen, da wir all die Schritte bereits behandelt haben.)
Lass uns zunächst das Symbol speichern, das den Bearbeiten-Button darstellt. Lade pencil-fill.svg herunter und speichere es in blog/templates/blog/icons/
.
Öffne blog/templates/blog/post_detail.html
im Code-Editor und füge folgenden Code innerhalb des Elements article
hinzu:
blog/templates/blog/post_detail.html
<aside class="actions">
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}">
{% include './icons/pencil-fill.svg' %}
</a>
</aside>
damit die Vorlage so aussieht:
blog/templates/blog/post_detail.html
{% extends 'blog/base.html' %}
{% block content %}
<article class="post">
<aside class="actions">
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}">
{% include './icons/pencil-fill.svg' %}
</a>
</aside>
{% if post.published_date %}
<time class="date">
{{ post.published_date }}
</time>
{% endif %}
<h2>{{ post.title }}</h2>
<p>{{ post.text|linebreaksbr }}</p>
</article>
{% endblock %}
Öffne blog/urls.py
im Code-Editor und fügen diese Zeile hinzu:
blog/urls.py
path('post/<int:pk>/edit/', views.post_edit, name='post_edit'),
Wir werden die Vorlage blog/templates/blog/post_edit.html
wiederverwenden, daher ist das einzig Fehlende eine neue View.
Öffne blog/views.py
im Code-Editor und füge ganz am Ende der Datei Folgendes hinzu:
blog/views.py
def post_edit(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == "POST":
form = PostForm(request.POST, instance=post)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.published_date = timezone.now()
post.save()
return redirect('post_detail', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_edit.html', {'form': form})
Sieht genauso aus wie unsere post_new
-View, oder? Aber nicht ganz. Zum einen übergeben wir einen zusätzliche pk
-Parameter aus urls
. Und: Wir bekommen das Post
-Model, welches wir bearbeiten wollen, mit get_object_or_404(Post, pk=pk)
und wenn wir dann ein Formular erstellen, übergeben wir diesen Post als instance
, wenn wir das Formular speichern…
blog/views.py
form = PostForm(request.POST, instance=post)
als auch, wenn wir ein Formular mit post zum Editieren öffnen:
blog/views.py
form = PostForm(instance=post)
Ok, lass uns mal schauen, ob das funktioniert! Geh auf die post_detail
-Seite. Dort sollte sich ein Editier-Button in der oberen rechten Ecke befinden:
Wenn du darauf klickst, siehst du das Formular mit unserem Blogpost:
Probier doch einmal, den Titel oder den Text zu ändern und die Änderungen zu speichern!
Herzlichen Glückwunsch! Deine Anwendung nimmt immer mehr Gestalt an!
Falls du mehr Informationen über Django-Formulare benötigst, solltest du die offizielle Dokumentation lesen: https://docs.djangoproject.com/en/2.2/topics/forms/
Sicherheit
Neue Posts durch Klick auf einen Link zu erstellen ist großartig! Aber im Moment ist jeder, der deine Seite besucht, in der Lage, einen neuen Blogpost zu veröffentlichen, und das ist etwas, was du garantiert nicht willst. Lass es uns so machen, dass der Button für dich angezeigt wird, aber für niemanden sonst.
Öffne die Datei blog/templates/blog/base.html
im Code-Editor, finde darin unseren header
und das Anchor-Element, welches du zuvor eingefügt hast. Es sollte so aussehen:
blog/templates/blog/base.html
<a href="{% url 'post_new' %}" class="top-menu">
{% include './icons/file-earmark-plus.svg' %}
</a>
Wir fügen ein weiteres {% if %}
-Tag ein, was dafür sorgt, dass der Link nur für angemeldete Nutzer angezeigt wird. Im Moment bist das also nur du! Ändere das <a>
-Element zu Folgendem:
blog/templates/blog/base.html
{% if user.is_authenticated %}
<a href="{% url 'post_new' %}" class="top-menu">
{% include './icons/file-earmark-plus.svg' %}
</a>
{% endif %}
Dieses {% if %}
sorgt dafür, dass der Link nur zu dem Browser geschickt wird, wenn der anfragende Nutzer auch angemeldet ist. Das verhindert das Erzeugen neuer Posts nicht komplett, ist aber ein sehr guter erster Schritt. In der Erweiterungslektion kümmern wir uns ausgiebiger um Sicherheit.
Erinnerst du dich an den Editier-Button, den wir gerade zu unserer Seite hinzugefügt haben? Wir wollen dort dieselbe Anpassung machen, damit andere Leute keine existierenden Posts verändern können.
Öffne blog/templates/blog/post_detail.html
im Code-Editor und finde folgende Zeile:
blog/templates/blog/post_detail.html
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}">
{% include './icons/pencil-fill.svg' %}
</a>
Ändere es wie folgt:
blog/templates/blog/post_detail.html
{% if user.is_authenticated %}
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}">
{% include './icons/pencil-fill.svg' %}
</a>
{% endif %}
Da du wahrscheinlich angemeldet bist, wirst du beim Refresh der Seite keinen Veränderung feststellen. Lade jedoch die Seite in einem anderen Browser oder einem Inkognito-Fenster ("In Private" im Windows Edge) und du wirst sehen, dass dieser Link nicht auftaucht und das Icon ebenfalls nicht angezeigt wird!
Eins noch: Zeit für das Deployment!
Mal sehen, ob das alles auch auf PythonAnywhere funktioniert. Zeit für ein weiteres Deployment!
- Commite als Erstes deinen neuen Code und schiebe ihn auf GitHub:
command-line
$ git status
$ git add .
$ git status
$ git commit -m "Added views to create/edit blog post inside the site."
$ git push
- Dann führe Folgendes in der PythonAnywhere Bash-Konsole aus:
PythonAnywhere command-line
$ cd ~/<deine-pythonanywhere-domain>.pythonanywhere.com
$ git pull
[...]
(Denk daran, <deine-pythonanywhere-domain>
durch deine tatsächliche PythonAnywhere-Subdomain zu ersetzen - ohne die spitzen Klammern.)
- Gehe schließlich noch rüber auf die Seite "Web" (benutze den Menü-Knopf in der rechten oberen Ecke der Konsole) und klicke Reload. Lade deinen Blog https://subdomain.pythonanywhere.com neu, um die Änderungen zu sehen.
Und das war's. Glückwunsch! :)