#33. Django ModelForm, CBV
Django ModelForm, Class-Based View
План
I. ModelForm
- ModelForm
- Валідація
- Метод save() у форми
- Передача об’єкта
II. Class-based View
- Class View
- Class TemplateView
- Class RedirectView
- Class DetailView
- Class ListView
- Class FormView
- Class CreateView
- Class UpdateView
- Class DeleteView
- Class LoginView
- Class LogoutView
- 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().
Якщо перший відповідає за валідацію форми, то другий відповідає за валідацію для об’єкта моделі.
Перевірка об’єктів моделі проходила в три етапи:
- Перевірка полів моделі - Model.clean_fields()
- Перевірка всього об’єкта - Model.clean()
- Перевірка унікальності полів - 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">« 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 »</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">« 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 »</a>
{% endif %}
</span>
</div>
</div>
<form method="post" action="{% url 'note-create' %}">
{% csrf_token %}
{{ create_form }}
<input type="submit" value="Create">
</form>
{% endblock %}
Вийшов цілком робочий додаток для ведення нотаток
