8. Working with forms - LiVanych/locallibrary GitHub Wiki
Django Form Control Process
Next, we're going to add a page to allow librarians to renew borrowed books. To do this we'll create
a form that allows users to enter a date value. We'll seed the field with an initial value 3 weeks
from the current date (the normal borrowing period), and add some validation to ensure that the
librarian can't enter a date in the past or a date too far in the future. When a valid date has
been entered, we'll write it to the current record's BookInstance.due_back
field.
The example will use a function-based view and a Form class. The following sections explain how forms work, and the changes you need to make to our ongoing LocalLibrary project.
The Form class is the heart of Django's form handling system. It specifies the fields in the form, their layout, display widgets, labels, initial values, valid values, and (once validated) the error messages associated with invalid fields. The class also provides methods for rendering itself in templates using predefined formats (tables, lists, etc.) or for getting the value of any element (enabling fine-grained manual rendering).
To create a Form, we import the forms library, derive from the Form class, and declare the form's
fields. A very basic form class for our library book renewal form is shown below:
Create and open the file locallibrary/catalog/forms.py
:
from django import forms
class RenewBookForm(forms.Form):
renewal_date = forms.DateField(
help_text="Enter a date between now and 4 weeks (default 3).")
Create and open the file locallibrary/catalog/forms.py
and copy the entire code listing from the
previous block into it.
import datetime
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
class RenewBookForm(forms.Form):
renewal_date = forms.DateField(
help_text="Enter a date between now and 4 weeks (default 3).")
def clean_renewal_date(self):
data = self.cleaned_data['renewal_date']
# Check if a date is not in the past.
if data < datetime.date.today():
raise ValidationError(_('Invalid date - renewal in past'))
# Check if a date is in the allowed range (+4 weeks from today).
if data > datetime.date.today() + datetime.timedelta(weeks=4):
raise ValidationError(_(
'Invalid date - renewal more than 4 weeks ahead'))
# Remember to always return the cleaned data.
return data
Before we create our view, let's add a URL configuration for the renew-books page. Copy the
following configuration to the bottom of locallibrary/catalog/urls.py
.
urlpatterns += [
path('book/<uuid:pk>/renew/', views.renew_book_librarian,
name='renew-book-librarian'),
]
The URL configuration will redirect URLs with the format /catalog/book/<bookinstance id>/renew/
to the function named renew_book_librarian()
in views.py
, and send the BookInstance
id as the
parameter named pk
. The pattern only matches if pk
is a correctly formatted uuid
.
Note: We can name our captured URL data "pk" anything we like, because we have complete control over the view function (we're not using a generic detail view class that expects parameters with a certain name). However, pk short for "primary key", is a reasonable convention to use!
The final view is therefore as shown below. Please copy this into the bottom of locallibrary/catalog/views.py
.
import datetime
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse
from catalog.forms import RenewBookForm
@permission_required('catalog.can_mark_returned')
def renew_book_librarian(request, pk):
"""View function for renewing a specific BookInstance by librarian."""
book_instance = get_object_or_404(BookInstance, pk=pk)
# If this is a POST request then process the Form data
if request.method == 'POST':
# Create a form instance and populate it
# with data from the request (binding):
form = RenewBookForm(request.POST)
# Check if the form is valid:
if form.is_valid():
# process the data in form.cleaned_data as required (here
# we just write it to the model due_back field)
book_instance.due_back = form.cleaned_data['renewal_date']
book_instance.save()
# redirect to a new URL:
return HttpResponseRedirect(reverse('all-borrowed') )
# If this is a GET (or any other method) create the default form.
else:
proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
form = RenewBookForm(initial={'renewal_date': proposed_renewal_date})
context = {
'form': form,
'book_instance': book_instance,
}
return render(request, 'catalog/book_renew_librarian.html', context)
Create the template referenced in the view (/catalog/templates/catalog/book_renew_librarian.html
)
and copy the code below into it:
{% extends "base_generic.html" %}
{% block content %}
<h1>Renew: {{ book_instance.book.title }}</h1>
<p>Borrower: {{ book_instance.borrower }}</p>
<p{% if book_instance.is_overdue %} class="text-danger"{% endif %}>
Due date: {{ book_instance.due_back }}
</p>
<form action="" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Submit">
</form>
{% endblock %}
If you accepted the "challenge" in "Django Tutorial Part 7: User authentication and permissions" you'll have a list of all books on loan in the library, which is only visible to library staff. We can add a link to our renew page next to each item using the template code below.
{% if perms.catalog.can_mark_returned %}
- <a href="{% url 'renew-book-librarian' bookinst.id %}">
Renew
</a>
{% endif %}
Note: Remember that your test login will need to have the permission "catalog.can_mark_returned
" in order to access the renew book page (perhaps use your superuser account).
You can alternatively manually construct a test URL like this —
http://127.0.0.1:8000/catalog/book/<bookinstance_id>/renew/
(a valid bookinstance id can be obtained
by navigating to a book detail page in your library, and copying the id field).
In this section we're going to use generic editing views to create pages to add functionality to create, edit, and delete Author records from our library — effectively providing a basic reimplementation of parts of the Admin site (this could be useful if you need to offer admin functionality in a more flexible way that can be provided by the admin site).
Open the views file (locallibrary/catalog/views.py
) and append the following code block to the
bottom of it:
from django.views.generic.edit import CreateView,UpdateView,DeleteView
from django.urls import reverse_lazy
from catalog.models import Author
class AuthorCreate(CreateView):
model = Author
fields = '__all__'
initial = {'date_of_death': '05/01/2018'}
class AuthorUpdate(UpdateView):
model = Author
fields = ['first_name', 'last_name', 'date_of_birth', 'date_of_death']
class AuthorDelete(DeleteView):
model = Author
success_url = reverse_lazy('authors')
The "create
" and "update
" views use the same template by default, which will be named after
your model: model_name_form.html (you can change the suffix to something other than _form
using the template_name_suffix field
in your view, e.g. template_name_suffix = '_other_suffix'
)
Create the template file locallibrary/catalog/templates/catalog/author_form.html
and copy
in the text below.
{% extends "base_generic.html" %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Submit">
</form>
{% endblock %}
The "delete" view expects to find a template named with the format model_name_confirm_delete.html
(again, you can change the suffix using template_name_suffix in your view). Create the template
file locallibrary/catalog/templates/catalog/author_confirm_delete.html
and copy in the text below.
{% extends "base_generic.html" %}
{% block content %}
<h1>Delete Author</h1>
<p>Are you sure you want to delete the author: {{ author }}?</p>
<form action="" method="POST">
{% csrf_token %}
<input type="submit" value="Yes, delete.">
</form>
{% endblock %}
Open your URL configuration file (locallibrary/catalog/urls.py
) and add the following configuration
to the bottom of the file:
urlpatterns += [
path('author/create/', views.AuthorCreate.as_view(),
name='author_create'),
path('author/<int:pk>/update/', views.AuthorUpdate.as_view(),
name='author_update'),
path('author/<int:pk>/delete/', views.AuthorDelete.as_view(),
name='author_delete'),
]
Change as specified bellow in catalog/views.py
...
class AuthorCreate(
PermissionRequiredMixin,CreateView):
permission_required = 'catalog.can_mark_returned'
model = Author
fields = '__all__'
initial = {'date_of_death': '05/01/2018'}
class AuthorUpdate(PermissionRequiredMixin,UpdateView):
permission_required = 'catalog.can_mark_returned'
model = Author
fields = ['first_name', 'last_name', 'date_of_birth', 'date_of_death']
class AuthorDelete(PermissionRequiredMixin,DeleteView):
permission_required = 'catalog.can_mark_returned'
model = Author
success_url = reverse_lazy('authors')
Add this bolded line into catalog/templates/base_generic.html
...
{% if user.is_staff %}
<hr />
<ul class="sidebar-nav">
<li>Staff</li>
{% if perms.catalog.can_mark_returned %}
<li><a href="{% url 'all-borrowed' %}">All borrowed</a></li>
<li><a href="{% url 'author_create' %}">Add author</a></li>
{% endif %}
</ul>
{% endif %}
...
Add 2 symlinks into catalog/templates/catalog/author_detail.html
...
{% block content %}
<h1>Author: {{ author.first_name }} {{ author.last_name }}</h1>
<p>{{ author.date_of_birth }}
{% if author.date_of_death %}
- {{ author.date_of_death }}
{% endif %}
</p>
<p>{% if perms.catalog.can_mark_returned %}
<a href="{% url 'author_update' author.pk %}">Update info</a> |
<a href="{% url 'author_delete' author.pk %}">Delete author</a>
{% endif %}
</p>
...
Repeat the exactly same operations for creating 'book_create
', 'book_update
' and
'book_delete
' Generic editing views and have fun! ;-)
Working with forms (Django docs)
Writing your first Django app, part 4 > Writing a simple form (Django docs)
The Forms API (Django docs)
Form fields (Django docs)
Form and field validation (Django docs)
Form handling with class-based views (Django docs)
Creating forms from models (Django docs)
Generic editing views (Django docs)