#33. Django ModelForm, CBV

Django ModelForm, Class-Based View

План

I. ModelForm

  1. ModelForm
  2. Валідація
  3. Метод save() у форми
  4. Передача об’єкта

II. Class-based View

  1. Class View
  2. Class TemplateView
  3. Class RedirectView
  4. Class DetailView
  5. Class ListView
  6. Class FormView
  7. Class CreateView
  8. Class UpdateView
  9. Class DeleteView
  10. Class LoginView
  11. Class LogoutView
  12. LoginRequiredMixin

III. Живий приклад
IV. Література (що почитати)

ModelForm

Якщо ви створюєте додаток, керований базою даних, то, найімовірніше, у вас будуть форми, які тісно пов’язані з моделями Django. У цьому випадку було б зайвим визначати типи полів у формі, тому що ви вже визначили поля в моделі.

З цієї причини Django надає допоміжний клас, який дозволяє вам створити клас Form з моделі Django.

ModelForm - це тип форми, який генерується безпосередньо з моделі. Це дуже потужний інструмент роботи з моделями.
Документація тут

models.py

from django.db import models

class Author(models.Model):  
    pseudonym = models.CharField(max_length=120, blank=True, null=True)  
    name = models.CharField(max_length=120)  
  
    def __str__(self):  
        return self.name  
  
  
class Article(models.Model):  
  NOT_SELECTED = 1  
  COMEDY = 2  
  ACTION = 3  
  BEAUTY = 4  
  OTHER = 5  
  GENRE_CHOICES = (  
        (NOT_SELECTED, _("Not selected")),  
        (COMEDY, _("Comedy")),  
        (ACTION, _("Action")),  
        (BEAUTY, _("Beauty")),  
        (OTHER, _("Other"))  
    )  
  
    author = models.ForeignKey(  
        Author,  
        on_delete=models.CASCADE,  
        null=True,  
        related_name='articles',  
    )  
    text = models.TextField(null=True)  
    created_at = models.DateTimeField(default=timezone.now)  
    updated_at = models.DateTimeField(default=timezone.now)  
    genre = models.IntegerField(choices=GENRE_CHOICES, default=NOT_SELECTED)  
  
    def __str__(self):  
        return f'Author - {self.author.name}, genre - {self.genre}, id - {self.id}'

forms.py

from django.forms import ModelForm
from .models import Author, Article

class AuthorForm(ModelForm):  
    class Meta:  
        model = Author  
        fields = '__all__'  
  
  
class ArticleForm(ModelForm):  
    class Meta:  
        model = Article  
        fields = ['text', 'author']

Поле fields або exclude є обов’язковими


Такий вид форм, рівнозначний з
forms.py

from django import forms
from .models import Author, Article

class AuthorForm(forms.Form):  
    pseudonym = forms.CharField(max_length=120, required=False)  
    name = forms.CharField(max_length=120)  
  
  
class ArticleForm(forms.Form):  
    text = forms.CharField(widget=forms.Textarea)
    author = forms.ModelChoiceField(queryset=Author.objects.all())

views.py

from django.shortcuts import render  
  
from .forms import ArticleForm, AuthorForm

def form_view(request):  
    article_form = ArticleForm(request.POST or None)  
    author_form = AuthorForm(request.POST or None)  
    return render(  
        request,  
        'form.html',  
        {  
            'article_form': article_form,  
            'author_form': author_form  
        }  
    )

forms.html

{% extends 'base.html' %}  
{% block content %}  
    <p>Article</p>  
    <form method="post" action="{% url 'form-view' %}">  
        {% csrf_token %}  
        {{ article_form.as_p }}  
        <button type="submit">Send Article</button>  
    </form>  
    <p>Author</p>  
    <form method="post" action="{% url 'form-view' %}">  
        {% csrf_token %}  
        {{ author_form.as_p }}  
        <button type="submit">Send Author</button>  
    </form>  
{% endblock %}

введіть тут опис зображення

Валідація

Валідація проходить у два етапи.
Крім стандартного методу форми is_valid(), модел форма викличе вбудований у модель метод full_clean().

Якщо перший відповідає за валідацію форми, то другий відповідає за валідацію для об’єкта моделі.
Перевірка об’єктів моделі проходила в три етапи:

  1. Перевірка полів моделі - Model.clean_fields()
  2. Перевірка всього об’єкта - Model.clean()
  3. Перевірка унікальності полів - Model.validate_unique()

Усі три етапи виконуються під час виклику методу full_clean().

Ви можете перевизначити метод clean() на формі моделі, щоб забезпечити додаткову перевірку так само, як і на звичайній формі.

Метод save() у форми

Метод save в ModelForm об’єкті виконує, по суті, дві дії, отримує об’єкт моделі, ґрунтуючись на переданих даних, і викликає метод save, але вже для моделі.

Метод save() може приймати аргумент commit, за замовчуванням True. Якщо вказати commit як False, метод save моделі не буде викликано (об’єкт не буде збережено в базу), буде тільки створено попередній об’єкт моделі, використовується, коли потрібно “доповнити” дані перед збереженням у базу. Дуже часто використовується! Наприклад додавання користувача з request.

form = PartialAuthorForm(request.POST)
author = form.save(commit=False)
author.title = 'Mr'
author.save()

Передача об’єкта

Така форма може приймати не тільки дані, а й цілий об’єкт із бази:

from myapp.models import Article
from myapp.forms import ArticleForm

# Create a form instance from POST data.
f = ArticleForm(request.POST)

# Save a new Article object from the form's data.
new_article = f.save()

# Create a form to edit an existing Article, but use
# POST data to populate the form.
a = Article.objects.get(pk=1)
f = ArticleForm(request.POST, instance=a)
f.save()

Оскільки ми можемо не тільки створювати новий інстанс, а й оновлювати наявний.

Function-based View (FBV) vs Class-based View (CBV):

На самому початку було визнано, що існують загальні ідіоми і шаблони, які трапляються під час розроблення подань. Для абстрагування цих патернів і полегшення розроблення подань для загальних випадків було введено загальні подання на основі функцій.

Проблема із загальними уявленнями на основі функцій полягає в тому, що, хоча вони добре покривали прості випадки, не було можливості розширити або налаштувати їх, крім деяких простих опцій конфігурації, що обмежувало їхню корисність у багатьох реальних додатках.

Узагальнені подання на основі класів були створені з тією самою метою, що й узагальнені подання на основі функцій, - спростити розробку подань. Однак спосіб реалізації рішення - використання міксинів - забезпечує інструментарій, який призводить до того, що узагальнені уявлення на основі класів є більш розширюваними та гнучкими, ніж їхні аналоги на основі функцій.

Якщо в минулому ви пробували використовувати загальні уявлення, засновані на функціях, і знайшли їх недостатніми, вам не слід думати про загальні уявлення, засновані на класах, як про простий еквівалент, заснований на класах, а скоріше як про новий підхід до розв’язання вихідних проблем, які мали розв’язувати загальні уявлення.

Class-based View

Class-based View - це базові абстрактні класи, що реалізують загальні завдання веб-розробки на Django. Вони досить “потужні” в плані використання і повністю використовують можливості об’єктно-орієнтованого програмування та множинного успадкування Python для розширення своєї функціональності. Вони більше, ніж просто загальні базові спрощення використання Django - вони надають утиліти, які можна змішувати зі складними уявленнями у своїх завданнях.

Подання на основі класів забезпечують альтернативний спосіб реалізації подань у вигляді об’єктів Python замість функцій. Вони не замінюють подання на основі функцій, але мають певні відмінності та переваги порівняно з поданнями на основі функцій:

  • Організацію коду, пов’язаного з конкретними методами HTTP (GET, POST тощо), може бути вирішено окремими методами замість умовного розгалуження.
  • Для поділу коду на багаторазово використовувані компоненти можна використовувати об’єктно-орієнтовані методи, як-от міксини (множинне успадкування)
    На початку був тільки контракт функції подання, Django передавав вашій функції HttpRequest і очікував назад HttpResponse. Це була межа того, що надавав Django.

*З цього моменту ми переходимо на використання view заснованих виключно на класах.

Усі основні наявні класи описані тут
введіть тут опис зображення

Class View

Документація

Основою всіх класів, що використовуються у view, є клас View.
Методи цього класу використовуються всіма іншими класами.

Основні атрибути:

`http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']`

Атрибут, який потрібен, щоб визначити, які види запитів будуть доступні для запитів.

Основні функції:

as_view - метод, який завжди викликається, щоб використати клас в urls.py, всередині викликає методи setup і dispatch

setup - метод, який додає request у self, завдяки чому request буде доступний в абсолютно будь-якому методі всіх наших view класів

http_method_not_allowed - метод, який генерує помилку запиту (не можу обробити, наприклад, POST запит.

dispatch - метод, що відповідає за виклик обробника під час запиту.

def dispatch(self, request, *args, **kwargs):
    # Try to dispatch to the right method; if a method doesn't exist,
    # defer to the error handler. Also defer to the error handler if the 
    # request method isn't on the approved list.
    if request.method.lower() in self.http_method_names: # Якщо запит перебуває у списку дозволених, то заходимо.
        handler = getattr(self, request.method.lower(),
                          self.http_method_not_allowed) # Намагаємося із self отримати атрибут або метод, що збігається назвою з методом запиту (POST - post, GET - get), якщо не виходить, то повернути метод http_method_not_allowed
    else:
        handler = self.http_method_not_allowed # Повернути метод http_method_not_allowed
    return handler(request, *args, **kwargs) # Викликати метод, який ми отримали раніше, якщо вдалося, то, наприклад, get(), або post(), якщо ні, то http_method_not_allowed()

Як це працює?

Якщо успадковуватися від цього класу, то ми можемо описати функцію get та/або post, щоб описати, що потрібно робити під час запиту методами GET або POST.

І можемо описати які взагалі запити ми очікуємо приймати в атрибуті http_method_names.

Наприклад:

У views.py

from django.views import View
from django.shortcuts import render

class MyView(View):
    http_method_names = ['get', ]

    def get(self, request, *args, **kwargs):
        return render(request, 'index.html')

У urls.py:

path('some-url/', MyView.as_view(), name='some-name')

Чим така конструкція краща, ніж звичайна функція? Тим, що звичайна функція зобов’язана приймати будь-який запит і далі тільки за допомогою if розділяти різні запити.

Такий клас прийматиме тільки запити описаних методів, відхиляючи всі інші, і кожен запит буде написаний в окремому методі, що сильно покращує читабельність коду.

Class TemplateView

Документація

Клас необхідний для рендеру html файлів

Основні атрибути:

template_name = None # Ім'я html-файлу, який потрібно рендерувати
extra_content = None # Словник із контентом

Основні методи:

Описано метод get

def get(self, request, *args, **kwargs):
    context = self.get_context_data(**kwargs)
    return self.render_to_response(context)

get_context_data - Метод, що повертає дані, які будуть додані в контекст

Як цим користуватися?

from django.views.generic.base import TemplateView

from articles.models import Article

class HomePageView(TemplateView):
    template_name = "home.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['latest_articles'] = Article.objects.all()[:5]
        return context

Ми описали клас, який буде рендерувати файл home.html, у контексті якого буде змінна latest_articles, в якій буде колекція з об’єктів моделі.

Те ж саме можна було зробити через extra_context:

from django.views.generic.base import TemplateView

from articles.models import Article

class HomePageView(TemplateView):
    template_name = "home.html"
    extra_context = {"latest_articles": Article.objects.all()[:5]}

Class RedirectView

Документація

Клас необхідний, щоб перенаправляти запити з одного url на інший.

Основні атрибути:

query_string = False # чи зберегти квері параметри (те що в рядку браузера після ?) у разі редиректу
url = None # Урл на який треба перейти
pattern_name = None # Ім'я урла, на який треба перейти

Основні методи:

Описано всі HTTP методи, і всі вони посилаються на get, наприклад delete:

def delete(self, request, *args, **kwargs):
    return self.get(request, *args, **kwargs)

Метод get_redirect_url відповідає за те, що ы отримати url на який треба перейти

Як користуватися

class ArticleRedirectView(RedirectView):
    query_string = True
    pattern_name = 'article-detail'

Class DetailView

Документація

Клас, який необхідний для того, щоб зробити сторінку для перегляду одного об’єкта.

Йому необхідно передати pk абоslug, і це дасть змогу відобразити один об’єкт (статтю, товар тощо).

Як цим користуватися?

Наприклад:

У views.py

from django.views.generic.detail import DetailView

from articles.models import Article

class ArticleDetailView(DetailView):
    model = Article

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['now'] = timezone.now() # Просто додаємо поточний час до контексту
        return context

У urls.py:

from django.urls import path

from article.views import ArticleDetailView


urlpatterns = [
    path('<pk:pk>/', ArticleDetailView.as_view(), name='article-detail'),
]

Цього вже достатньо, щоб відмалювати сторінку деталей об’єкта. Якщо template_name не вказано явно, то Django намагатиметься відобразити templates/app_name/model_detail.html де app_name - назва застосунку, model - назва моделі, detail - константа.

У контекст буде передано змінну object. Методи post, put тощо не визначені.

Важливі параметри.

pk_url_kwarg = 'pk' # Як змінна називається в urls.py, наприклад `<int:my_id>`
queryset = None # якщо вказано, то можливість обмежити доступ тільки для частини об'єктів (наприклад, прибрати з можливості оновлення деактивовані об'єкти).
template_name = None # вказати ім'я шаблону.
model = None # клас моделі, якщо не вказано queryset, згенерує queryset із моделі.

Важливі методи:

get_queryset - перевизначити queryset

get_context_data - те саме що й у TemplateView

get_object - визначає логіку отримання об’єкта

Class ListView

Документація

Клас, необхідний для відображення списку об’єктів.

Додає в контекст список об’єктів та інформацію про пагінацію.

Як користуватися?

class CommentListView(ListView):
    paginate_by = 10
    template_name = 'comments_list.html'
    queryset = Comment.objects.filter(parent__isnull=True)

Пагінація

введіть тут опис зображення

Документація

Дуже часто наш застосунок зберігає велику кількість даних, і під час відображення нам не потрібно показувати прямо все (припустімо, в нас блог на 1000000000 статей), для видачі даних порціями це і називається пагінація, або розбиття на сторінки.

Коли ви гортаєте стрічку, насправді, ви довантажуєте все нові й нові сторінки, просто за допомогою JS це зроблено так, що ви цього не помічаєте.

За це відповідає параметр:

paginate_by = None # можна вказати скільки має бути об'єктів на одній сторінці 

У шаблон буде передано як список об’єктів, так і дані щодо пагінації

context = {
    'paginator': paginator, # об'єкт класу пагінації, зберігає всі подробиці, які тільки можуть бути.
    'page_obj': page,
    # інформація про поточну сторінку, яка це сторінка, скільки всього сторінок, урл на наступну і попередню сторінку
    'is_paginated': is_paginated,
    # чи були дані взагалі пагіновані, можливо у вас 10 об'єктів на сторінку, а їх всього 5, тоді немає сенсу в пагінації
    'object_list': queryset # Сам кверисет з об'єктами
}

Важливі параметри, такі ж як у DetailView, і ще нові

allow_empty = True # чи дозволити відображення порожнього списку
ordering = None # явно вказати ордеринг

Все ще описано тільки метод get. Методи post, put тощо не дозволені.

Важливі методи:

get_queryset - перевизначити queryset

get_context_data - те саме що й у TemplateView

get_paginator - визначає логіку отримання класу пагінатора

get_paginate_by - визначає логіку отримання значення paginate_by

get_ordering - визначає логіку отримання ордерингу

get_allow_empty - визначає логіку отримання змінної allow_empty

Class FormView

Документація

Не всі класи призначені тільки для читання даних.

FormView клас необхідний для обробки форми.

Як користуватися?

У forms.py

from django import forms

class ContactForm(forms.Form):
    name = forms.CharField()
    message = forms.CharField(widget=forms.Textarea)

    def send_email(self):
        # send email using the self.cleaned_data dictionary
        pass

У views.py

from myapp.forms import ContactForm
from django.views.generic.edit import FormView

class ContactView(FormView):
    template_name = 'contact.html'
    form_class = ContactForm
    success_url = '/thanks/'

    def form_valid(self, form):
        # This method is called when valid form data has been POSTed.
        # It should return an HttpResponse.
        form.send_email()
        return super().form_valid(form)

У contact.html:

<form method="post"> {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Send message">
</form>

Важливі параметри:

Такі ж, як у TemplateView і ще свої

form_class = None # сам клас форми
success_url = None # На яку сторінку перейти якщо форма була валідна
initial = {} # Словник з базовими значеннями форми

Важливі методи:

Тут нарешті визначено метод post:

def post(self, request, *args, **kwargs):
    """
 Handle POST requests: instantiate a form instance with the passed
 POST variables and then check if it's valid.
 """
    form = self.get_form()
    if form.is_valid():
        return self.form_valid(form)
    else:
        return self.form_invalid(form)

Так само всі методи з TemplateView

get_context_data - додатково додає змінну form у темплейт

get_form - отримати об’єкт форми

get_form_class - отримати клас форми

form_valid - Що робити якщо форма валідна

form_invalid - Що робити якщо форма не валідна

get_success_url - Переобумовити генерацію урла, на який буде здійснено перехід, якщо форма валідна

Class CreateView

Документація

Клас для створення об’єктів.

Як цим користуватися?

У views.py

from django.views.generic.edit import CreateView
from myapp.models import Author

class AuthorCreate(CreateView):
    template_name = 'author_create.html'
    model = Author
    fields = ['name']

У author_create.html

<form method="post">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Save">
</form>

У клас потрібно передати або ModelForm, або модель і поля, щоб клас сам згенерував таку форму.

Метод get відкриє сторінку, на якій буде перемінена form, як і інші view, до яких додається форма.

Метод post виконає ті самі дії, що й FormView, але в разі валідності форми, попередньо виконає form.save()

Важливі параметри:

Такі ж, як у FormView і ще свої

form_class = None # Повинен приймати ModelForm
model = None # Можна вказати модель замість форми, щоб згенерувати її на ходу
fields = None # Поля моделі, якщо не вказана форма

Важливі методи:

Усі методи з FormView, але доповнені під створення об’єкта:

post - попередньо додасть класу атрибут self.object = None

form_valid - додатково виконає такий рядок self.object = form.save()

Class UpdateView

Документація

Клас для оновлення об’єкта, як користуватися:

У views.py:

from django.views.generic.edit import UpdateView
from myapp.models import Author

class AuthorUpdate(UpdateView):
    model = Author
    fields = ['name']
    template_name_suffix = '_update_form'

в author_update_form.html:

<form method="post">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Update">
</form>

Методи і атрибути майже повністю збігаються з CreateView, тільки UpdateView, перед діями викликає метод get_object, для отримання потрібного об’єкта, і url повинен приймати pk для визначення цього об’єкта

Class DeleteView

Документація

Клас для видалення обхектів.

Як користуватися?

У views.py:

from django.urls import reverse_lazy
from django.views.generic.edit import DeleteView
from myapp.models import Author

class AuthorDelete(DeleteView):
    model = Author
    success_url = reverse_lazy('author-list')

У html:

<form method="post">{% csrf_token %}
    <p>Are you sure you want to delete "{{ object }}"?</p>
    <input type="submit" value="Confirm">
</form>

Не приймає форму! Приймає модель або кверісет, і обов’язково url повинен приймати ідентифікатор, для визначення об’єкта

Class LoginView

Документація

Клас, що реалізує логіку логіна.

Заснований на FormView, якщо форма не була замінена, то за замовчуванням використовує django.contrib.auth.forms.AuthenticationForm, ця форма містить два поля, username та password, перевіряє, чи дані є валідними, і якщо дані є валідними, а користувач активним, додає користувача до об’єкта форми.

Так само в LoginView переписаний метод form_valid:

def form_valid(self, form):
    """Security check complete. Log the user in."""
    auth_login(self.request, form.get_user())
    return HttpResponseRedirect(self.get_success_url())

Якщо форма валідна, то провести авторизацію.

Class LogoutView

Документація

Клас для logout.

У LogoutView переписано метод dispatch, тож яким би методом ви не звернулися до класу, ви все одно будете розлогінені.

Реєстрація

По суті реєстрація, це CreateView зі своїми особливостями (пароль хеширується), тому для реєстрації використовують просто CreateView, і існує заздалегідь описана форма UserCreationForm

class UserCreationForm(forms.ModelForm):
    """
 A form that creates a user, with no privileges, from the given username and
 password.
 """
    error_messages = {
        'password_mismatch': _("The two password fields didn't match."),
    }
    password1 = forms.CharField(label=_("Password"),
                                widget=forms.PasswordInput)
    password2 = forms.CharField(label=_("Password confirmation"),
                                widget=forms.PasswordInput,
                                help_text=_("Enter the same password as above, for verification."))

    class Meta:
        model = User
        fields = ("username",)

    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError(
                self.error_messages['password_mismatch'],
                code='password_mismatch',
            )
        return password2

    def save(self, commit=True):
        user = super(UserCreationForm, self).save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user

Приймає username і два рази пароль, перевіряє, щоб паролі були однакові, і під час збереження записує пароль у хешованому вигляді.

LoginRequiredMixin

Якщо необхідно закрити доступ для не залогінених юзерів від будь-якого з класів, то використовують LoginRequiredMixin,

Під час успадкування його необхідно вказати перед основним класом. Наприклад:

from .forms import MyForm
from django.views.generic import FormView
from django.contrib.auth.mixins import LoginRequiredMixin

class MyFormView(LoginRequiredMixin, FormView):
    template_name = 'index.html'
    http_method_names = ['get', 'post']
    form_class = MyForm
    success_url = '/'
    login_url = '/login/'

Додає в клас атрибут login_url, який визначає, куди потрібно перейти, якщо користувач намагається отримати доступ, але він не авторизований.

Живий приклад

Живий приклад правильніший, і тільки він великий.

П’єр Корнель

Припустимо, нам потрібен сайт, на якому можна зареєструватися, залогінитися, розлогінитися і написати замітку, якщо ти залогінений. Нотатки мають відображатися списком, остання створена відображається першою. Усі користувачі бачать усі замітки, біля тих, які створив поточний користувач, має бути кнопка видалити.

Як це зробити?

Розробка завжди починається з опису моделей, нам потрібно дві сутності, юзер і замітка.

Ми не будемо змінювати юзера, нам підходить стандартний.

Створимо модель замітки:

У models.py:

from django.contrib.auth.models import User
from django.db import models

# Create your models here.
class Note(models.Model):
    text = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notes')

    class Meta:
        ordering = ['-created_at', ]

Не забуваємо про міграції, і про додавання додатка в settings.py

Створимо необхідні шаблони: base, index, login, register. Поки що порожні, заповнимо трохи пізніше.

Створимо view. Для базової сторінки, на якій відображається список нотаток, найкраще підходить ListView, для логіна і логаута - наявні класи, для реєстрації - CreateView.

Для логіна і реєстрації скористаємося готовою формою.

Базову сторінку і логаут закриємо від не залогінених користувачів.

Виходить якось так:

У views.py

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView, LogoutView
from django.views.generic import ListView, CreateView

from app.models import Note

class NoteListView(LoginRequiredMixin, ListView):
    model = Note
    template_name = 'index.html'
    login_url = 'login/'

class Login(LoginView):
    template_name = 'login.html'

class Register(CreateView):
    form_class = UserCreationForm
    template_name = 'register.html'
    success_url = '/'

class Logout(LoginRequiredMixin, LogoutView):
    next_page = '/'
    login_url = 'login/'

Для того, щоб після успішного логіна всіх користувачів редиректувало на необхідну сторінку, необхідно в settings.py прописати таке
LOGIN_REDIRECT_URL = '/'
за замовчуванням, url такий /accounts/profile/

У urls.py проекту додамо через інклюд urls.py застосунку

У app/urls.py:

from django.urls import path
from .views import NoteListView, Login, Logout, Register

urlpatterns = [
    path('', NoteListView.as_view(), name='index'),
    path('login/', Login.as_view(), name='login'),
    path('register/', Register.as_view(), name='register'),
    path('logout/', Logout.as_view(), name='logout'),
]

І заповнимо html файли

base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Base</title>
</head>
<body>
<div>
    {% block content %}
    {% endblock %}
</div>
</body>
</html>

index.html

{% extends 'base.html' %}

{% block content %}
<div>
	<form method="post" action="{% url 'logout' %}">
		{% csrf_token %}
		<button type="submit">Logout</button>
	</form>
</div>
{% endblock %}

login.html

{% extends 'base.html' %}

{% block content %}
{% extends 'base.html' %}

{% block content %}
<span>Wanna register? <a href="{% url 'register' %}">Sign Up</a></span>

<form method="post">
    {% csrf_token %}
    {{ form }}
    <input type="submit" value="Login">
</form>
{% endblock %}

register.html

{% extends 'base.html' %}

{% block content %}
<span>Already has account? <a href="{% url 'login' %}">Login</a></span>

<form method="post">
    {% csrf_token %}
    {{ form }}
    <input type="submit" value="Register">
</form>
{% endblock %}

Структура для логіна, логаута і реєстрації готова.

Додамо відображення списку заміток:

в index.html:

{% extends 'base.html' %}

{% block content %}
<div>
	<form method="post" action="{% url 'logout' %}">
		{% csrf_token %}
		<button type="submit">Logout</button>
	</form>
</div>

<div>
    {% for obj in object_list %}
    <div>
        {{ obj.text }} from {{ obj.author.username }}
    </div>
    {% endfor %}
</div>
{% endblock %}

Але як додати створення нотаток?

Нам потрібна форма для створення нотаток, і CreateView

У forms.py

from django.forms import ModelForm

from app.models import Note

class NoteCreateForm(ModelForm):
    class Meta:
        model = Note
        fields = ('text',)

У полях тільки текст, тому що час створення буде заповнюватися автоматично, айді теж, а юзера ми будемо брати з реквесту.

Чи будемо ми відображати окрему сторінку для створення? Ні, значить окремий хтмл файл нам не потрібен, а якщо ми не будемо відображати сторінку, то і метод get нам не потрібен. Залишимо тільки post.

Створимо CreateView:

В views.py

class NoteCreateView(LoginRequiredMixin, CreateView):
    login_url = 'login/'
    http_method_names = ['post']
    form_class = NoteCreateForm
    success_url = '/'

B виведемо урл під цей клас:

У urls.py

from django.urls import path
from .views import NoteListView, Login, Logout, Register, NoteCreateView

urlpatterns = [
    path('', NoteListView.as_view(), name='index'),
    path('login/', Login.as_view(), name='login'),
    path('register/', Register.as_view(), name='register'),
    path('logout/', Logout.as_view(), name='logout'),
    path('note/create/', NoteCreateView.as_view(), name='note-create'),
]

Чи достатньо цього, щоб створювати нотатки? Ні, тому що ми нікуди не вивели форму для створення нотаток. Давайте виведемо її на нашу основну сторінку.

У views.py змінимо клас NoteListView, додавши атрибут extra_context = {'create_form': NoteCreateForm()}.

class NoteListView(LoginRequiredMixin, ListView):
    model = Note
    template_name = 'index.html'
    login_url = 'login/'
    extra_context = {'create_form': NoteCreateForm()}

Тепер ми можемо вивести форму в шаблоні, змінимо index.html

{% extends 'base.html' %}

{% block content %}
<div>
	<form method="post" action="{% url 'logout' %}">
		{% csrf_token %}
		<button type="submit">Logout</button>
	</form>
</div>

<div>
    {% for obj in object_list %}
    <div>
        {{ obj.text }} from {{ obj.author.username }}
    </div>
    {% endfor %}
</div>
<form method="post" action="{% url 'note-create' %}">
    {% csrf_token %}
    {{ create_form }}
    <input type="submit" value="Create">
</form>
{% endblock %}

Чи достатньо цього? Ні. Наша замітка повинна зберігати в собі користувача, а ми ніде його не додаємо. під час спроби викликати ``save()` ми отримаємо помилку, не можу зберегти без юзера.

Що будемо робити? Переписувати логіку form_valid, ми знаємо, що методsave() для CreateView викликається там.

Щоб додати користувача, будемо використовувати commit=False для ModelForm, а користувача візьмемо з реквесту.

Перепишемо клас NoteCreateView:

У views.py:

class NoteCreateView(LoginRequiredMixin, CreateView):
    login_url = 'login/'
    http_method_names = ['post']
    form_class = NoteCreateForm
    success_url = '/'

    def form_valid(self, form):
        obj = form.save(commit=False)
        obj.author = self.request.user
        obj.save()
        return super().form_valid(form=form)

Зверніть увагу після успіху, ми потрапляємо назад на / (success_url), де ми одразу ж побачимо нову замітку.

Створення готове.

Як додамо видалення? Створимо нову DeleteView, вона навіть не вимагає форму.

У views.py

class NoteDeleteView(LoginRequiredMixin, DeleteView):
    model = Note
    success_url = '/'

Не забуваємо додати url

У urls.py:

from django.urls import path
from .views import NoteListView, Login, Logout, Register, NoteCreateView, NoteDeleteView

urlpatterns = [
    path('', NoteListView.as_view(), name='index'),
    path('login/', Login.as_view(), name='login'),
    path('register/', Register.as_view(), name='register'),
    path('logout/', Logout.as_view(), name='logout'),
    path('note/create/', NoteCreateView.as_view(), name='note-create'),
    path('note/delete/<int:pk>/', NoteDeleteView.as_view(), name='note-delete'),
]

І додаємо форму для видалення в шаблон.

У index.html:

{% extends 'base.html' %}

{% block content %}
<div>
	<form method="post" action="{% url 'logout' %}">
		{% csrf_token %}
		<button type="submit">Logout</button>
	</form>
</div>

<div>
    {% for obj in object_list %}
    <div>
        {{ obj.text }} from {{ obj.author.username }}
        <form method="post" action="{% url 'note-delete' obj.pk %}">
            {% csrf_token %}
            <input type="submit" value="Delete">
        </form>
    </div>
    {% endfor %}
</div>
<form method="post" action="{% url 'note-create' %}">
    {% csrf_token %}
    {{ create_form }}
    <input type="submit" value="Create">
</form>
{% endblock %}

Це вже буде працювати. Але нам же потрібно, щоб кнопка видаляти була тільки у своїх нотаток. Додамо if.

{% extends 'base.html' %}

{% block content %}
<div>
	<form method="post" action="{% url 'logout' %}">
		{% csrf_token %}
		<button type="submit">Logout</button>
	</form>
</div>

<div>
    {% for obj in object_list %}
    <div>
        {{ obj.text }} from {{ obj.author.username }}
        {% if obj.author == request.user %}
        <form method="post" action="{% url 'note-delete' obj.pk %}">
            {% csrf_token %}
            <input type="submit" value="Delete">
        </form>
        {% endif %}
    </div>
    {% endfor %}
</div>
<form method="post" action="{% url 'note-create' %}">
    {% csrf_token %}
    {{ create_form }}
    <input type="submit" value="Create">
</form>
{% endblock %}

Залишилася маленька деталь, зараз ми відображаємо всі наявні нотатки, а що, якщо їх буде мільйон? це не раціонально, давайте додамо пагінацію.

ListView вже передає всі необхідні дані, нам потрібно тільки додати розмір сторінки і додати відображення за сторінками в шаблоні.

У views.py змінимо NoteListView

class NoteListView(LoginRequiredMixin, ListView):
    model = Note
    template_name = 'index.html'
    login_url = 'login/'
    extra_context = {'create_form': NoteCreateForm()}
    paginate_by = 5

А в index.html:

{% extends 'base.html' %}

{% block content %}
<div>
	<form method="post" action="{% url 'logout' %}">
		{% csrf_token %}
		<button type="submit">Logout</button>
	</form>
</div>

    <div>
        {% for obj in page_obj %} {# зверніть увагу я замінив об'єкт #}
            <div>
                {{ obj.text }} from {{ obj.author.username }}
                {% if obj.author == request.user %}
                    <form method="post" action="{% url 'note-delete' obj.pk %}">
                        {% csrf_token %}
                        <input type="submit" value="Delete">
                    </form>
                {% endif %}
            </div>
        {% endfor %}
        <div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1">&laquo; first</a>
            <a href="?page={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}

        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">next</a>
            <a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
        {% endif %}
    </span>
        </div>
    </div>
    <form method="post" action="{% url 'note-create' %}">
        {% csrf_token %}
        {{ create_form }}
        <input type="submit" value="Create">
    </form>
{% endblock %}

І, наостанок, додамо стилі в наш проєкт, оскільки користувацький інтерфейс має не дуже приємний вигляд.

Підключаємо Bootstrap 5 до базового шаблону
Документація тут

base.html:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>Base</title>  
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"  
  integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">  
</head>  
<body>  
<div>  
    {% block content %}  
    {% endblock %}  
</div>  
</body>  
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"  
  integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"  
  crossorigin="anonymous"></script>  
</html>

Будемо використовувати компонет card, детальніше тут
Також, виведемо дату створення замітки за допомогою тега date.

І знову змінюємо index.html:

{% extends 'base.html' %}  
  
{% block content %}  
<div>
	<form method="post" action="{% url 'logout' %}">
		{% csrf_token %}
		<button type="submit">Logout</button>
	</form>
</div> 
    <div>  
        <div class="row">  
            {% for obj in page_obj %}  
                <div class="card" style="width: 18rem; margin-left: 5px; margin-right: 5px">  
                    <div class="card-body">  
                        <div class="caption">  
                            <h3 class="card-title">{{ obj.text }}</h3>  
                            <p class="card-text">{{ obj.author.username }}</p>  
                            <p class="card-text">{{ obj.created_at|date:"d/m/Y" }}</p>  
                            {% if obj.author == request.user %}  
                                <form method="post" action="{% url 'note-delete' obj.pk %}">  
                                    {% csrf_token %}  
                                    <input type="submit" class="btn btn-primary" value="Delete">  
                                </form>  
                            {% endif %}  
                        </div>  
                    </div>  
                </div>  
            {% endfor %}  
        </div>  
    </div>  
        <div class="pagination">  
    <span class="step-links">  
        {% if page_obj.has_previous %}  
            <a href="?page=1">&laquo; first</a>  
            <a href="?page={{ page_obj.previous_page_number }}">previous</a>  
        {% endif %}  
  
        <span class="current">  
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.  
        </span>  
  
        {% if page_obj.has_next %}  
      <a href="?page={{ page_obj.next_page_number }}">next</a>  
  <a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>  
  {% endif %}  
    </span>  
        </div>  
   </div>  
    <form method="post" action="{% url 'note-create' %}">  
  {% csrf_token %}  
  {{ create_form }}  
     <input type="submit" value="Create">  
    </form>  
{% endblock %}

Вийшов цілком робочий додаток для ведення нотаток
введіть тут опис зображення

Переходимо до завдання на модуль.

Література (що почитати)

  1. Django : Class Based Views vs Function Based Views
  2. Classy Class-Based Views