8. Working with forms - LiVanych/locallibrary GitHub Wiki

Read full version

Django Form Control Process Django Form Control Process

Renew-book form using a Form and function view

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.

Form

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).

Declaring a Form

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).")

Validation

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

URL Configuration

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!

View

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)

The template

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 %}

Testing the page

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).

Generic editing views

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).

Views

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')

Templates

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 %}

URL configurations

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'),
]

Permissions

Views

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')

Templates

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>
...

Generic editing views for Book Model

Repeat the exactly same operations for creating 'book_create', 'book_update' and 'book_delete' Generic editing views and have fun! ;-)

See also

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)

Read full version

⚠️ **GitHub.com Fallback** ⚠️