Tvorba internetových aplikací

Jakub Klinkovský

:: České vysoké učení technické v Praze
:: Fakulta jaderná a fyzikálně inženýrská
:: Katedra softwarového inženýrství

Akademický rok 2023-2024

Dodatečné informace ke Django modelům

Třída Meta – definice dodatečných vlastností modelů

Třída Meta vnořená do definice modelu umožňuje definovat dodatečné vlastnosti nebo modifikovat chování modelu, např.:

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

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

    class Meta:
        ordering = ["name"]   # defaultní způsob řazení objektů ("-name" obrátí pořadí)

Dokumentace obsahuje seznam dostupných Meta-parametrů.

Nejčastěji používané parametry jsou ordering, indexes a constraints.

Použití modelů v aplikaci – definice view

V souboru diary/views.py můžeme definovat funkci index pro úvodní stránku aplikace (přehled všech objektů uložených v databázi):

def index(request):
    # set rendering context - variables used in the template
    context = {}
    context["genres_count"] = models.Genre.objects.count()
    context["authors_count"] = models.Author.objects.count()
    context["books_count"] = models.Book.objects.count()
    context["readings_count"] = models.Reading.objects.count()

    # list of the newest books
    context["newest_books"] = models.Book.objects.all().order_by("-issue_date")[:5]

    # render the template
    return render(request, "diary/index.html", context)

Důležité prvky: atribut objects, metoda count(), metoda all(), metoda
order_by("-issue_date"), omezení množiny výsledků pomocí [:5]

QuerySets

QuerySet je objekt, který představuje množinu výsledků dotazu do databáze.

  • každý prvek z QuerySet je instance daného modelu – atributy odpovídají fieldům
  • příklady metod, které vrací QuerySet:
    • all() – vrací všechny objekty
    • filter() – vrací objekty, které splňují danou podmínku, např.
      models.Genres.objects.filter(name__contains="comedy")
    • exclude() – vrací objekty, které nesplňují danou podmínku (opak filter)
    • order_by() – vrací objekty seřazené dle zadaného kritéria, typicky se aplikuje na existující QuerySet, např. another_query.order_by("name")
  • metody lze řetězit: q.filter(first_name="Joe").exclude(last_name="Doe")
  • je to součást ORM – automatické mapování mezi daty v databázi a objekty v Pythonu, pomocí atributového přístupu se dá dostat k navázaným objektům

Použití modelů v aplikaci – definice šablony

V šabloně diary/index.html se vhodným způsobem zpracují všechny proměnné, které předala odpovídající view funkce:

  • použití proměnných s filtrem default v úvodním textu:
    <p>V současné době je zde {{ books_count|default:0 }} knih od {{ authors_count|default:0 }} autorů
    v {{ genres_count|default:0 }} žánrech. Celkový počet přečtení je {{ readings_count|default:0 }}.
    </p>
    
  • sestavení seznamu nejnovějších knih:
    {% for book in newest_books %}
        <li>"{{ book.title }}" by {{ book.author.first_name }} {{ book.author.last_name }}</li>
    {% endfor %}
    
  • použití metody book.short_description:
    <dt>"{{ book.title }}" by {{ book.author.first_name }} {{ book.author.last_name }}</dt>
    <dd>{{ book.short_description }}</dd>
    

Generic views

Představte si úkol: zobrazit seznamy všech žánrů, autorů a knih na samostatných stránkách.

Django poskytuje obecné řešení založené na generic views – obecná implementace view pomocí třídy. Pro tento úkol se nám hodí ListView (k dispozici jsou i další).

  1. Definice ve views.py:
    from django.views.generic import ListView
    
    class BooksListView(ListView):
        model = models.Book
    
    Další volitelné atributy (viz dokumentace):
    • queryset – množina zobrazených objektů (defaultně ...objects.all())
    • template_name – použitá šablona (zde defaultně diary/book_list.html)
    • context_object_name – název objektu přístupný v šabloně (object_list)

Použití generic views

  1. Pomocí metody as_view() v urls.py:

    path("books/", views.BooksListView.as_view(), name="books_list"),
    
  2. Přidání příslušné šablony – templates/diary/book_list.html:

    {% extends "diary/base.html" %}
    
    {% block content %}
        <h2>Books</h2>
        <ul>
            {% for book in object_list %}
                <li>{{ book.title }}</li>
            {% endfor %}
        </ul>
    {% endblock %}
    

Cvičení – další rozšíření pohledů

  • zobrazte další informace o knize v seznamu nejnovějších knih
  • zkuste seznam autorů seřadit podle počtu knih, které napsali
  • zkuste zobrazit seznam autorů, kteří nemají ani jednu knihu v databázi
  • zkuste v seznamu žánrů zobrazit počet knih spadajících do dané skupiny

Další možnosti

  • stránkování seznamů
    • stránka by nikdy neměla zobrazovat neomezený počet objektů!
    • z důvodu efektivity je nutné seznam rozdělit na několik stránek a poskytnout navigaci mezi stránkami (dopředu/dozadu, první/poslední, ...)
    • Django poskytuje třídu Paginator – použití si ukážeme jindy
  • lepší CSS, více obsahu...

Webové formuláře

Formuláře v HTML

Formuláře poskytují standardní způsob pro předání dat od uživatele webové aplikaci – ať už klientskému skriptu nebo serverové části. Princip je založen na několika vrstvách:

  1. značky HTML – poskytují uživatelské rozhraní
  2. (volitelně) zpracování JavaScriptem
  3. protokol HTTP – přenos dat v rámci požadavku (běžně se používají metody GET a POST)
  4. serverová aplikace – stará se o zpracování dat a o vytvoření odpovědi

Formulářové značky v HTML I

Formuláře slouží pro zadání dat (uživatelem), která pak budou nějakým způsobem zpracována (skriptem na straně klienta nebo serveru). Každý formulář je uveden v párové značce form, jejíž atributy určují především to, jak se budou data odesílat na server (method) a který skript data zpracuje (action):

<form action="nějaká URL" method="post">...</form>

Poznámka: místo metody post lze využít get (vysvětlíme si později).

Uvnitř formuláře se definují jednotlivé prvky – podle typu informací, které má uživatel zadat (např. značky input, select – viz dále).

Je vhodné využívat značky label, legend, fieldset a optgroup, aby se uživatelé ve formuláři lépe orientovali.

Formulářové značky v HTML II

Prvky, které lze definovat uvnitř formuláře:

  • „jednořádkový“ prvek formuláře input (nepárová značka), kde atribut type určuje typ zadávaných dat, např.:
    • text pro zadání textu, password pro zadání hesla, email, number, atd.
    • radio pro volbu jediné z možností, checkbox pro „zaškrtávací políčko“
    • submit pro tlačítko k odeslání vyplněného formuláře
    • file pro odeslání souboru, hidden pro skrytý prvek
  • víceřádkový text textarea (párová značka)
  • výběrový seznam („menu“) select (párová značka), jehož prvky se vkládají do nepovinně párových option

Všechny prvky musí mít uveden atribut name, jinak je nelze zpracovat!

Přehled značek naleznete např. na webu w3schools nebo mdn web docs.

CSS pro formuláře

Vzhled formulářů lze upravit pomocí CSS – viz návod.

Vzhled může záviste na stavu formulářových prvků – k tomu slouží určité pseudo-třídy:

  • :enabled, :disabled, :checked, :focus, :required, :optional, atd.
  • :in-range – prvky, jejichž hodnota je v zadaném rozsahu
  • :out-of-range – prvky, jejichž hodnota je mimo zadaný rozsah
  • :valid – prvky, jejichž hodnota je správná (validní)
  • :invalid – prvky, jejichž hodnota je chybná (nevalidní)
  • :read-only – styluje prvky, u kterých je uveden atribut readonly
  • :read-write – styluje prvky, u kterých není uveden atribut readonly

Zpracování formuláře JavaScriptem

Pomocí JavaScriptu lze s formulářem provést cokoliv – ale ne všechny nápady jsou vhodné... Ve většině případů použití JavaScriptu není potřeba 😄

Validace dat z formuláře pomocí JavaScriptu:

  1. Vytvoříme funkci pro kontrolu jednotlivých položek formuláře
    (typicky v samostatném souboru připojeném ke stránce).
  2. V atributu onsubmit značky form uvedeme kód, kde použijeme vytvořenou funkci a předáme jí objekt this, který zastupuje aktuální objekt.
  3. Pokud chceme znemožnit odeslání chybných dat na server, tak v atributu onsubmit můžeme vrátit false. Např. pokud kontrola vrací true/false:
    <form onsubmit="return kontrola(this)">
    

Viz také atributy a metody JavaScriptového objektu form.

Formuláře ve frameworku Django

  1. Formulář lze definovat několika způsoby:
    • vlastnoruční definice pomocí značek v nějaké šabloně
    • objektová definice pomocí API pro formuláře
  2. Data přenesená od uživatele jsou transparentně dekódovaná a přístupná v objektu HttpRequest – atribut request.GET nebo request.POST
  3. Hlavní zpracování dat z formuláře probíhá ve view funkci/třídě.
    Prvním krokem při zpracování dat by vždy měla být validace!
    • pro vlastnoruční formulář je nutné provést ručně
    • pro objektový formulář lze využít pokročilé nástroje

Příklad: aplikaci, která je komentovaná na následujících slajdech, najdete na GitLabu.

Vlastnoruční definice formuláře

<form action="{% url 'add_author' %}" method="post">
    {% csrf_token %}
    <fieldset>
        <legend>Add new author</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>
    </fieldset>
    <input type="submit" value="Send">
</form>
  • atribut action obsahuje URL, kam se mají data poslat metodou v atr. method
  • {% csrf_token %} poskytuje ochranu proti CSRF útokům
  • atribut for obsahuje hodnotu id cílového elementu
  • atribut name slouží jako klíč odesílané hodnoty/proměnné

Vlastnoruční validace dat z formuláře

Pro jednoduchost vše v jednom pohledu:

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

Dále ověříme metodu požadavku a přečteme parametry formuláře:

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

Poznámka: na slovník request.POST je důležité použít metodu get() místo operátoru [] a získat defaultní hodnotu None (kód musí fungovat, i když některé parametry formuláře chybí).

Validace dat z formuláře pomocí validace modelu

Účelem našeho formuláře je vložit data do databáze. Proto pro validaci můžeme vytvořit objekt daného modelu a použít metodu full_clean():

        # create an object (potentially add more attributeus...)
        author = models.Author(first_name=first_name, last_name=last_name)

        try:
            # validate the input data and save the object
            author.full_clean()
            author.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"))

Po zpracování dat poslaných metodou POST je nutné vrátit přesměrování, aby se zabránilo duplicitním požadavkům (např. po obnovení stránky) – viz PRG pattern.

Validace dat z formuláře – dokončení

V případě výjimky typu ValidationError je potřeba zobrazit chybu uživateli – k tomu slouží proměnná error_message použitá v naší šabloně při definici formuláře:

        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["authors_count"] = models.Author.objects.count()
    return render(request, "diary/add_author.html", context)

Po validaci pokračujeme se standardním zpracováním požadavku (poslední dva řádky jsou mimo blok if request.method == "POST" a provedou se tedy i pro obě metody).

Objektové API pro práci s formuláři

Třída Form poskytuje základ pro objektovou práci s formuláři (analogie třídy Model).

  • odvozené třídy reprezentují jednotlivé formuláře
  • prvky formuláře – fields – stejně jako pro modely, ale z modulu django.forms

Stručný přehled funkcionality:

  • odvozená třída ModelForm – umožňuje automatické mapování atributů daného modelu na prvky formuláře
  • rozhraní pro validaci dat – metody clean() a is_valid()
  • rozhraní pro renderování do HTML – metody as_div(), as_p(), as_ul(), as_table()

Více najdete v dokumentaci k formulářovému API.

Objektový přístup 1 – definice formuláře

V souboru forms.py definujeme potřebné formuláře, např. pro model Author:

from django import forms

class AuthorForm(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")

Poznámka: fields ve formuláři jsou defaultně povinné (parametr required=True).

Poznámka: metoda clean slouží k validaci dat (podobně jako v rozhraní modelů).

Objektový přístup 1 – zpracování dat – základ

V souboru views.py upravíme funkci zpracovávající formulář:

from . import forms

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

    ### zde vytvoříme a zpracujeme formulář - proměnná form

    # continue with normal rendering
    context["authors_count"] = models.Author.objects.count()
    context["form"] = form
    return render(request, "diary/add_author.html", context)

Objektový přístup 1 – zpracování dat – validace formuláře

Základní kostra je stejná jako dříve. Formulář vytvoříme buď prázdný nebo s daty:

    # check if there are input data to be processed
    if request.method == "POST":
        # create a form with data from the request
        form = forms.AuthorForm(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...

            ### zpracování dat - vytvoření objektu a vložení do databáze
            ### stejně jako v manuálním případě - včetně validace modelu!

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

Objektový přístup 1 – zpracování dat – validace modelu

Doplnění kódu v předchozím slajdu:

            # create an object (potentially add more attributeus...)
            author = models.Author(first_name=first_name, last_name=last_name)

            try:
                # validate the input data and save the object
                author.full_clean()
                author.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}"

Objektový přístup 2 – definice pomocí ModelForm

Alternativně můžeme využít ModelForm, který umožňuje definovat formulář automaticky na základě již definovaného modelu.

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

    class Meta:
        model = models.Author
        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")

Objektový přístup 2 – validace pomocí ModelForm

ModelForm zjednodušuje validaci dat – není nutné validovat zvlášť model a formulář.

    # check if there are input data to be processed
    if request.method == "POST":
        # create a form with data from the request
        form = forms.AuthorForm(request.POST)
        # check if the form is valid
        if form.is_valid():
            # create, validate and save the object in database
            new_author = 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.AuthorForm()

Objektový přístup – použití formuláře v šabloně

V šabloně lze použít některou z metod as_div(), as_p(), as_ul(), as_table().
Obecné chyby nepřiřazené žádnému fieldu zobrazí metoda non_field_errors().

<form action="{% url 'add_author' %}" method="post">
    {% csrf_token %}
    <fieldset>
        <legend>Add new author</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>

Poznámka: z vlastnoručního řešení zůstaly jen značky form, fieldset, legend, tlačítko submit a případné chyby. Pokud použijete ModelField, můžete vynechat také řádek s error_message.

--- ## JavaScript Tipy k funkci pro kontrolu dat ve formuláři: - vstupem funkce může být objekt, který zastupuje celý formulář (ten pak funkci předáme při jejím volání) - funkce by měla vracet `true` nebo `false` - přístup k hodnotám elementů `input` poskytuje atribut `value` - pro pokročilejší kontroly textových údajů lze využít regulární výrazy