Web Application Development

Jakub Klinkovský

:: Czech Technical University in Prague
:: Faculty of Nuclear Sciences and Physical Engineering
:: Department of Software Engineering

Academic Year 2024-2025

Additional Information on Django Models

Meta Class – Definition of Additional Model Properties

The Meta class nested in the model definition allows for the definition of additional properties or modification of the model's behavior, e.g.:

class Genre(models.Model):
    name = models.CharField(max_length=100, unique=True)
    description = models.TextField(null=True, blank=True)

    def __str__(self):
        return f"{self.name}"

    class Meta:
        ordering = ["name"]   # default sorting of objects ("-name" reverses the order)

The documentation contains a list of available Meta parameters.

The most commonly used parameters are ordering, indexes, and constraints.

Using Models in an Application – View Definition

In the demo/views.py file, we can define the index function for the application's home page (overview of all objects stored in the database):

def index(request):
    # list of the newest albums
    albums = Album.objects.all().order_by("-release_date")[:5]
    # set rendering context - variables used in the template
    context = {
        "number_of_albums": Album.objects.count(),
        "number_of_musicians": Musician.objects.count(),
        "number_of_genres": Genre.objects.count(),
        "albums": albums,
    }
    # render the template
    return render(request, "demo/index.html", context)

Important elements: the objects attribute, the count() method, the order_by("-release_date") method, and the limitation of the result set using [:5]

QuerySets

QuerySet is an object that represents a set of query results from the database.

  • each element of a QuerySet is an instance of the given model – attributes correspond to fields
  • examples of methods that return a QuerySet:
    • all() – returns all objects
    • filter() – returns objects that meet a given condition, e.g.
      models.Genres.objects.filter(name__contains="comedy")
    • exclude() – returns objects that do not meet a given condition
    • order_by() – returns objects sorted by a given criterion, typically applied to an existing QuerySet, e.g. another_query.order_by("-release_date")
  • method chaining: q.filter(first_name="Joe").exclude(last_name="Doe")
  • this is part of ORM – automatic mapping between data in the database and objects, using attribute access to access related objects

Using Models in an Application – Template Definition

In the demo/index.html template, all variables passed by the corresponding view function are processed appropriately:

  • use of variables with the default filter in the introductory text:
    <p>There are currently {{ number_of_albums|default:0 }} albums,
       {{ number_of_musicians|default:0 }} musicians, and {{ number_of_genres|default:0 }} genres.
    </p>
    
  • assembling the list of the newest albums:
    <ul>
    {% for album in albums %}
      <li><a href="{% url 'album' album.id %}">{{ album.name }} by {{ album.artist }}</a></li>
    {% endfor %}
    </ul>
    

The link to the album works thanks to the mapping in the mysite/urls.py file:

... path("album/<int:album_id>/", views.album_detail, name="album"), ...

Generic Views

Imagine the task: display lists of all genres, authors, and books on separate pages.

Django provides generic views – a general implementation of a view using a class.
For this task, we can use ListView (other available views are listed here):

  1. Definition in views.py:
    from django.views.generic import ListView
    
    class GenreListView(ListView):
        model = models.Genre
    
    Other optional attributes (see documentation):
    • queryset – set of displayed objects (by default objects.all())
    • template_name – used template (here by default demo/genre_list.html)
    • context_object_name – name of the variable accessible in the template (object_list)

Using Generic Views

  1. Using the as_view() method in urls.py:

    path("genres/", views.GenreListView.as_view(), name="genres"),
    
  2. Adding the corresponding template: templates/demo/genre_list.html:

    {% extends "demo/base.html" %}
    
    {% block content %}
        <h2>Genres</h2>
        <ul>
            {% for genre in object_list %}
                <li><strong>{{ genre.name }}</strong>
                    {{ genre.description }}
                </li>
            {% endfor %}
        </ul>
    {% endblock %}
    

Further Possibilities

  • pagination of lists
    • a page should never display an unlimited number of objects!
    • for efficiency reasons, it is necessary to divide the list into several pages and provide navigation between pages (forward/backward, first/last, ...)
    • Django provides the Paginator class – we will see its use later
  • better CSS, more content, etc.

Web Forms

Forms in HTML

Forms provide a standard way to pass data from the user to the web application – whether to a client script or a server-side application. The process involves several layers:

  1. HTML tags – provide a user interface for data entry (e.g. input, select, etc.)
  2. (optionally) processing using JavaScript
  3. HTTP protocol – data transfer within the framework of the protocol (typically using the GET or POST methods)
  4. server-side application – responsible for processing the data and creating a response

Form Tags in HTML I

Forms are used to enter data (by the user), which is then processed (by a script on the client side or on the server side). Each form is enclosed in a pair of form tags, whose attributes determine how the data will be sent to the server (method) and which script will process the data (action):

<form action="some URL" method="post">...</form>

Note: method="get" can be used instead of method="post"

Inside the form, individual elements are defined – according to the type of information the user needs to enter (e.g. input, select – see below).

It is recommended to use the label, legend, fieldset, and optgroup tags to help users navigate the form.

Form Tags in HTML II

Elements that can be defined inside a form:

  • "single-line" form element input (non-pair tag), where the type attribute determines the type of entered data, e.g.:
    • text for text input, password for password input, email, number, etc.
    • radio for selecting one of the options, checkbox for a "checkbox"
    • submit for a button to submit the completed form
    • file for file upload, hidden for a hidden element
  • multi-line text textarea (pair tag)
  • selection list ("dropdown menu") select (pair tag), containing option elements

All elements must have the name attribute specified, otherwise they cannot be processed!

An overview of tags can be found on the w3schools or mdn web docs websites.

CSS for Forms

The appearance of forms can be modified using CSS – see tutorial.

The appearance can depend on the state of form elements – for this purpose, certain pseudo-classes are used:

  • :enabled, :disabled, :checked, :focus, :required, :optional, etc.
  • :in-range – elements whose value is within a specified range
  • :out-of-range – elements whose value is outside a specified range
  • :valid – elements whose value is correct (valid)
  • :invalid – elements whose value is incorrect (invalid)
  • :read-only – styles elements with the readonly attribute
  • :read-write – styles elements without the readonly attribute

Form Processing Using JavaScript

Using JavaScript, you can do anything with a form – but not all ideas are good... In most cases, using JavaScript is not necessary 😄

Data validation in a form using JavaScript:

  1. Create a function to check individual form elements
    (typically in a separate file attached to the page).
  2. In the onsubmit attribute of the form tag, include the code where you use the created function and pass it the this object, which represents the current object.
  3. If you want to prevent the submission of incorrect data to the server, you can return false in the onsubmit attribute. E.g. if kontrola returns true/false:
    <form onsubmit="return some_check(this)">
    

See also the attributes and methods of the JavaScript form object.

Forms in the Django Framework

  1. A form can be defined in several ways:
    • manual definition using tags in a template
    • object definition using the API for forms
  2. Data transferred from the user is transparently decoded and accessible in the HttpRequest object – the request.GET or request.POST attribute
  3. The main processing of data from the form takes place in the view function/class.
    The first step in processing data should always be validation!
    • for manual forms, it is necessary to perform it manually
    • for object forms, you can use advanced tools

Manual Form Definition

<form action="{% url 'add_musician' %}" method="post">
    {% csrf_token %}
    <fieldset>
        <legend>Add new musician</legend>
        {% if error_message %}<p class="error">{{ error_message }}</p>{% endif %}
        <label for="fname">First name:</label>
        <input type="text" id="fname" name="first_name"><br>
        <label for="lname">Last name:</label>
        <input type="text" id="lname" name="last_name"><br>
        <!-- and similarly with other fields... -->
    </fieldset>
    <input type="submit" value="Send">
</form>
  • the action attribute contains the URL to which the data will be sent using the method in the method attribute
  • {% csrf_token %} provides protection against CSRF attacks
  • the id attribute contains the value of the target element
  • the name attribute is the key of the variable/parameter

Manual Validation of Data from a Form

For simplicity, everything is in one view:

def add_musician(request):
    # set rendering context - variables used in the template
    context = {}
    ...

Next, we check the request method and read the form parameters:

    if request.method == 'POST':
        first_name = request.POST.get("first_name")
        last_name = request.POST.get("last_name")
        # and similarly with other fields...

Note: it is important to use the get() method on the request.POST dictionary and get the default value None (the code must work even if some form parameters are missing).

Validation of Data from a Form Using Model Validation

The purpose of our form is to insert data into the database. Therefore, for validation, we can use the full_clean() method:

        # create an object (potentially add more arguments...)
        musician = models.Musician(first_name=first_name, last_name=last_name)

        try:
            # validate the input data and save the object
            musician.full_clean()
            musician.save()

            # Always return an HttpResponseRedirect after successfully dealing
            # with POST data. This prevents data from being posted twice if the
            # user hits the <Back> or <Reload> button.
            return HttpResponseRedirect(reverse("index"))

After processing the data sent using the POST method, it is necessary to return a redirect, to prevent duplicate requests (e.g. after refreshing the page) – see the PRG pattern.

Validation of Data from a Form – Completion

In case of an exception of type ValidationError, it is necessary to display the error to the user – for this purpose, we use the error_message variable used in our template when defining the form:

        except ValidationError as e:
            context["error_message"] = "Some form fields are not valid:"
            for field, messages in e:
                message = " ".join(messages)
                context["error_message"] += f" {field}{message}"

    # continue with normal rendering
    context["musicians_count"] = models.Musician.objects.count()
    return render(request, "demo/add_musician.html", context)

After validation, we continue with the standard processing of the request (the last two lines are outside the if request.method == "POST" block and are executed for both methods).

Exercise 1

Assemble the examples from the previous slides into a working state 😊

The previous sample solution from last week can be found on GitLab.

Object-oriented API for Working with Forms

The Form class provides the basis for an object-oriented approach to working with forms (analogous to the Model class).

  • derived classes represent individual forms
  • elements of the form – fields – same concept as for models, but from the django.forms module

A brief overview of functionality:

  • derived class ModelForm – allows automatic mapping of attributes of a given model
  • interface for data validation – clean() and is_valid() methods
  • interface for rendering to HTML – as_div(), as_p(), as_ul(), as_table() methods

More information can be found in the Form API documentation.

Object-oriented Approach 1 – Form Definition

In the forms.py file, we define the necessary forms, e.g. for the Musician model:

from django import forms

class MusicianForm(forms.Form):
    error_css_class = "error"

    first_name = forms.CharField(label="First name", max_length=50)
    last_name = forms.CharField(label="Last name", max_length=50)
    # and other fields...

    def clean(self):
        if " " in self.cleaned_data["first_name"]:
            raise forms.ValidationError("First name cannot contain spaces")

Note: fields in the form are mandatory by default (parameter required=True).

Note: the clean method is used for data validation (similar to the interface of models).

Object-oriented Approach 1 – Data Processing – Basics

In the views.py file, we modify the function that processes the form:

from . import forms

def add_musician(request):
    # set rendering context - variables used in the template
    context = {}

    ### here we create and process the form - the variable form

    # continue with normal rendering
    context["musicians_count"] = models.Musician.objects.count()
    context["form"] = form
    return render(request, "demo/add_musician.html", context)

Object-oriented Approach 1 – Data Processing – Form Validation

The basic structure is the same as before. The form can be created empty or with data:

    # check if there are input data to be processed
    if request.method == "POST":
        # create a form with data from the request
        form = forms.MusicianForm(request.POST)
        # check if the form is valid
        if form.is_valid():
            first_name = form.cleaned_data["first_name"]
            last_name = form.cleaned_data["last_name"]
            # and other fields...

            ### processing data - creating an object and inserting it into the database
            ### similarly to manual validation!

    # create an empty form for GET or any other method
    else:
        form = forms.MusicianForm()

Object-oriented Approach 1 – Data Processing – Model Validation

Completing the code from the previous slide:

            # create an object (potentially add more attributes...)
            musician = models.Musician(first_name=first_name, last_name=last_name)

            try:
                # validate the input data and save the object
                musician.full_clean()
                musician.save()

                # Always return an HttpResponseRedirect after successfully dealing
                # with POST data. This prevents data from being posted twice if the
                # user hits the <Back> or <Reload> button.
                return HttpResponseRedirect(reverse("index"))

            except ValidationError as e:
                context["error_message"] = "Some form fields are not valid:"
                for field, messages in e:
                    message = " ".join(messages)
                    context["error_message"] += f" {field}{message}"

Object-oriented Approach 1 – Using the Form in a Template

In the template, you can use one of the as_div(), as_p(), as_ul(), as_table() methods.
General errors not assigned to any field are displayed using the non_field_errors() method.

<form action="{% url 'add_musician' %}" method="post">
    {% csrf_token %}
    <fieldset>
        <legend>Add new musician</legend>
        {% if error_message %}<p class="error">{{ error_message }}</p>{% endif %}
        {{ form.non_field_errors }}
        {{ form.as_div }}
    </fieldset>
    <input type="submit" value="Send">
</form>

Note: from the manual solution, only the form, fieldset, legend, submit tags, and possible errors remain.

Exercise 2

Assemble the examples from the previous slides into a working state and compare the result with the solution from Exercise 1 😊

Object-oriented Approach 2 – Definition Using ModelForm

Alternatively, you can use ModelForm, which allows you to define a form automatically based on an already defined model.

class MusicianForm(forms.ModelForm):
    error_css_class = "error"

    class Meta:
        model = models.Musician
        fields = ["first_name", "last_name", ]  # and other fields...

    def clean(self):
        # call the method from the parent class
        super().clean()

        # do some additional validation
        if " " in self.cleaned_data["first_name"]:
            raise forms.ValidationError("First name cannot contain spaces")

Object-oriented Approach 2 – Validation Using ModelForm

ModelForm simplifies data validation – it is not necessary to validate the model and the form separately.

    # check if there are input data to be processed
    if request.method == "POST":
        # create a form with data from the request
        form = forms.MusicianForm(request.POST)
        # check if the form is valid
        if form.is_valid():
            # create, validate, and save the object in the database
            new_musician = form.save()

            # Always return an HttpResponseRedirect after successfully dealing
            # with POST data. This prevents data from being posted twice if the
            # user hits the <Back> or <Reload> button.
            return HttpResponseRedirect(reverse("index"))

    # create an empty form for GET or any other method
    else:
        form = forms.MusicianForm()

Object-oriented Approach 2 – Using the Form in a Template

In the template, you can use one of the as_div(), as_p(), as_ul(), as_table() methods.
General errors not assigned to any field are displayed using the non_field_errors() method.

<form action="{% url 'add_musician' %}" method="post">
    {% csrf_token %}
    <fieldset>
        <legend>Add new musician</legend>
        {{ form.non_field_errors }}
        {{ form.as_div }}
    </fieldset>
    <input type="submit" value="Send">
</form>

Note: compared to the object-oriented approach 1, the error_message line is also missing.

Exercise 3

Assemble the examples from the previous slides into a working state and compare the result with the solutions from Exercises 1 and 2 😊

Add more forms to the application: insert a new genre and a new album.

- use of the `short_description` method in the list of books: ```django <dt>"{{ book.title }}" by {{ book.author.first_name }} {{ book.author.last_name }}</dt> <dd>{{ book.short_description }}</dd> ```

--- ## JavaScript Tips for the function to check data in a form: - the input to the function can be an object representing the entire form (which is then passed to the function when it is called) - the function should return `true` or `false` - access to the values of `input` elements is provided by the `value` attribute - for more advanced checks of text data, you can use regular expressions