Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Latest commit

 

History

History
812 lines (526 loc) · 39.1 KB

lesson32.md

File metadata and controls

812 lines (526 loc) · 39.1 KB

Урок 32. Формы, реквест, юзер, авторизация.

HTML формы

HTML форма - это специальный тег, который "сообщает" браузеру, что данные из этого тега нужно сгруппировать и подготовить к отправке на сервер.

Принимает два параметра action и method.

action описывает, куда форма по результату должна обращаться (в нашем случае это будет url), если не указан явно, то форма будет отправлена на тот же урл, на котором сейчас находится.

method отвечает за метод отправки, варианты - get и post. get будет использован по умолчанию, если не указан явно.

Формы с методом POST используются для передачи данных, не подлежащих огласке, например, логин и пароль. Формы с методом GET используются для общедоступной информации, например, строки поиска.

Внутри формы мы указываем нужное количество тегов <input> с нужными типами. Именно эти данные будут впоследствии переданы серверу.

Ссылка на общее описание форм

Основные типы инпутов

number - ввод числа

text - ввод текста

checkbox - чекбокс (выбор нескольких элементов через галочки)

radio - радиобаттон (выбор только одного элемента из списка)

button - классическая кнопка (если в форме есть один такой элемент, но нет сабмита, браузер автоматически посчитает его сабмитом)

hidden - скрытое поле, чаще всего нужно для целей безопасности или добавления информации и данных, не отображая их (не отображается)

submit - отправка формы

Это далеко не все типы инпутов, которые могут быть. Ссылка на типы инпутов

GET форма

Создадим простейшую форму. Для этого создадим url, функцию для обработки и HTML страницу.

В urls.py

...
from .views import form_view

...

...
path('form-url/', form_view, name='form-view'),
...

Во views.py

def form_view(request):
    return render(request, 'form.html')

В form.html

{% extends 'base.html' %}

{% block content %}
<form method="get" action="{% url 'form-view' %}">
    <label>
        <input type="text" name="my_name">
    </label>
    <button type="submit">Send name</button>
</form>
{% endblock %}

Обратите внимание, мы указали GET форму, action - урл на эту же страницу, который обрабатывается нашей же функцией form_view.

В форме у нас один input, которому мы указали 2 атрибута type и name.

Атрибут name нам необходим для того, чтобы мы смогли обработать данные во view.

Также у нас есть кнопка submit, она необходима для того, чтобы отправить запрос на сервер.

При нажатии на кнопку формируется и отправляется request.

Примерно вот так будет выглядеть наша страница если зайти на адрес http://127.0.0.1:8000/form-url/:

request

Объект request мы принимаем первым параметром функции обработчика и его же передаём первым в функцию render. Зачем он нужен и из чего он состоит?

Зачем нужен? Чтобы обрабатывать любые пользовательские или служебные данные, которые были переданы.

Из чего состоит? Состоит из переданных данных или файлов (если были переданы) и служебной информации (информации о пользователе, методе запроса, о том, на какой url был запрос, из какого браузера, другой системной информации, о ней отдельная лекция).

Давайте отправим request

Что будет после нажатия кнопки Send name?

Будет сформирован GET (метод формы) запрос со всеми заполненными нами данными и отправлен на сервер.

Обратите внимание на новый url.

my_name - это предварительно указанный атрибут name на нашей форме, а Vlad - значение, которое я передал в этот инпут.

В случае GET запроса данные передаются явно, прям в url в виде ключ-значение. Если бы значений было больше одного, они были бы соединены при помощи символа & (например, если бы я добавил к полю с указанным атрибутом name еще и поле с атрибутом age и заполнил бы его значением 26, то url после запроса выглядел бы так /form-url/?my_name=Vlad&age=26). Никакой разницы между заполнением формы или записью этих данных руками прям в строке браузера для GET запроса нет.

Обработка данных во view

Мы можем обработать данные во view при помощи переменной request. Данные из GET запроса будут находиться в переменной request.GET

Данные находятся в виде словаря, где ключами являются атрибуты name в каждом инпуте формы.

Эти данные можно использовать для любых целей, но чаще всего через GET передаются данные по фильтрации или дополнительные параметры отображения. Например, когда вы добавляете фильтры в интернет магазине, пишете текст в поиске, или когда на YouTube пересылаете ссылку с таймкодом, она тоже передаётся как GET параметр.

POST запрос

Давайте заменим метод нашей формы с GET на POST:

В form.html:

{% extends 'base.html' %}

{% block content %}
<form method="post" action="{% url 'form-view' %}">{# Тут я поменял метод #}
    <label>
        <input type="text" name="my_name">
    </label>
    <button type="submit">Send name</button>
</form>
{% endblock %}

Что произойдёт при отправке такого запроса?

Произойдёт примерно такая ошибка:

Это ошибка CSRF токена.

Чтобы понять, что это, нужно понимать разницу того, где используются разные запросы.

GET запросы - это запросы общедоступные и информационные: открыть страницу, отфильтровать данные и т. д.

POST запросы - это запросы с чувствительными данными: создание записей в базе, передача пароля, отправка денег со счёта на счёт и т. д.

Так вот, если GET запрос отправить 5 раз подряд, то с точки зрения сервера ничего не изменится, вы просто 5 раз подряд запросите одну и туже информацию.

Если изменить параметры, то тоже ничего страшного не произойдёт, просто запросятся другие данные.

А вот если повторить несколько раз или подделать данные в POST запросе, то можно совершить разные проблемные действия: создание лишних записей в базе данных, перевод средств на счёт злоумышленников вместо ожидаемого и т. д.

Поэтому в Django изначально есть дополнительное требование к POST формам - это еще одно скрытое поле, заранее сгенерированное сервером. Оно называется CSRF токен, где он проверяется и почему мы видим ошибку, мы разберём на следующих занятиях.

Чтобы добавить нужный токен, используется специальный темплейт тег {% csrf_token %}. Его нужно добавить в любом месте внутри тега <form>.

{% extends 'base.html' %}

{% block content %}
<form method="post" action="{% url 'form-view' %}">
    {% csrf_token %}{# Тут я добавил темплейт тег #}
    <label>
        <input type="text" name="my_name">
    </label>
    {# А мог и тут #}
    <button type="submit">Send name</button>
    {# Или тут, не имеет значения #}
</form>
{% endblock %}

Что изменится с точки зрения HTML:

Появилось поле типа hidden. Это значит, что оно не будет отображаться, но эти данные все равно попадут на сервер. Это часто используется, когда вам нужно передать данные, которые у вас уже есть при отрисовке, но их не видно явно. Допустим, если мы пишем комментарий к комментарию, то чтобы грамотно его создать, нам нужен id родителя, его обычно и передают как hidden поле.

Теперь наш запрос отправится успешно.

Обратите внимание, что url не изменится!

Потому что данные отправленные через POST не должны быть общедоступны.

Обработка во view

Обработать данные из POST запроса можно точно также, данные будут находиться в переменной request.POST, если это просто данные, и в request.FILES, если были переданы файлы.

Обратите внимание, что вместе с нашими данными был передан и csrf токен. Обычно при обработке данных он не нужен, но данные были переданы, а значит они придут на сервер.

Django Forms

Django предоставляет нам возможность генерировать HTML формы из кода на Python!

Что для этого нужно? Создадим в нашем приложении файл forms.py

Внутри этого файла укажем:

forms.py

from django import forms


class MyForm(forms.Form):
    nickname = forms.CharField(label='My nickname', max_length=100)
    age = forms.IntegerField(label='My age')

Обработчик для урла заменим на:

Во views.py заменим нашу функцию на:

from django.shortcuts import render

from .forms import MyForm


def form_view(request):
    # if this is a POST request we need to process the form data
    if request.method == 'POST':
        # create a form instance and populate it with data from the request:
        form = MyForm(request.POST)
        # check whether it's valid:
        if form.is_valid():
            # process the data in form.cleaned_data as required
            # some actions
            return render(request, 'form_was_valid.html')

    # if a GET (or any other method) we'll create a blank form
    else:
        form = MyForm()

    return render(request, 'form.html', {'form': form})

Не забываем импортировать форму

Изменим файл form.html:

{% extends 'base.html' %}

{% block content %}
<form method="post" action="{% url 'form-view' %}">
    {% csrf_token %}
    {{ form }} {# Инпуты были заменены на переменную, в которой лежит объект класса Form #}
    <button type="submit">Send form</button>
</form>
{% endblock %}

и создадим файл form_was_valid.html:

{% extends 'base.html' %}

{% block content %}
<div style="background-color: deeppink"> FORM WAS VALID</div>
<a href="{% url 'form-view' %}">To the form page</a>
{% endblock %}

Что именно мы сделали?

Описание формы

В файле forms.py мы создали класс формы, в котором описали два атрибута nickname и age.

Они будут соответствовать двум инпутам, текстовому и числовому.

Естественно, типов существует гораздо больше.

Основные типы:

BooleanField - булево значение

CharField - текст

ChoiceField - поле для выбора

DateTimeField - дата/время

EmailField - имейл

FileField - файл

IntegerField - целое число

MultipleChoiceField - множественный выбор

И многие другие, почитать про них нужно тут

У полей формы есть такое понятие как виджет. Он отвечает за то, как именно будет отображаться конкретное поле, например, для текста - это текстовое поле, а для даты и времени - это встроенный пикер (выпадающее окно с календарём и часами) и т. д.

Виджет можно указать отличающийся от стандартного.

Прочитать про виджеты нужно тут.

Каждому полю мы можем указать дополнительные аттрибуты:

`required - является ли поле обязательным

label- лейбл, подпись к инпуту

label_suffix - символ между label и инпутом

initial - значение по умолчанию

widget - читай выше

help_text - подсказка к инпуту

error_messages - переписать стандартные тексты для ошибок типов полей

validators - дополнительные проверки поля

localize - информация о переводе формы на другие языки

disabled - сделать поле не активным (без возможности изменения)

Описание view

В переменной request хранится информация о том, какой именно тип запроса к нам пришел, а это значит, что простым if мы можем разграничить логику, которая будет обрабатывать разные типы запросов.

Если мы просто открываем страницу в браузере, то на самом деле мы посылаем обыкновенный GET запрос.

Взглянем на код. При GET запросе мы не попадаем в первое условие, переменной form назначаем объект класса MyForm без каких-либо данных, и после этого рендерим страницу, передав на страницу пустой объект класса формы.

При рендере объекта класса формы в шаблоне этот объект преобразуется в набор инпутов с уже указанными атрибутами name

Если мы заполним данные и нажмём на кнопку Send form, то мы отправим по этому же url запрос, но уже типа POST с заполненными данными.

Посмотрим в код еще раз, мы попадём в первый if и переменной form назначим объект класса MyFrom, но предварительно передав туда данные через request.POST.

А значит на этом этапе у нас есть объект с данными, переданными нам от клиента.

Данные, которые мы получили из реквеста, всегда нужно валидировать (проверять).

Валидация формы

Тут вся дока по валидации.

За валидацию данных в форме отвечает встроенный метод is_valid() который применяется к объекту класса формы.

Этот метод возвращает нам булево значение: True, если данные валидны, False, если нет.

После вызова этого метода у переменной, к которой он был вызван (в нашем случае переменная form), появляются дополнительные атрибуты.

Если форма валидна, то появляется дополнительный аттрибут cleaned_data - это словарь, в котором хранятся все данные, присланные нам пользователем (например, логин и пароль).

Если форма не валидна, то появляется дополнительные аттрибут errors, который хранит в себе информацию об ошибках конкретных полей или общих ошибках.

Этот атрибут сразу хранит информацию о том, как отображать эти ошибки в шаблоне, если они существуют.

Валидность

Что же такое валидность?

Валидность - это соответствие заданным критериям. Например, если мы ожидаем в поле возраста получить числовой тип, а пользователь отправляет текст, то данные не валидны.

Некоторые распространённые виды валидаций можно указать как атрибут поля формы, например, максимальную длину для строки, максимальное и минимальное значение для числа.

clean_field()

Если мы вызываем метод is_valid(), мы проверяем все описанные валидации. Но где они описаны, и можем ли мы добавить свои?

Описаны они в классе формы, и да, мы можем добавить свои.

Все базовые валидации были описаны при создании полей.

Но допустим, что мы считаем, что для нашей формы валидным является только чётный возраст, как нам это проверить?

Для проверки конкретного поля в форме класса нужно указать метод, который будет начинаться со слова clean_ и после этого название поля, которое мы валидируем.

Все данные будут лежать в аттрибуте self.cleaned_data.

Если значение валидно, то метод должен возвращать значение этого аттрибута.

Если значение не валидно, то метод должен возбуждать ошибку ValidationError с описанием ошибки, которая позже будет отображаться в html.

В forms.py:

from django import forms
from django.core.exceptions import ValidationError


class MyForm(forms.Form):
    nickname = forms.CharField(label='My nickname', max_length=100)
    age = forms.IntegerField(label='My age')

    def clean_age(self):
        age = self.cleaned_data.get('age')
        if not age % 2:
            raise ValidationError('Age should be even.')
        return age

clean()

А что делать если нужно проверить соответствие данных между собой? Например, что пользователь не использовал свой возраст, как часть своего никнейма?

Для этого мы можем использовать метод clean(), в котором можем выполнить все необходимые нам проверки.

Для выполнения всех базовых проверок обычно используется super().

В forms.py

from django import forms
from django.core.exceptions import ValidationError


class MyForm(forms.Form):
    nickname = forms.CharField(label='My nickname', max_length=100)
    age = forms.IntegerField(label='My age')

    def clean_age(self):
        age = self.cleaned_data.get('age')
        if not age % 2:
            raise ValidationError('Age should be even.')
        return age

    def clean(self):
        cleaned_data = super().clean()
        age = cleaned_data.get('age')
        nickname = cleaned_data.get('nickname')
        if str(age) in nickname:
            raise ValidationError('Age can\'t be in nickname')

Метод clean() ничего не возвращает, это нормально :)

Если при проверке у вас может быть больше одной ошибки, то raise вам не подходит.

Для этого может использоваться метод класса формы add_error(). Он принимает два параметра: название поля, к которому относится ошибка (может быть None, если ошибка не относится к какому-либо полю), и сообщение, например, неправильные имя пользователя и/или пароль.

В forms.py

from django import forms
from django.core.exceptions import ValidationError


class MyForm(forms.Form):
    nickname = forms.CharField(label='My nickname', max_length=100)
    age = forms.IntegerField(label='My age')

    def clean_age(self):
        age = self.cleaned_data.get('age')
        if not age % 2:
            raise ValidationError('Age should be even')
        return age

    def clean(self):
        cleaned_data = super().clean()
        age = cleaned_data.get('age')
        nickname = cleaned_data.get('nickname')
        if str(age) in nickname:
            self.add_error('age', 'Age can\'t be in nickname')
        self.add_error(None, 'This form is always incorrect')

Отображение формы в шаблоне

Итак, если наша форма была валидна, то мы отрендерили вообще другую страницу, но если всё-таки была не валидна, то мы отрендерим форму, у которой есть атрибут errors, ошибки сразу же будут отрисованы.

Также у нас есть способы по разному отрисовывать формы:

У объекта формы есть стандартные поля и методы, которые мы можем указывать в шаблоне, например:

{{ form.as_table }} - рендер в виде таблицы, через теги

{{ form.as_p }} - рендер каждого поля через теги

{{ form.as_ul }} - рендер в виде списка через теги

  • Также можно рендерить форму не целиком, а, например, по отдельным полям, при помощи стандартного обращения через точку: {{ form.name }}.

    У каждого поля есть аттрибут errors, который хранит информацию об ошибках по этому полю, если они были обнаружены: {{ form.my_field.errors }}.

    Если запустить форму через for в итерируемом объекте будут поля.

    {% for field in form %}
    <div class="fieldWrapper">
        {{ field.errors }}
        {{ field.label_tag }} {{ field }}
        {% if field.help_text %}
        <p class="help">{{ field.help_text|safe }}</p>
        {% endif %}
    </div>
    {% endfor %}

    И многие другие атрибуты и методы, подробно можно прочитать тут

    Методы и свойства модели User

    Модель User

    Django предоставляет нам встроенную модель юзера, у которой уже реализовано много полей и методов.

    Подробнейшая информация про юзера тут

    Стандартный юзер содержит в себе такие полезные поля как:

    username
    password
    email
    first_name
    last_name
    

    Также содержит много системных полей.

    Также содержит базовый метод set_password() и информацию о группах доступа.

    Для использования модели пользователя, которую нам нужно расширить (а это нужно почти всегда), используется наследование от базового абстрактного юзера.

    Выглядит примерно так:

    from django.contrib.auth.models import AbstractUser
    from django.db import models
    
    
    class MyUser(AbstractUser):
        birth_date = models.DateField()
        avatar = models.ImageField(blank=True, null=True)

    Чтобы Django оценивала эту модель как пользователя в settings.py нужно в любом месте указать:

    AUTH_USER_MODEL = 'myapp.MyUser' 

    Где myapp - название приложения, MyUser - название модели.

    Юзер обязательно должен быть описан до первой миграции!! Иначе Django автоматически будет использовать базового встроенного юзера, и использовать сразу несколько юзеров у вас не получится. Так как по дефолту, если этой переменной нет, то Django считает, что там указанна ссылка на базового юзера, и создаёт таблицу юзера, основываясь на базовом юзере, поменять такую таблицу нельзя.

    Все возможные подробности про модель юзера тут

    Кроме стандартных полей юзер содержит в себе информацию о группах, в которых состоит пользователь, о пользовательских правах, два поля статуса is_staff и is_superuser (чаще всего стафф - это сотрудник, которым можно в админку, но у них ограниченные права, суперюзеру можно всё, но всегда зависит от ситуации).

    is_stuff - поле для определения сотрудника (допустим, сотрудник магазина, который добавляет товары)

    is_superuser - поле для определения администратора (например, может изменять список сотрудников, а чаще всего обладает практически неограниченными параметрами)

    Также хранит инфо о последнем логине пользователя и дате создания пользователя.

    Объект юзера содержит кучу полезных методов

    get_username()  # получить юзернейм
    
    get_full_name()  # получить имя и фамилию через пробел
    
    set_password(raw_password)  # установить хешированный пароль
    
    check_password(raw_password)  # проверить пароль на правильность
    
    set_unusable_password()  # разрешить использовать пустую строку как пароль
    
    email_user(subject, message, from_email=None, **kwargs)  # отправить пользователю имейл 

    Например:

    u = User.objects.get(username='blabla')
    u.check_password('some_cool_password')  # True

    И другие методы, отвечающие за доступы, группы и т. д.

    Менеджер юзера

    Содержит дополнительные методы

    create_user(username, email=None, password=None, **extra_fields)

    create_superuser(username, email, password, **extra_fields)

    create_user() отличается от create() тем, что create_user() правильно задаст пароль через set_password()

    ** Мы не храним пароль в чистом виде, только хешированным. **

    Логин

    На самом деле логин состоит из нескольких частей, давайте их рассмотрим.

    Аутентификация, идентификация, авторизация

    Аутентификация - процесс проверки подлинности доступа.

    Например, проверить логин и пароль на соответствие.

    Если говорить о бытовом примере, то когда вы проходите на любую проходную, например, заходя в университет, вы должны предъявить студенческий. То, что он у вас есть и есть процесс аутентификации.

    Идентификация - процесс определения конкретного лица.

    Например, получить конкретного пользователя из базы.

    В примере с университетом, если охранник возьмет ваш студенческий и прочитает, как ваз зовут и из какой вы группы, это и будет идентификация.

    Авторизация - процесс предоставления доступа.

    Охранник вас пропустит.

    Как это работает?

    Чтобы пользователь мог авторизоваться на сайте, нам нужны его входные данные и стандартные методы authenticate, login

    Метод authenticate отвечает сразу за два процесса: аутентификацию и идентификацию. Он принимает имя пользователя и пароль, и если находит совпадение, то возвращает объект пользователя(модели), если не находит, то возвращает None.

    Если нам вернулся объект юзера, значит, аутентификация пройдена, и пользователь идентифицирован.

    Метод login принимает реквест и объект модели пользователя и отвечает за процесс авторизации, после этого действия во всех следующих запросах в переменной request будет храниться наш текущий пользователь.

    Поэтому стандартным способом авторизации является примерно такой код:

    В forms.py

    from django.contrib.auth import authenticate
    from django import forms
    
    
    class AuthenticationForm(forms.Form):
        username = forms.CharField(max_length=254)
        password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
    
        def clean(self):
            cleaned_data = super().clean()
            username = cleaned_data.get('username')
            password = cleaned_data.get('password')
            if username and password:
                self.user = authenticate(username=username, password=password)
                if self.user is None:
                    raise forms.ValidationError()

    в view.py

    from .forms import AuthenticationForm
    from django.contrib.auth import login
    
    
    def my_login(request):
        # if this is a POST request we need to process the form data
        if request.method == 'POST':
            # create a form instance and populate it with data from the request:
            form = AuthenticationForm(request.POST)
            # check validity:
            if form.is_valid():
                # process the data in form.cleaned_data as required
                # some actions
                login(request, form.user)
                return HttpResponseRedirect('/')
    
        # if a GET (or any other method) we'll create a blank form
        else:
            form = AuthenticationForm()
    
        return render(request, 'login.html', {'form': form})

    Logout

    Для вывода пользователя из системы используется метод logout, который принимает только реквест.

    from django.contrib.auth import logout
    
    
    def logout_view(request):
        logout(request)
        # Redirect to a success page.

    Проверка на то, что пользователь уже зашел в систему

    В реквесте всегда есть поле user, у которого всегда есть аттрибут is_authenticated, проверяя его, мы можем определять является ли пользователь авторизированным.

    request.user.is_authenticated

    Закрыть страницу от незалогиненного пользователя

    Чтобы не предоставлять доступ незалогиненным пользователям, существует два способа: для функционально описанных views - это декоратор @login_required

    from django.contrib.auth.decorators import login_required
    
    
    @login_required
    def my_view(request):
        ...

    Он также может принимать ссылку на страницу логина и автоматически отправлять на эту страницу незалогиненного пользователя.

    from django.contrib.auth.decorators import login_required
    
    
    @login_required(login_url='/accounts/login/')
    def my_view(request):
        ...

    Практика / Домашка:

    1. Пишем страницы для логина и для регистрации (на каждой из них должна быть ссылка на другую).
    2. Если пользователь не залогинен, то его должно перебрасывать на страницу с логином.
    3. Добавляем в верхнюю часть главной страницы перечисление существующих топиков. При нажатии на которые мы должны видеть отфильтрованный список блогов, относящихся только к выбранному топику (GET запрос).
    4. Добавляем строку для поиска по блогам. После поиска должны отображаться посты, в названии которых есть частичное совпадение без учета регистра с искомыми данными. (GET форма)
    5. Добавляем возможность создания поста.
    6. На странице с деталями поста добавляем возможность писать комментарии.