11. Forms

“Django provides a range of tools and libraries to help you build forms to accept input from site visitors, and then process and respond to the input.”

Working with forms | Django documentation

Now that the frontend login is working, we can build the form to create bookmarks.

11.1. Add URLs for the forms

First you have to extend the marcador app’s URLconf in mysite/marcador/urls.py by two new URLs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django.conf.urls import url


urlpatterns = [
    url(r'^user/(?P<username>[-\w]+)/$', 'marcador.views.bookmark_user',
        name='marcador_bookmark_user'),
    url(r'^create/$', 'marcador.views.bookmark_create',
        name='marcador_bookmark_create'),
    url(r'^edit/(?P<pk>\d+)/$', 'marcador.views.bookmark_edit',
        name='marcador_bookmark_edit'),
    url(r'^$', 'marcador.views.bookmark_list', name='marcador_bookmark_list'),
]

The first one ^create/$ is completely static and binds the corresponding view to the path /create/.

The pattern ^edit/(?<pk>\d+)/$ has a variable part which catches the primary key (PK) of a bookmark. The PK is added to every model by Django, so every entry gets a unique identifier. The path /edit/1/ for example leads to the bookmark with the PK 1.

Between the static parts edit/ at the beginning and / at the end stands the group (?P<pk>\d+). With \d+ it catches any number of numerical characters and stores them in the variable pk which can be used in the view.

11.2. Add the Form

The next step is the form. To create it, add the file mysite/marcador/forms.py:

1
2
3
4
5
6
7
8
9
from django.forms import ModelForm

from .models import Bookmark


class BookmarkForm(ModelForm):
    class Meta:
        model = Bookmark
        exclude = ('date_created', 'date_updated', 'owner')

The form class BookmarkForm is also called a ModelForm because it inherits from django.forms.ModelForm. ModelForms automatically create form fields for every field in the model to which they belong. The linking of the models is done in the inner class Meta which must be present in every ModelForm.

For security reasons the value owner mustn’t be editable through the form because otherwise it would be possible to foist a bookmark on another user. Therefore owner is added to the list of excluded fields. The same is done with the fields date_created and date_updated because they are set automatically.

11.3. Add the Views

Now you can create two new views which make use of the BookmarkForm. The first one is used to create new bookmarks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404, redirect, render

from .forms import BookmarkForm
from .models import Bookmark


def bookmark_list(request):
    bookmarks = Bookmark.public.all()
    context = {'bookmarks': bookmarks}
    return render(request, 'marcador/bookmark_list.html', context)


def bookmark_user(request, username):
    user = get_object_or_404(User, username=username)
    if request.user == user:
        bookmarks = user.bookmarks.all()
    else:
        bookmarks = Bookmark.public.filter(owner__username=username)
    context = {'bookmarks': bookmarks, 'owner': user}
    return render(request, 'marcador/bookmark_user.html', context)


@login_required
def bookmark_create(request):
    if request.method == 'POST':
        form = BookmarkForm(data=request.POST)
        if form.is_valid():
            bookmark = form.save(commit=False)
            bookmark.owner = request.user
            bookmark.save()
            form.save_m2m()
            return redirect('marcador_bookmark_user',
                username=request.user.username)
    else:
        form = BookmarkForm()
    context = {'form': form, 'create': True}
    return render(request, 'marcador/form.html', context)

The decorator @login_required makes sure that the view is only accessible by authenticated users. Other visitors are redirected to the login page.

The first step is to check if the request was done via POST. If yes, the form is initialized with the POST data.

The form gets validated by using form.is_valid(). If the validation was successful, the new bookmark can be saved with form.save(commit=False). The argument commit=False prevents the ModelForm from saving the bookmark, it just returns the model instance prepared with the validated data. Now the current user is added as the owner and the bookmark is saved. After that form.save_m2m() is called to create the relationships to the Tag models. Finally the user gets redirected to his or her bookmark list.

If the validation wasn’t successful or if the request was done via GET, the form is passed to the template where it can be rendered including eventual error messages.

Two values are passed to the template as context variables. The dictionary key form contains the instance of BookmarkForm. create is a boolean value and is used to determine if the template is used to create or edit a bookmark because it is used in both views.

The second view allows editing of bookmarks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect, render

from .forms import BookmarkForm
from .models import Bookmark


def bookmark_list(request):
    bookmarks = Bookmark.public.all()
    context = {'bookmarks': bookmarks}
    return render(request, 'marcador/bookmark_list.html', context)


def bookmark_user(request, username):
    user = get_object_or_404(User, username=username)
    if request.user == user:
        bookmarks = user.bookmarks.all()
    else:
        bookmarks = Bookmark.public.filter(owner__username=username)
    context = {'bookmarks': bookmarks, 'owner': user}
    return render(request, 'marcador/bookmark_user.html', context)


@login_required
def bookmark_create(request):
    if request.method == 'POST':
        form = BookmarkForm(data=request.POST)
        if form.is_valid():
            bookmark = form.save(commit=False)
            bookmark.owner = request.user
            bookmark.save()
            form.save_m2m()
            return redirect('marcador_bookmark_user',
                username=request.user.username)
    else:
        form = BookmarkForm()
    context = {'form': form, 'create': True}
    return render(request, 'marcador/form.html', context)


@login_required
def bookmark_edit(request, pk):
    bookmark = get_object_or_404(Bookmark, pk=pk)
    if bookmark.owner != request.user and not request.user.is_superuser:
        raise PermissionDenied
    if request.method == 'POST':
        form = BookmarkForm(instance=bookmark, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('marcador_bookmark_user',
                username=request.user.username)
    else:
        form = BookmarkForm(instance=bookmark)
    context = {'form': form, 'create': False}
    return render(request, 'marcador/form.html', context)

Here the primary key pk is used to fetch the bookmark from the database. If no bookmark with this PK exists, the HTTP status code 404 is returned. If the current user isn’t the owner (bookmark.owner != request.user) and isn’t a superuser (not request.user.is_superuser), access is denied and the HTTP status code 403 is returned.

Apart from that, the process is similar to bookmark_create. But the form is initialized with a bookmark instance (instance=bookmark). This way all form fields are pre-filled with the old values. If POST data is present, it overrides the values from the bookmark instance.

In addition the key create in the context dictionary is set to False because this view is only used to edit existing bookmarks.

11.4. Add the Templates

Now you can add the template for the form mysite/marcador/templates/marcador/form.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block title %}
  {% if create %}Create{% else %}Edit{% endif %} bookmark
{% endblock %}

{% block heading %}
  <h2>
    {% if create %}
      Create bookmark
    {% else %}
      Edit bookmark
    {% endif %}
  </h2>
{% endblock %}

{% block content %}
  {% if create %}
    {% url "marcador_bookmark_create" as action_url %}
  {% else %}
    {% url "marcador_bookmark_edit" pk=form.instance.pk as action_url %}
  {% endif %}
  <form action="{{ action_url }}" method="post" accept-charset="utf-8">
    {{ form|crispy }}
    {% csrf_token %}
    <p><input type="submit" class="btn btn-default" value="Save"></p>
  </form>
{% endblock %}

The variable create is used in the title and heading blocks to display different text depending on whether the template is used to create or edit a bookmark. The URL for the form’s action attribute is also set according to the value of create and assigned to action_url. Note that the current bookmark fetched from the database has been assigned to form.instance and can be used to create the edit URL for this bookmark.

Now you can add a link to create new bookmarks to the file mysite/templates/toggle_login.html. The corresponding line is highlighted:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{% if user.is_authenticated %}
<ul class="nav navbar-nav navbar-right">
  <li><a href="{% url "marcador_bookmark_create" %}">Create bookmark</a></li>
  <li class="dropdown">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
        aria-expanded="false">{{ user.username }} <span class="caret"></span></a>
    <ul class="dropdown-menu" role="menu">
      <li><a href="{% url "marcador_bookmark_user" user.username %}">
          My bookmarks</a></li>
      <li><a href="{% url "mysite_logout" %}">Logout</a></li>
    </ul>
  </li>
</ul>
{% else %}

The last step is to extend the template mysite/marcador/templates/marcador/bookmark.html at the end with the following lines:

15
16
17
18
19
20
21
<br>by <a href="{% url "marcador_bookmark_user" bookmark.owner.username %}">
    {{ bookmark.owner.username }}</a>
{{ bookmark.date_created|timesince }} ago
{% if bookmark.owner == user or user.is_superuser %}
  <br><a class="btn btn-default btn-xs" role="button"
      href="{% url "marcador_bookmark_edit" bookmark.pk %}">Edit bookmark</a>
{% endif %}

This way authenticated users see an edit button bellow their own bookmarks.

11.5. Test the form

Now load the main page and click on the link to create a new bookmark. You should see this form:

Create Bookmark Form

It displays all fields we have configured in the form. Tags can’t be added, but you can build a second form to do this.

If you click on one the links to edit a bookmark you will see this edit form:

Edit Bookmark Form