Джанго - фреймворк. Фреймворк - типовое решение для решения типовых проблем.
Установка последней версии
pip3 install django
Установка определённой версии
pip3 install django==3.2
По умолчанию подключается sqlite база данных.
Поддерживаются много разных реляционных баз данных. Чтобы изменять настройки подключения надо изменить параметр DATABASES
в settings.py
:
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "DB_NAME",
"USER": "DB_USER_NAME",
"PASSWORD": "DB_USER_PASSWORD",
"HOST": "127.0.0.1",
"PORT": "5433",
}
}
и установить драйвер для postgresql (binary версию в случае линукса)
pip3 install psycopg2-binary
После этого станет доступна команда django-admin
.
django-admin startproject project_name
При этом создастся в одноименный каталог с файлом с именем manage.py
и внутри ищё один каталог с именем проекта.
manage.py
- не стоит менять этот файл. Этот файл используется в командной строке для внесения изменений в джанго-проект. Он принимает на вход аргументы, обрабатывает их и изменяет проект.__init__.py
- пустой, служит для того, чтобы каталог с ним стал питоновским пакетом.asgi.py
,wsgi.py
- необходимы при деплое проекта.setting.py
- содержит константы, отвечающие за настройки всего проекта.url.py
- регистрирует все страницы для сайта.
Файл manage.py
делает то же, что и django-admin
, но подставляет нужные переменные окружения.
По
тому запускает этот файл через интерпретатор питона
python3 manage.py runserver 8000
Последним аргументом опционально указывается порт, на котором будет работать дев сервер.
Проект в джанго стоит из приложений. Чистый проект в django уже состоит из нескольких приложений.
Список приложений можно увидеть в настройках (файл setup.py
) в переменной INSTALLED_APPS
Для создания собственный приложений надо воспользоваться командой
python3 manage.py startapp app_name
при этом создается одноименная папка.
URL - Uniform Resource Locator.
- http:// - протокол
- доменное имя
- ссылка на index.html - главную страницу сайта
- каждому url соответствует определённый результат на сайте
- часть совпадает с доменным именем - остальная роут - маршрут
Возможный пример роута car-online.ru
:
/
- стартовая страница/cars
- список марок машин/cars/volvo
- список моделей марки вольво/cars/volva/xc-90
- информации ок конкретной марки вольво
Для каждого ендпоинта надо описать логику. Логика описывается во views. views.py
Это может быть либо функция, любо класс для каждого url.
Переменная URLConf, которая задает список страниц на стайте (список эндпоинтов) называется urlpatterns
и находится в файле urls.py
. Содержит список всех url, которые обрабатываются бэкэндом.
from horoscope.views import leo
urlpatterns = [
path('leo/', leo),
]
При этом будет получено status код 301, если клиент зашел на ссылку без слеша. В этом случае браузер перенаправит пользователя на адрес со слешем на конце.
Теперь надо определеить представление, которое будет срабатывать при переходе по новому url.
создадим нужное представление в файле views.py
.
#vies.py
from django.http import HttpResponse
def leo(request):
return HttpResponse("<p>Hello from Django!</p>")
Данное представление вернёт страницу с кодом 404
# views.py
from django.http import Http404
def get404(request):
...
return Http404
https://django.fun/ru/docs/django/4.1/topics/http/urls/
Это файл urls.py
.
Можно импортировать роуты из другого приложения, чтобы каждый раз не подставлять путь приложения. Таким образом можно подключить роут приложения по адресу приложения.
Для этого в индексном роуте есть инструкция по импорту дочерних роутов из приложений джанго.
Вместо приложения роуты также можно импортировать из пакета (директории с __init__.py
).
# project_folder/urls.py
from django.urls import include
from django.url import path
urlpatterns = [
path('horoscope/', include('app_name.views')),
]
В дочернем роуте надо создать такой же параметр urlpatterns
. В подключаемых роутах также можно подключить другой роут или контроллер (вьюху).
# app_folder/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('leo/', views.leo),
]
В VSC может придётся создать следующий файл конфигурации.
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Django",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/django-projects/my_site/manage.py",
"args": [
"runserver", "777"
],
"django": true,
"justMyCode": true
}
]
}
Для того, чтобы Django воспринимал параметр в роуте его надо поместить в <parameter>
.
Также данный паарметр надо передать внутрь нашего вьюшки, которая обрабатывает запросы от этого роута.
# urls.py
urlpatterns = [
path('<sign>', view.sign_controller),
]
#views.py
def sign_controller(request, sign):
pass
По-умолчанию все параметры считываются в виде строки.
Конвертеры позволяют конвертировать динамические параметры в нужный ип данных или брать только часть url строки.
При этом система автоматически заресолвит тот роут, что первым подойдёт по списку. Поэтому роут с самыми общими правилами надо ставить самым последним, иначе до остальных роутов дело может так и не дойти.
конвертер | описание |
---|---|
int |
целое число |
str |
строка до первого символа / |
slug |
слаг - строка из букв, цифр, - и _ |
uuid |
тип уникального идентификатора - прописные буквы разделённые тире |
path |
включает не пустую строку, содержащую весь путь включая последующие / |
Также можно регистрировать свои собственные конверторы.
# horoscope/urls.py
urlpatterns = [
path('<str:sign_of_zodiac>', get_info_by_sign),
path('<int:zodiac_by_number>', get_info_by_number),
]
def get_info_by_number(request, zodiac_by_number: int):
pass
Перенаправление (redirect) - это способ перенаправления пользователей с одной страницы на другую. При этом он может пригодиться например при перенаправлении пользователей с адреса week_days/1/
на week_days/monday/
.
Redirect в Django можно выполнить при помощи класса HttpResponseRedirect
:
from django.http import HttpResponseRedirect
def my_controller(request):
return HttpResponseRedirect("https://yandex.ru")
За классом HttpResponseRedirect
возвращает статус код 302.
Есть и другие классы, которые возвращают другие коды редиректа:
класс | код |
---|---|
HttpResponseRedirect |
302 |
HttpResponsePermanentRedirect |
301 |
HttpResponseNotModified |
304 |
Это функция - обертка, которая возвращает или класс HttpResponseRedirect
или класс HttpResponsePermanentRedirect
.
from django.shortcuts import redirect
def my_controller(request):
return redirect('http:google.com', permanent=True,)
# urls.py
urlpatterns = [
path('some_address/', views.controller, name='route_alias_name'),
]
Теперь осталось передать это имя функции reverse
в контроллере.
Она позволяет генерировать url адреса согласно имени url адреса в приложении.
reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None)
viewname
- строка, содержащая имя URL адреса. Обязательный аргумент.urlconf
- позволяет задать имя модуля с настройками url адресовurls.py
. Нужен для того, чтобы задать другой модуль со своим файлом urls.py.args
- список или кортеж элементов для передачи вurl
адрес:reverse('my_view_name', args=[1])
kwargs
- словарь именованных аргументов для передачи в url адрес.reverse('my_view_name2', kwargs={'width': 2})
# views.py
from django.urls import reverse
def controller(request):
redirect_url = reverse('square_area', kwargs={'width': 2})
return HttpResponseRedirect(redirect_url)
Можно задать пространство имен во urls.py при помощи переменной app_name
#urls.py
app_name = "horoscope"
urls = [
path("/", views.index, name="zodiac_index"),
]
тогда надо при указании псевдонимов использовать это пространство имен:
# views.py
from django.urls import reverse
pass
redirect_url = reverse(viewname="horoscope:zodiac_index")
Страница для всех знаков зодиака. Для этого надо внедрить html код.
Пример базовых конвертеров можно найти в модуле from django.urls import converters
.
Конвертер представляет собой класс из трех составляющих:
regex
- переменная, описывающая регулярное выражениеto_python(self, value)
- функция, конвертирущая найденную строку в питоновский типto_url(self, value)
- функция, конвертирующая питоновский объект в представление строки
Данный класс помещается в файл converters.py
.
# converters.py
class FourDigitsConverter:
regex = r'\d{4}'
def to_python(self, value) -> int:
return int(value)
def to_url(self, value) -> str:
return f"{value:04}"
Для регистрации конвертера нужно импортировать
from django.urls import register_converters
# urls.py
from django.urls import register_converters
from . import converters # импорт модуля с классом конвертера
# регистрация собственного конвертера
register_converters(converters.FourDigitConverter, 'yyyy')
urlpatterns = [
path('<yyyy:year_num>', views.get_info-about_year,),
]
Юнит тесты или модульное тестирование. Будем тестировать отдельные модули - представления во вьюхах. Обычно тестирование приложения в джанго происходит таким образом - что проверяются адреса страниц и то, что пользователь может по этим адресам получить контент.
Обычно тесты на уровне приложения создаются в корне проекта в каталоге tests
.
Но мы будем писать тесты на уровне приложения в каталоге приложения - файле tests.py
.
Создаем класс, отнаследованный от TestCase
и внутри пишем набор функция, каждая их которых тестирует одно наше представление.
Названия методов для тестирования в классе должны начинаться с test_
.
Самый важный для нас атрибут это self.client
. Под ним подразумевается объект, который заменяет нам браузер. Браузер может отправлять все виды запросов (в том числе get
и post
).
Также есть метод assertEqual
, который проверяет два параметра на равенство.
response = self.client.get(url)
status_code = response.status_code
self.assertEqual(200, status_code)
# tests.py
from django.test import TestCase
class TestHoroscope(TestCase):
def test_index(self):
response = self.client.get('/horoscope/')
self.assertEqual(200, response.status_code)
def test_libra_redirect(self):
response = self.client.get('/horoscope/7/')
self.assertEqual(302, response.status_code)
self.assertEqual(response.url, '/horoscope/libra/')
Можно из ответа получить страницу в байтах: response.content
.
Контент можно декодировать в строку: response.content.decode()
.
Далее можно проверить на вхождение чего-то в результате:
def test_libra(self):
response = self.client.get('/horoscope/')
self.assertEqual(200, response.status_code)
self.assertIn('libra', response.content.decode())
И конечно можно и нужно использовать функцию reverse
для ресолва url адресов по их псевдонимам.
assert | выражение |
---|---|
assertEqual(a, b) |
a == b |
assertNotEqual(a, b) |
a != b |
assertTrue(x) |
bool(x) is True |
assertFalse(x) |
bool(x) is False |
assertIs(a, b) |
a is b |
assertIsNot(a, b) |
a is not b |
assertIsNone(x) |
x is None |
assertIsNotNone(x) |
x is not None |
assertIn(a, b) |
a in b |
assertNotIn(a, b) |
a not in b |
assertIsInstance(a, b) |
isinstance(a, b) |
assertNotIsInstance(a, b) |
not isinstance(a, b) |
Команда для запуска тестирования приложения:
python3 manage.py test app_name
{
"name":"Python: horoscope_test",
"type":"python",
"request":"launch",
"python":"путь до python.exe",
"program":"путь до manage.py",
"args":[
"test",
"horoscope"
],
"django":true,
"justMyCode":true
}
Django поддерживает кроме своего шаблонизатора Django Template Language ещё и сторонние шаблонизаторы, например jinja2.
Шаблон в джанге - это статические html файл, который состоит из статических частей и динамических.
Шаблоны создаются в каталоге приложения в директории templates/
.
По договоренности внутри создается еще один каталог с именен приложений.
Например валидное такое название файла: django_project/app_name/templates/app_name/app_name_index.html
.
Ещё может понадобиться зарегистрировать свой шаблон в файле settings.py
.
В файле settings.py
в переменной TEMPLATES
хранятся настройки шаблонизатора.
BACKEND
- указание на язык шаблона, напримерdjango.tempalte.backends.django.DjangoTemplates
говорит что будет использоваться язык шаблонов django template languageDIRS
- список. где джанго ищет шаблоны. Пути должны быть абсолютными?!DIRS = [ "BASEDIR\app_name\templates", ]
APP_DIRS
- искать ли в папках с приложением
Или можно зарегистрировать своё приложение в переменную INSTALLED_APPS
, чтобы шаблоны и искались среди шаблонов указанного приложения.
Чтобы вернуть файл пользователю он отправляется в представлении (вьюхе).
from django.template.loader import render_to_string
def get_info(request):
response = render_to_string('app_name/app_name_index.html')
return HttpResponse(response)
Поиск шаблонов осуществляется в том порядке, в каком подключены приложения в settings.INSTALLED_APPS
. Если шаблон нашелся по пути в каталоге templates
первого по списку подключенного приложения, то он применится. Поэтому следует избегать одинаковых путей к шаблонам для разных приложений. Для этих целей и создают в каталоге templates
отдельный каталог для каждого приложения, чтобы не было коллизий имён шаблонов.
from django.template.loader import render_to_string
from django.http import HttpResponse
def index(request):
response = render_to_string('app_name/index.html')
return HttpResponse(response)
можно заменить с использованием функции render
from django.shortcuts import render
def index(request):
return render(request, 'app_name/index.html')
def render(
request, template_name, context=None, content_type=None, status=None, using=None
):
"""
Return an HttpResponse whose content is filled with the result of calling
django.template.loader.render_to_string() with the passed arguments.
"""
content = loader.render_to_string(template_name, context, request, using=using)
return HttpResponse(content, content_type, status)
Внутри себя render
вызывает всю ту же функцию render_to_string
и затем возвращает HttpResponse
.
render(request, template_name, context: dict, content_type, status, using)
request
- обязательный параметр - запросtemplate_name
- обязательный - имя шаблонаcontext
- словарь с переменными для шаблонаcontent_type
- тип возвращаемого контентаtext/html
, по-умолчаниюstatus
- код ответа HTTP, default = 200using
- строка с именем бд, если используется несколько бд.
В контексте render
передаются параметры, которые встраиваются в шаблон DTL
render(request, template_name, context: dict, content_type, status, using)
Встраивание в шаблон:
{{ variable }}
- переменная{{ dict.key }}
- значение по ключу в словаре{{ var1 }}
{{ var2 }}
- встраивание нескольких переменных
При помощи фильтров можно выполнять вычисления внутри шаблонов. В шаблонах нельзя вызывать методы питона. https://docs.djangoproject.com/en/4.2/ref/templates/builtins/#built-in-filter-reference
capfirst
|{{ var|capfirst }}
| делает первую букву заглавнойupper
|{{ var|upper }}
| в верхний регистрtitle
|{{ var|title }}
| каждая первая буква каждого слова становится заглавнойadd
|{{ value|add:"2" }}
{{ var1|add:var2 }}
| прибавляет значение (2) к переменной. Работает для целых чисел, строк, массивов и т.п. Можно складывать две переменных шаблона.center
|{{ value|center:15 }}
| расширяет строку до n символов (15) и центрирует вывод значенияljust
|{{ value|ljust:"10" }}
| строка длиной 10 символов выравнивание слеваrjust
|{{ value|rjust:"10" }}
| строка длиной 10 символов выравнивание к правому краюcut
|{{ string|cut:symbol }}
| удаляет все символы из строкиtruncatewords
|{{ contents|truncatewords:2 }}
| оставляет первые два слова и затем ставит многоточиеtruncatechars
|{{ content|truncatechars:100 }}
| обрезает контент до 100 символов и ставит многоточиеtruncatechars_html
|{{ content|truncatechars_html:20 }}
обрезать с сохранением закрывающих тэгов htmldate
|{{ value|date:"D d M Y"}}
| преобразование даты к определённому форматуdefault
|{{ value|default:"nothing" }}
| заменяет ложные значения ("" пустую строку) на значения по-умолчаниюdefailt_is_none
|{{ value|default_is_none:default_value}}
| заменяет только в том случае, если значение Nonedictsort
|{{ books|dictsort:"author.age"}}
| возвращает отсортированный словарьdivisibleby
|{{ value|divisibleby:3 }}
| проверяет делится ли число на 3.True|False
first
|{{ value|first }}
| возвращает первый элемент последовательности- |
{{ value.0 }}
| вывести первый элемент последовательности last
|{{ value|last }}
| последний элемент последовательностиlength
|{{ value|length }}
| вернёт длину последовательностиlength_is
|{{ value"length_is:"4" }}
| True|False Будет ли длина value == 4get_digit
|{{ value|get_digit }}
| позиция цифры с правой стороныjoin
|{{ value|join:" - "}}
| джоинит последовательности через разделительmake_list
|{{ value|make_list }}
|"Joel"
->["J", "o", "e", "l"]
random
|{{ value|random }}
| Случайное значение из последовательностиsafe
|{{ value|safe }}
| Позволяет не игнорировать HTML символы в строкеsafeseq
|{{ value|safeseq }}
| применяетsafe
к каждому элементу последовательностиslice
|{{ value|slice:"2" }}
| позволяет делать срезыslugify
|{{ value|slugify }}
| в нижний регистр и все пробелы заменяются на-
Фильтры в Django можно использовать не только в шаблонах, но и в любом другом месте python-кода. Для этого нужно импортировать нужный фильтр из django.template.defaultfilters
и вызвать его, передав нужные аргументы
Пример использования фильтра slugify
вне шаблона:
from django.template.defaultfilters import slugify
title = "This is a title with spaces and punctuation!"
slug = slugify(title)
print(slug)
Все работает как обычные функции в python: вызываете функцию, передаете аргументы, получаете результат.
Все тэги заключаются в {% %}
. Внутри тэга обычные переменные сразу доступны.
Позволяет добавить условный оператор внутрь шаблона.
{% if athlete_list and not coach_list != x or name in "abcdef" or name not is None %}
Number of athletes: {{ athlete_list|length }}
{% elif athlete_in_locker_room %}
Athletes is locker room.
{% else %}
No athletes.
{% endif %}
Удобен для рендеринга повторяющихся элементов в шаблоне.
{% for char in "abcde"|makelist %}
<p>{{ char }}</p>
<p>i = {{ forloop.counter }}</p>
{% endfor %}
Внутри цикла for
доступны некоторые специальные переменные.
forloop.counter
- текущий индекс итерации c 1forloop.counter0
- индекс с 0 (смещение)forloop.revcounter
- номер итерации с концаforloop.revcounter0
- номер итерации с конца с 0forloop.first
,forloop.last
- True, если итерация совпадает с первой/последней.forloop.parentloop
- содержит объект внешнего цикла, если есть (только для вложенных циклов)
Допустима распаковка при обходе
{% for i, j in values %}
Можно обходить словари
{% for key, value in dict.items %}
<p>
<b>{{ key }}</b> -- {{ value }}
</p>
{% endfor %}
Можно объодить значения в обратном порядке:
{% for i in list reversed %}
Данный блок срабатывает когда коллекция пустая
{% for i in list %}
<p>element: {{i}}</p>
{% empty %}
<p>Ooops list is empty!</p>
{% endfor %}
Является аналогом метода reverse
для шаблонов.
{% url "some_url_name" v1 v2 %}
или
{% url "some_url_name" arg1=v1 arg2=v2 %}
Наследование шаблона в том числе позволяет избавиться от дублирования кода.
В базовом шаблоне описываются все неизменяемые вещи - вся структура html.
Обычно базовый шаблон создают не на уровне приложения, а на уровне проекта по пути project/templates/base.html
. Естественно, что можно его создавать и на уровне приложения.
Если шаблон будет создан на уровне проекта, то надо добавить каталог с шаблоном в settings.py
переменную TEMPLATES
.
Для подстановки значений в родительский шаблон существует тэг block
:
# base.html
<title>
{% block title %}
{% endblock %}
</title>
<body>
{% block content %}
{% endblock %}
</body>
Для использования базового шаблона, мы должны от него отнаследоваться в дочернем шаблоне при помощи конструкции тэга extends
и можно задать значения недостающих блоков.
# index.html
{% extends 'base.html' %}
{% block title %}
Меню
{% endblock %}
{% block content %}
<h1>Контент</h1>
{% endblock %}
При переопределении блока может понадобиться включить содержимое блока из родительского шаблона:
{% extends 'base.html' %}
{% block content %}
{{ block.super }}
{% endblock %}
Позволяет включать один шаблон в другой.
По умолчанию создается каталог templates/app_name/includes
, в который помещается шаблон, например следующей структуры:
<nav>
<a href="#id1"></a>
<a href="#id2"></a>
</nav>
Затем данный шаблон может быть включен в другой шаблон
{% block content %}
{% block nav %}
{% include 'app_name/includes/navbar.html' %}
{% endblock %}
{% endblock %}
{% block footer %}
{% include 'app_name/includes/navbar.html' %}
{% endblock %}
Импортируемый шаблон получает доступ ко всем переменным того шаблона, из которого он импортируется.
Чтобы импортируемому шаблону запретить доступ ко всем внутренним переменным добавляется ключевое слово only
:
{% include 'horoscope/includes/navbar.html' only %}
Также можно вручную задавать переменные для передачи их в подключаемый шаблон при помощи конструкции with
:
{% include 'app_name/includes/navbar.html' only with a=100 b='abc' %}
https://docs.djangoproject.com/en/5.0/howto/static-files/
Шаблоны не являются статикой. К статике можно отнести css, js, и не шаблонные html файлы. Статика может быть либо глобальной на весь проект, либо локальной на приложении.
За подключение статики отвечает строчка django.contrib.staticfiles
в списке INSTALLED_APPS
из файла setting.py
.
По соглашению, папка со статичными файлами в проекте app_name/static/
. Далее в этом каталоге, по аналогии с шаблонами, создается каталог с именем приложения, где размещаются статичные файлы для избежания коллизии имён.
Далее принято разделять тип статики
css/
js/
img/
Глобальную статику, относящуюся ко всему проекту целиком принято выносить в каталог с проектом - project_folder/static/
.
Для добавления каталогов для поиска статичных файлов надо вписать переменную STATICFILES_DIRS
в файл settings.py
:
# settings.py
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
В переменной STATIC_URL
содержится путь до статичных файлов для деплоя проекта.
STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
Для включения функциональности, связанной со статичными файлами используется тэг шаблона {% load static %}
<head>
{% load static %}
<link rel="stylesheet" href="{% static 'app_name/css/style.css' %}">
</head>
После этой строчки можно указать какой файл css
можно подгрузить в шаблон.
Поиск статичный файлов происходит в каталогах static
для каждого приложения, поэтому путь отсчитывается от этого пути.
Если подключения различных стилей в каждом шаблоне можно воспользоваться блочной структурой и определить блоки, которые подключаются в заголовке.
Чтобы собрать всю статику локально и вебсерверу было удобнее с ней работать можно воспользоваться командой
python manage.py collectstatic
После этого даже все внешние стили и прочее будут скопированы в каталог STATIC_ROOT
.
https://docs.djangoproject.com/en/3.2/howto/custom-template-tags/
Для создания собственных фильтров и тэгов надо создать питоновский пакет (каталог с __init__.py
).
Название пакета - templatetags
.
Т.е. путь каталога: project_dir/app_name/templatetags/__init__.py
Создаем по этому пути файл фильтра, например my_filter.py
:
# my_filter.py
from django import template
# объект для регистрации фильтров
# для регистрации навешивается как декоратор
register = template.Library()
@register.filter(name="filter_name")
def split(value, key=" ") -> list:
return value.split(key)
Для добавления фильтра шаблон его надо загрузить при помощи тэга {% load %}
указав имя файла без расширения.
{% load my_filter %}
{{ value|split:" - " }}
Для того, чтобы гарантировать, что в качестве водного параметра в нащ фильтр попадет строка надо навесить на функцию ещё один декоратор:
# my_filters.py
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter(name='split')
@stringfilter
def split(value: str, key: str = " ") -> list[str]:
return value.split(key)
Настройки для джанго для подключения к бд хранятся в файле settings.py
в переменной DATABASES
.
Модели для ORM размещаются в файле models.py
.
Классы представляют таблицы в бд, а экземпляры класса являются записями в таблице.
Для создания своей модели данных надо создать класс, отнаследовав его от django.db.models.Model
from django.db import models
class Movie(models.Model):
name = models.CharField(max_length=40)
rating = models.Integer()
class Meta:
verbose_name = "Фильм"
verbose_name_plural = "Фильмы"
Свойство id
создается автоматически.
Далее следует указать поля. Класс CharField
является потомком класса models.Field
, который является базовым для всех полей объекта.
https://docs.djangoproject.com/en/5.0/topics/db/models/#meta-options Класс мета позволяет задать модели метаданные, т.е. это такие данные, которые не не являются полями. Например это может быть сортировка, название таблицы в базе данных, удобочитаемые названия и т.п. Полный список таки атрибутов https://docs.djangoproject.com/en/5.0/ref/models/options/
db_table: str
- имя таблицы в базе данныхordering: list[str] = ["-name", "age"]
- сортировка против имени и по возрастуconstraints
- ограниченияindexes
- индексы моделиverbose_name
- удобочитаемое названиеverbose_name_plural
- удобочитаемое название во множественном лице
https://docs.djangoproject.com/en/5.0/ref/models/fields/
CharField
- для строк фиксированной длины.max_length
- обязательный параметр.TextField
- для хранения строк без ограничения по длинеIntegerField
- целые числаPositiveIntegerField
- положительные целыеFloatField
- вещественные числаDecimalField
- числа с фиксированной точностью. Без ошибок обрабатывает количество знаков после запятой.max_digits
- макс количество цифр в числе (до и после запятой)decimal_places
- количество цифр после запятой
BooleanField
- True|FalseEmailField
- для адресов электронной почты. Это поле типа CharField с некоторыми функциональными возможностями.- макс длина 254 символа
- валидирует данные
URLField
- для хранения и валидации URL адресов. Наследуется от CharField.- max_length = 200
- валидация URL адресов на соответствие шаблону
DateTimeField
auto_now_add: Bool
- дата добавляется автоматически при добавлении записи
При изменении модели может понадобиться переопределить значение по умолчанию или возможность ставить Null значения в таблице. Например при добавлении столбца в таблицу джанго не сможет понять какими значениями его заполнять, если в этом столбце будут запрещены ненулевые значения, а значение по-умолчанию будет отсутствовать. В таком случае можно либо в модели задать default
или null=True
перед миграцией.
class Field(RegisterLookupMixin):
"""Base class for all field types"""
def __init__(
self,
verbose_name=None,
name=None,
primary_key=False,
max_length=None,
unique=False,
blank=False,
null=False,
db_index=False,
rel=None,
default=NOT_PROVIDED,
editable=True,
serialize=True,
unique_for_date=None,
unique_for_month=None,
unique_for_year=None,
choices=None,
help_text="",
db_column=None,
db_tablespace=None,
auto_created=False,
validators=(),
error_messages=None,
):
pass
verbose_name: str
- имя поля, которое будет отображаться в админском интерфейсе джанго и в других контекстах.help_text: str
- подсказка под формой ввода в админском интерфейсеnull: bool
- указывает может ли поле содержать NULL значения. default: Falseblank: bool
- (False) указываем может ли поле быть пустым в формах.default: object
- определяет значения по-умолчанию для модели. Применяется только при создании объекта. При обновлении данных не затрагивает сущность.unique: bool
- (False) являются ли значения в этом столбце уникальными (множеством)- бд создают индекс по уникальным полям, что приводит к уменьшению задержек при выборке значений по уникальным полям
Механизм миграция является аналогом системы версионирования в гите. Миграции отвечают за сохранения состояний в таблицах.
Для создания миграция надо выполнить команду. Предварительно не забыть добавить приложение в список приложений INSTALLED_APPS
, иначе не изменения не будут найдены.F
python manage.py makemigrations
Для применения миграции вводится команда
python3 manage.py migrate
При первой миграции происходит создание таблиц, заданных в уже подключенных приложениях.
У первой миграции не будет зависимостей (поле dependencies
). У остальных будет - это та миграция, которую надо произвести перед данной.. Таким образом выстраивается порядок применения миграций (дерево).
Начальная миграция имеет атрибут initial = True
.
В этом атрибуте лежит список всех изменений, которые надо применить к текущей миграции.
Чтобы просмотреть все миграции можно воспользоваться командой
python3 manage.py showmigrations
При этом миграция может оказаться намного больше, чем мы делали. Показываются все миграции, но применены к БД будут только те, которые мы сделали.
Все примененные миграции будут отмечены x
.
Просмотреть миграции
python3 manage.py showmigrations
Откатиться к конкретной миграции можно при помощи команды migrate и номера миграции (например 003).
python3 manage.py migrate app_name migration_number
Чтобы накатить миграции для какого-то приложения можно выполнить команду migrate
без указания номера:
python3 manage.py migrate app_name
Можно запустить окружение питона в консоли со всеми переменными.
python manage.py shell
Далее в консоли можно будет импортировать нужный класс и создать экземпляр.
from movie_app.models import Movie
movie = Movie(name="FilmName", rating=50)
movie.save()
Для того чтобы изменения применились, надо сохранить этот объект (произвести транзакцию) методом object_name.save()
.
Чтобы вывести список sql запросов надо обратиться к модулю django.db.connection
:
from django.db import connection
print(connection.queries)
Добавить данные можно еще так:
Movie.objects.create(name="Avatar 2", rating=30)
метод save
при этом не вызывается.
Также можно воспользоваться модулем django-extensions
который позволяет сразу выводить sql запросы после выполнения транзакций.
Устанавливаем.
pip3 install django-extensions
Добавляем к списку приложений.
INSTALLED_APPS = [
...,
'django_extensions',
]
Запускаем консоль с параметрами --print-sql
python manage.py shell_plus --print-sql
https://docs.djangoproject.com/en/3.2/ref/models/querysets/
В классе таблиц (отнаследованных от django.models.Model) есть специальный атрибут objects
, который является экземпляром Manager
. У класса Manager
есть метод .all()
который выводит все элементы - коллекция типа QuerySet.
rows = Movie.objects.all()
Данный метод поддерживает операцию индексации и срезы. При этом измениться сам sql запрос к базе данных (LIMIT OFFSET). После получения конкретной записи можно обращаться к её атрибутам через точечную нотацию.
movie = Movie.objects.all()
print(movie.rating)
Model.objects.all()
- возвращает объектQuerySet
Model.objects.values()
- возвращает список словарей с полями моделейModel.objects.list_values()
- возвращает список списков со значениями всх полей
persons = Person.objects.all()
Это тот же самый запрос, только с использованием Django ORM. Это QuerySet
. Отличие от выборки в SQL заключается в том, что QuerySet
может быть создан, отфильтрован, нарезан и, как правило, передан без фактического запроса к базе данных. Так же QuerySet
это итерируемый объект и мы можем проитерировать каждую запись, если мы хотим обработать каждую запись в отдельности:
for person in persons:
print(person.name)
print(person.gender)
print(person.age)
SELECT name, age FROM Person;
Данный запрос вернет все записи из таблицы Person но в результате будут только столбцы name и age.
Person.objects.only('name', 'age')
SELECT DISTINCT name, age FROM Person;
Person.objects.values('name', 'age').distinct()
SELECT * FROM Person LIMIT 10;
Вернет 10 первых записей из таблицы.
Person.objects.all()[:10]
SELECT * FROM Person OFFSET 5 LIMIT 5;
Вернет первые 5 записей, но прежде он пропустит 5 первых записей. Т.е. по сути, он пропускает первые 5 записей и берет 5 записей после них начиная с 6й.
Person.objects.all()[5:10]
SELECT * FROM Person WHERE id = 1;
Вернет одну запись у которой id = 1.
Person.objects.filter(id=1)
Больше, меньше, больше или равно, меньше или равно, неравно
WHERE age > 18;
WHERE age >= 18;
WHERE age < 18;
WHERE age <= 18;
WHERE age != 18;
Person.objects.filter(age__gt=18)
Person.objects.filter(age__gte=18)
Person.objects.filter(age__lt=18)
Person.objects.filter(age__lte=18)
Person.objects.exclude(age=18)
SELECT * FROM Person WHERE age BETWEEN 10 AND 20;
Person.objects.filter(age__range=(10, 20))
WHERE name like '%A%';
WHERE name like binary '%A%';
WHERE name like 'A%';
WHERE name like binary 'A%';
WHERE name like '%A';
WHERE name like binary '%A';
Person.objects.filter(name__icontains='A')
Person.objects.filter(name__contains='A')
Person.objects.filter(name__istartswith='A')
Person.objects.filter(name__startswith='A')
Person.objects.filter(name__iendswith='A')
Person.objects.filter(name__endswith='A')
SQL
WHERE id in (1, 2);
Оператор IN позволяет определить, совпадает ли значение объекта со значением в списке. Используется с WHERE.
Django
Person.objects.filter(id__in=[1, 2])
SQL
WHERE gender='male' AND age > 25;
Логическое И вернет записи где gender = male
И возраст больше 25
Django
Person.objects.filter(gender='male', age__gt=25)
SQL
WHERE gender='male' OR age > 25;
Логическое ИЛИ вернет записи где gender male ИЛИ возраст больше 25.
Django
from django.db.models import Q
Person.objects.filter(Q(gender='male') | Q(age__gt=25))
Немного громоздко, но у всего есть свои плюсы и минусы.
Для русского языка в SQLite3 работает поиск через регулярки - iregex:
MyModel.objects.filter(Q(h1__iregex=query) | Q(content__iregex=query))
SQL
WHERE NOT gender='male';
Логическое НЕ. Вернет все записи где genre НЕ male.
Person.objects.exclude(gender='male')
SQL
WHERE age is NULL;
WHERE age is NOT NULL;
Вернет где age равно null (пустое) или наоборот, где не пустое.
Django
Person.objects.filter(age__isnull=True)
Person.objects.filter(age__isnull=False)
Person.objects.filter(age=None)
Person.objects.exclude(age=None)
SQL
SELECT * FROM Person order by age;
Вернет все записи отсортированные по age
Django
Person.objects.order_by('age')
SQL
SELECT * FROM Person ORDER BY age DESC;
Сделает тоже самое, но отсортирует по убыванию.
Django
Person.objects.order_by('-age')
SQL
INSERT INTO Person VALUES ('Jack', '23', 'male');
Создаст новую запись в таблице Person с указанными данными.
Django
Person.objects.create(name='jack', age=23, gender='male')
SQL
UPDATE Person SET age = 20 WHERE id = 1;
Обновляет запись в таблице Person
устанавливает age
в значение 1 где id = 1
. По сути обновится одна запись у которой id = 1
так как id
у каждой записи уникальный.
Django
person = Person.objects.get(id=1)
person.age = 20
person.save()
SQL
UPDATE Person SET age = age * 1.5;
Обновляет все записи в таблице Person умножая значение записанное в колонке age на 1.5
Django
from django.db.models import F
Person.objects.update(age=F('age')*1.5)
SQL
DELETE FROM Person;
Удаляет все записи в таблице Person
Django
Person.objects.all().delete()
Удаление определенных строки
SQL
DELETE FROM Person WHERE age < 10;
Удаляет строки из таблицы Person где age меньше 10.
Django
Person.objects.filter(age__lt=10).delete()
SQL
SELECT MIN(age) FROM Person;
Вернет минимальное значение столбца age
Django
from django.db.models import Min
Person.objects.all().aggregate(Min('age'))
{'age__min': 0}
SQL
SELECT MAX(age) FROM Person;
Вернет максимальное значение столбца age
Django
>>> from django.db.models import Max
>>> Person.objects.all().aggregate(Max('age'))
{'age__max': 100}
SQL
SELECT AVG(age) FROM Person;
Вернет среднее значение age.
Django
>>> from django.db.models import Avg
>>> Person.objects.all().aggregate(Avg('age'))
{'age__avg': 50}
SQL
SELECT SUM(age) FROM Person;
Вернет сумму всех значений столбца age.
Django
>>> from django.db.models import Sum
>>> Person.objects.all().aggregate(Sum('age'))
{'age__sum': 5050}
SQL
SELECT COUNT(*) FROM Person;
Посчитает все записи таблицы Person.
Django
Person.objects.count()
Для тоо, чтобы изменить значение в поле конкретной записи, надо е получить, присвоить полю новое значение и затем сохранить.
movie = Movies.objects.all()[2]
movie.name = "XXXX 2"
movie.save()
Таким образом можно изменять атрибуты только у одного объекта.
Удалять записи можно выбрать объект и применить метод .delete()
.
Movie.objects.all()[4].delete()
Для получения записей по фильтре у класса Manager
(objects
) есть метод .get()
.
Выбираем запись по её id
.
movie = Movie.objects.get(id=5)
movie2 = Movie.objects.get(rating=2)
При использовании get следует быть осторожным с ем, что если записи не найдется то вызовется исключение с типом DoesNotExist
.
В случае, если найдено более 1 объекта возникнет исключение MultipleObjectsReturned
.
Таки образом, get
применяется в случае выборки из уникальных свойств, и должен возвращать ровно один объект.
Для выбора нескольких объектов или не одного применяется метод .filter()
.
Для использования более сложных запросов на сравнение не на равенство используются специальные псевдонимы полей.
movies = Movie.object.filter(budget__gt=10)
__gt
->
_gte
->=
__lt
-<
__lte
-<=
__isnull
- is Null True|false__contains='abc'
-abc in fieldname
__icontains='Abc'
-abc in fieldname
- без чувствительности к регистру.__startswith='abc'
- начинается наabc
__endswith='abc'
- заканчивается наabc
__in=[1, 2, 3]
- проверка на вхождение во множествоTableClass.objects.exclude(field=value)
-!=
Метод exclude
работает прямо противоположено методу filter
- отсеивает все значения, подходящие под фильтр.
В фильтре можно производить фильтрацию по нескольким полям:
movies = Movie.objects.filter(name__isnull=False, budget__gt=10000)
или можно зачеинить операции
movies = Movie.exclude(name=None).filter(budget__gt=10000)
Для поиска строк содержащих часть другой строки стоит использовать фильтр __contains
или __icontains
.
Для поиска по вхождениям во множество:
movies = Movie.object.filter(id__in=[1, 2, 3])
Для использования более сложных условий понадобится класс Q:
from django.db.models import Q
Классом Q
можно пользоваться так же, как и обычным фильтром. Оборачивая в него условия. Все условия также объединяются логическим И.
movies = Movie.objects.filter(rating__gt=80)
movie = Movie.objects.filter(Q(rating__gt=80))
Но можно и явно указать союзы
movies = Movie.objects.all(Q(rating__gt=80) | Q(budget__lt=100000))
Таким образом можно составлять более сложные запросы. Отрицание для Q выглядит боле удобно ~
.
movies = Movie.objects.filter(
Q(filter1) | Q(filter2) & ~Q(filter3)
)
Для получения союза AND
можно использовать как знак ,
, так и &
.
Movie.objects.filter(Q(), Q() and Q())
Допускается в аргументах метода filter использовать как Q, так и обычный синтаксис, разве что в этом случае Q должен следовать первым.
movies = Movie.objects.all()
movies = movies.values
<table>
<tr>
{% for key, value in movies.0.items %}
<th>
{{ key }}
</th>
{% endfor %}
</tr>
{% for movie in movies %}
<tr>
{% for key, value in movie.items %}
<td>
{{ value }}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
Джанго имеет встроенные методы для получения значений, которые возвращают ответ 404 в случае, если данных не было найдено или возникло исключение при получении данных методом get
.
from django.shortcuts import get_object_or_404
from django.shortcuts import get_list_or_404
movie = get_object_or_404(klass=Movie, id=20)
movies = get_list_or_404(klass=Movie, name__isnull=False)
У модели (класса) можно создать собственный метод, который можно будет использовать в шаблоне. Например нам надо динамически генерировать урл адреса фильмов:
class Movie(django.db.models.Model)
def get_url(self):
return reverse(viewname="movie_app:movie_info", args=[self.id])
@property
def url(self):
return self.get_url()
В шаблоне к созданным методам надо обращаться без вызова
<a href="{{ movie.url }}"><p>{{movie.name}}</p></a>
Это уникальная строка-идентификатор, которая понятна человеку Для этого в модель добавляется дополнительное поле, для которого уже есть встроенный тип данных
from django.db import models
class Movie(models.Model):
...
slug = models.SlugField(
default='',
null=False,
)
max_length: int
min_length: int
default
- значение по-умолчаниюnull: boolean
- является ли пустым по умолчанию
Для того чтобы slug изменялся каждый раз при изменении полей модели, можно переопределить метод save
.
from django.utils.text import slugify
class Movie(models.Model):
...
def save(self, *args, **kwargs):
self.slug = slugify(value=self.name, allow_unicode=True) + \
"-" + "".join(random.choices(string.ascii_letters, k=5))
super(Model, self).save(*args, **kwargs)
Для более удобной работы с русскими именами можно использовать функцию slugify
из модуля pytils.translit
:
pip3 install pytils
from pytils.translit import slugify
Теперь можно сделать отдельную вьюху и роут для получения информации о фильме не по ид, а по слаг полю.
Можно передать как одно поле, так и несколько. Знак -
перед название м поля говорит об обратной порядке сортировки.
movies = Movie.object.order_by('-year', 'rating', 'name')
Для того чтобы влиять на порядок объектов с Null элементами (начало - конец) надо импортировать класс F
из django.db.models
.
Для выбора порядка сортировки используется методы asc
и desc
:
from django.db.models import F
movies = Movie.object.order_by(F('rating').desc(nulls_last=True)) # сортировка c null в конце
movies = Movie.object.order_by(F('rating').desc(nulls_first=True)) # сортировка c null в начале
# сортировка с F и без
movies = Movie.objects.sort_by(
F('rating').desc(),
'-year'
)
https://djangodoc.ru/3.1/topics/db/aggregation/ https://webdevblog.ru/razberaemsya-s-group-by-v-django-s-sql/
from django.db.models import Sum
from django.db.models import Min
from django.db.models import Max
from django.db.models import Count
from django.db.models import Mean
mean_rating = Movie.objects.all().aggregate(Sum('budget'))
# {'budget__sum': 256000000}
movies = Movie.objects.all()
min_rating = movies.aggregate(Avg('rating'))
# {'rating__min': 50}
В результате операции получим словарь с именем переменной в конце будет фильтр и значение как ключ.
Можно сделать множественные агрегации:
Movie.objects.aggregate(Sum('budget'), Avg('rating'), Max('year'))
# {'budget__sum': 256000000, 'rating__avg': 60.3}
https://django-debug-toolbar.readthedocs.io/en/latest/installation.html
Для создание дополнительных полей, которые не хранятся в базе данных есть операция annotate
.
from django.db.models import Value
Movie.objects.annotate(is_year=Value('year'))
В этом примере мы создали одно дополнительное поле со значением везде year
. Чтобы добавить значение надо обернуть его в объект класса Value
.
Можно создавать несколько полей
Movie.objects.annotate(is_year=Value('year'), is_active=Value(True))
Для создания значения но=а основе существующих колонок, нужно использовать объект F, который позволяет обращаться к колонкам внутри джанго запроса.
from django.db.models import F
Movie.objects.annotate(new_budget=F('budget') * 0.9)
Movie.objects.annotate(abc=F('budget') * F('year'))
Важно помнить, что в случае, если в операции используется NULL, то результатом будет NULL.
Для группировки есть функция values
. Например если надо сгруппировать по рейтингу, а потом посчитать средний рейтинг в каждой позиции:
Movie.objects.values('rating').annotate(avg=Avg('budget'))
# <QuerySet [{'rating': 2, 'avg': 0.0}, {'rating': 20, 'avg': 0.0}, {'rating': 30, 'avg': 0.0}, {'rating': 50, 'avg': 0.0}, {'rating': 89, 'avg': 74500000.0}, {'rating': 90, 'avg': 25000000.0}, {'rating': 91, 'avg': 22000000.0}, {'rating': 92, 'avg': 60000000.0}]>
Что нужно проверять при тестирование моделей
- создание и сохранение объектов моделей.
- получение и изменение данных
- валидация данных
- методы и свойства модели - проверяем, что они возвращают корректные значения.
Тесты будем писать в каталоге нашего приложения, т.е. в файле project/app_name/tests.py
:
from django.test import TestCase
class MovieModelTestCase(TestCase):
pass
Джанго предоставляется специальный тестовый инструментарий в рамках которого создается тестовая база данных. Тестовая база данных будет создана отдельно для каждого теста согласно настройкам проекта и к ней будут применены все миграции. После выполнения теста все тестовая база данных удаляется.
Метод setUp
используется в джанго для настройки тестовой среды перед выполнением каждого тестового метода.
Выполняется автоматически перед каждым запуска теста класса TestCase
.
Например перед каждым тестом мы будем создавать тестовые записи в базе данных.
from django.test import TestCase
from .models import Movie
class MovieModelTestCase(TestCase)
def setUp(self):
print('-' * 10)
self.print_info('Start setUp')
self.movie = Movie.objects.create(name="Test Movie", year=1998, rating=80)
Movie.objects.create(name="Test Matrix", year=2021, rating=90)
Movie.objects.create(name="Test Mask", year=2023, rating=80)
self.print_info("Finish setup")
@staticmethod
def print_info(message):
count = Movie.objects.count()
print(f"{message}: #all_movies={count}")
Тесты пишутся как и раньше. Тесты начинаются со слова test
и должны содержать проверки внутри себя.
class MovieModelTestCase(models.TestCase):
...
def test_movie_creation(self):
# Проверка создания объекта Movie
self.print_info('Start test_movie_creation')
self.assertEqual(self.movie.name, 'Test Movie')
self.assertEqual(self.movie.rating, 80)
self.assertEqual(self.movie.year, 2022)
self.assertEqual(self.movie.budget, 1000000)
self.assertRegex(self.movie.slug, r'test-movie.{4,}')
self.print_info('Finish test_movie_creation')
def test_movie_get_all_records(self):
# Проверка получения всех записей из бд
self.print_info('Start test_movie_get_all_records')
movies = Movie.objects.all()
self.assertEqual(len(movies), 3)
self.print_info('Finish test_movie_get_all_records')
def test_movie_get_record(self):
# Проверка получения записи из бд
self.print_info('Start test_movie_get_record')
mask = Movie.objects.get(name='Test Mask')
self.assertEqual(mask.year, 1995)
self.print_info('Finish test_movie_get_record')
def test_movie_get_url(self):
# Проверка метода get_url()
self.print_info('Start test_movie_get_url')
url = self.movie.get_url()
expected_url = reverse('movie_app:movie_info_slug', args=['test-movie'])
self.assertRegex(url, expected_url[:-1])
self.print_info('Finish test_movie_get_url')
def test_movie_str(self):
# Проверка метода __str__()
self.print_info('Start test_movie_str')
expected_str = 'Test Movie - 80%'
self.assertEqual(str(self.movie), expected_str)
self.print_info('Finish test_movie_str')
def test_movie_save_slug(self):
# Проверка сохранения корректного slug при сохранении объекта
self.print_info('Start test_movie_creation')
self.assertRegex(self.movie.slug, r'^test-movie.{5}')
self.print_info('Finish test_movie_save_slug')
def test_movie_budget_default_value(self):
# Проверка значения по умолчанию для budget
self.print_info('Start test_movie_budget_default_value')
movie = Movie.objects.create(name='Default Budget Movie', rating=75)
self.assertEqual(movie.budget, 0)
self.print_info('Finish test_movie_budget_default_value')
Синглетон модель - класс, который может иметь только один инстанс. Бывает удобно, если надо создать настройки сайта в административной панели. Тогда для каждой настройки будет использоваться своя модель. Например модель для настроек главной страницы, отдельная модель для настроек хэдера и т.п. Каждая такая модель настроек наследуется от синглетон модели, что гарантирует, что будет только один объект в этом классе.
#models
class Singleton(models.Model):
class Meta:
abstract = True
def save(self, *args, **kwargs):
self.pk = 1
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
pass
@classmethod
def load(cls):
obj, _ = cls.objects.get_or_create(pk=1)
return obj
class SiteSettings(Singleton):
title = models.CharField(max_length=100)
email = models.EmailField()
phone = models.CharField(max_length=20)
address = models.CharField(max_length=200)
#...
В примере выше классу синглетону запрещается иметь более одного объекта, через явное указание первичного ключа при сохранении объекта.
Для получения объекта при передаче во вьюху используется метод load()
.
Могут произойти проблемы при миграции, например если потребуется удалить объект этого класса, а метод удаления не будет работать.
Доступна по адресу /admin/
через установленное приложение django.contrib.admin
.
Для авторизации используется таблица auth_users
, которая становится доступной после выполнения начальной миграции.
Также есть auth_user_groups
, auth_group_permissions
, auth_user_user_permissions
.
Создание суперпользователя для админ панели производится командой
python manage.py createsuperuser
Сразу в админке доступна панель с моделями пользователя и групп пользователей.
Можно добавить и другие панели работы с моделями.
Для этого в файле project_dir/app_dir/admin.py
надо зарегистрировать модель:
from django.contrib import admin
from .models import Movie
admin.site.register(Movie)
python manage.py changepassword <username>
# setting.py
LANGUAGE_CODE = "ru-ru"
# admin.py
# new
admin.AdminSite.site_header = "Панель администратора"
# old
admin.site.site_header = "Панель администратора"
admin.site.site_title = "Название сайта"
admin.site.index_title = "Админка"
Чтобы просто добавить модель в админку
#admin.py
from .models import Director
admin.site.register(Director)
Для добавление дополнительных колонок для таблицы объектов модели нужно создать специальный класс, который дас нам возможность менять настройки и привязать его к нашей модели.
# admin.py
from django.contrib import admin
class MovieAdmin(admin.ModelAdmin):
list_display = [
'name',
'rating'
'year',
'budget',
]
admin.site.register(Movie, MovieAdmin)
Также привязку можно осуществить не через метод, а через декоратор admin.register
:
from django.contrib import admin
from . import models
@admin.register(models.Movie)
class MovieAdmin(admin.ModelAdmin):
pass
list_display: list[str]
- список колонок свойств моделиlist_editable: list[str]
- поля, которые можно сразу редактировать в таблице. Нельзя включать первое поле изlist_fields
, т.к. оно является кликабельной ссылкой на объект.list_filter: list[str]
- список колонок для фильтрацииsearch_fields: list[str]
- список колонок в которых ведётся поискordering: list
- колонки, по которым выполняется сортировка при открытии страницы.list_per_page: int
- число записей на одной страницеlist_max_show_all: int
- максимальное число записейfilter_horizontal: list[str]
- список полей для которых отображается горизонтальный фильтр (для полей многие-ео-многим)list_display_links
= ['first_name'] -list_select_related
= ['age'] - таблицы для связей (внешний ключ)list_exclude
= ['first_name'] -list_display_links_details
= True -list_display_links_details_count
= 5 -list_filter_horizontal
= ['age'] -list_filter_vertical
= ['age'] -list_filter_inline
= ['age'] -list_filter_vertical_count
= 3 -list_filter_inline_count
= 3 -list_filter_links_details
= True -list_filter_links_details_count
= 5 -list_filter_empty_choice_label
= "Нет фильтров" -list_filter_empty_choice_label_details
= True -
Метод annotate
для QuerySet
позволяет создавать вычисляемые поля для запросов.
Также можно добавить дополнительные поля для админки.
Для этого внутри класса администрирования модели добавляется метод, с названием, связанным с названием новой колонки.
class MovieAdmin(admin.ModelAdmin):
...
list_display = [..., rating_status]
@admin.display(ordering="rating", description="статус")
def rating_status(self, row: Movie):
if row.rating < 50:
return "Не очень"
elif row.rating < 70:
return "Смотрибельно"
else:
return "Хороший фильм"
Чтобы задать порядок сортировки только что созданному свойству можно навесить декоратор admin.display(ordering="column_name")
Переопределить название (которое генерируется из названия метода) можно при помощи декоратора@admin.display(description="column name")
Позволяет задавать дискретные значения для поля. Часто выбирается например для значения пола, сезона года - таких значений, когда они не будут изменяться на протяжении работы программы. Плюс в том что строка малой длины и пользователю предоставляется ограниченный выбор. Передается через список кортежей из двух значений. Первое значение а кортеже заносится в базу данных, а второе является отображаемым в административной панели для удобочитаемости.
class Billing(models.Model):
CARD = "CARD"
CASH = "CASH"
TYPES = {
CARD: "оплата по карте",
CASH: "оплата наличными",
}
payment_type = models.CharField(max_length=5, choices=TYPES.items())
class Movie(models.Model):
EURO = "eur"
DOLLARS = "usd"
ROUBLES = "rub"
CURRENCY_TYPES = {
EURO: "Euro",
DOLLARS: "US dollar",
ROUBLES: "Russian rouble",
}
currency = models.CharField(
max_length=5,
choices=CURRENCY_TYPES.items(),
default="ru"
)
Особенность choices
в том, что оно не создаёт ограничения на уровне базы данных, а только на уровне приложения. Поэтому после изменения модели, значения в базе данных могут изменяться от тех, что есть в choices.
Надо создать метод который будет принимать два параметра - request
и QuerySet
. Задекорироать его @admin.actions
и добавить в список действий actions
from django.contrib import admin
from django.db.models import QuerySet
from .models import Movie
class MovieAdmin(admin.ModelAdmin):
...
actions = ['set_dollars']
@admin.action(description="Валюта в долларах")
def set_dollars(self, request, query_set: QuerySet):
count = querySet.update(currency=Movie.DOLLARS)
self.message_user(request, f"Изменено {count} фильмов")
Полученное значение измененных записей можно использовать для отправки сообщений пользователю через метод self.message_user(request, message: str)
.
level = django.contrib.messages.INFO
- можно менять цвет сообщений- INFO
- SUCCESS
- WARNING
extra_tags:str = ''
fail_silently: bool = False
from django.contrib import messages
...
self.message_user(
self,
message="Сообщению пользователю",
level=messages.SUCCESS
)
Для того, чтобы появилась поисковая строка надо заполнить поле search_fields: list[str]
.
Для того чтобы искать не в любом месте поля, добавляются дополнительные параметры к названию строки
__startswith
- начинается__endswith
- заканчивается__istartswith
- без учета регистра символов__iendswith
- без учета регистра символов__regex
- поиск по регулярному вырадению__iregex
- без учета регистра символов В sqlite поиск ведется неригистрозависимый.
С поиском по регистру следует быть осторожным - он может привести к ошибке 500 (например если искать по *).
from django.db.models import ModelAdmin
class MovieAdmin(ModelAdmin):
...
search_fields = [
'name',
"last_name__startswith",
"middle_name__endswith"
]
https://docs.djangoproject.com/en/4.2/ref/contrib/admin/filters/#modeladmin-list-filters
Для фильтрации добавляется новый атрибут - list_filter: list[str]
, со списком полей, по которым мы хотим фильтроваться.
from django.db.models import ModelAdmin
class MovieAdmin(ModelAdmin):
...
list_filter = ['name', 'currency']
Но для создание собственного фильтра понадобится создать отдельный класс
# admin.py
class MovieRatingFilter(admin.SimpleListFilter):
title = "Фильтр по рейтингу"
parameter_name = "rating"
def lookups(self, request, model_admin):
return (
("40", "низкий"),
("40_60", "средний"),
("60_80", "высокий"),
("80_100", "экселенц"),
)
def queryset(self, request, query_set: QuerySet):
value = self.value() # request.GET.get('rating')
match value:
case "_40":
return queryset.filter(rating__lt=40)
case "40_60":
return queryset.filter(rating__gte=40, rating__lt=60)
case "60_80":
return queryset.filter(rating__lt=60, rating__gte=80)
case "80_100":
return queryset.filter(rating__gte=80)
return queryset.all() # если нет значения фильтра, то возвращаем все записи
@admin.register(models.Movie)
class MovieAdmin(admin.ModelAdmin):
...
list_filter = ['name', 'year', MovieRatingFilter]
paramater_name
- задаёт имя query параметра, который передается в бэкэнд со значениями из функцииlookups
. lookups содержит список туплов, где- 1й элемент - возможное значение query параметра
paramter_name
- 2й элемент - удобочитаемое название значения в админке
По умолчанию в форме отдельного элемента мы видим все поля, которые есть у модели. Но на это поведение можно поменять.
Для отображения только определённый полей нужно в классе моделиАдмин заполнить параметр fields: list[str]
(сделать его не пустым). Чтобы поставить несколько полей в одну строчку, достаточно поместить их внутри кортежа.
class MovieAdmin(admin.ModelAdmin):
...
fields = ['title', ('rating', 'budget'), 'year']
readonly_fields = ['budget']
exclude = ['slug']
Поля переданные в списке для исключения exсlude
не будут отображаться. Следует избегать добавления обязательных для заполнения полей, т.к. эти поля также исключаются из формы для добавления нового элемента, что приводит к 500 ошибке.
Можно добавить поля только для чтения, значения которых нельзя не изменять ни задавать при создании новых элементов.
Обязательноcть ввода полей задается в модели через параметр поля blank=False|True
.
Можно задать имя поля через verbose_name
в атрибуте параметра модели и подсказку к полю через help_text
.
https://docs.djangoproject.com/en/4.0/ref/validators/#maxvaluevalidator Часть валидаций джанго делает за нас. Но часть валидаций, которых нет в модели надо делать самостоятельно.
# models.py
from django.db import models
from django.core.validators import MaxValueValidator
from django.core.validators import MinValueValidator
class Movie(models.Model):
rating = models.PositiveIntegerField(
validators=[MinValueValidator(0), MaxValueValidator(100)]
)
Некоторые поля, например slug
могут вычисляться и являться обязательными для базы данных. Но пользователь не должен их задавать. Если поле вычисляется в момент сохранения (переопределён в модели метод save()
), то поле можно скрыть из формы отдельного элемента и при создании такого элемента его заполненность будет игнорироваться, хотя он будет добавляться корректно в базу данных.
Или в модель данных можно добавить параметр prepopulated_fields
# admin.py
@admin.register(Movie)
class MovieAdmin(models.ModelAdmin):
...
prepopulated_fields = {
'slug': ('name',),
}
Благодаря prepopulated_fields
в форме в поле для slug
будут автоматически подставляться значения.
Зачем вообще нужно создавать несколько таблиц в БД? Почему бы нам не хранить все данные в одной большой таблице? Не посещал ли вас такой вопрос? Давайте разбираться
Если мы будем все данные хранить в одной большой таблице, это может привести к дублированию данных. А такое повторение данных может привести к:
- созданию очень больших отношений;
- поддерживать и обновлять данные станет непросто, так как это потребует поиска множества связанных записей;
- потери и плохое использование дискового пространства и ресурсов;
- увеличивается вероятность ошибок и несоответствий.
Таким образом, чтобы справиться с этими проблемами, мы должны проанализировать и разложить отношения с избыточными данными на более мелкие, простые и хорошо структурированные отношения, которые удовлетворяют желаемым свойствам. Нормализация — это процесс разложения отношений на отношения с меньшим количеством атрибутов.
- Нормализация — это процесс организации данных в базе данных.
- Нормализация используется для минимизации избыточности отношения или набора отношений. Она также используется для устранения нежелательных характеристик, таких как аномалии вставки, обновления и удаления.
- Нормализация делит большую таблицу на меньшие и связывает их с помощью отношений.
- Нормальная форма используется для уменьшения избыточности таблицы базы данных. Зачем нужна нормализация?
Основной причиной нормализации отношений является устранение этих аномалий. Если не устранить аномалии, это приведет к избыточности данных и может привести к нарушению целостности данных и другим проблемам по мере роста базы данных. Нормализация состоит из ряда рекомендаций, которые помогут вам создать хорошую структуру базы данных.
Аномалией называется такая ситуация в таблице БД, которая приводит к противоречию в БД либо существенно усложняет обработку БД. Причиной является излишнее дублирование данных в таблице, которое вызывается наличием функциональных зависимостей от не ключевых атрибутов. Аномалии модификации данных можно разделить на три типа:
- Аномалии-модификации проявляются в том, что изменение одних данных может повлечь просмотр всей таблицы и соответствующее изменение некоторых записей таблицы.
- Аномалии-удаления — при удалении какого либо кортежа из таблицы может пропасть информация, которая не связана напрямую с удаляемой записью.
- Аномалии-добавления возникают, когда информацию в таблицу нельзя поместить, пока она не полная, либо вставка записи требует дополнительного просмотра таблицы.
Чтобы привести БД к нормальной форме, необходимо:
- Объединить имеющиеся данные в группы.
- Выяснить логические связи между группами. Чтобы обеспечить правильность связей, связываемые поля должны иметь один тип. Если таблица не нормализована, она может хранить информацию о нескольких сущностях и включать в себя повторяющиеся столбцы, а они, в свою очередь, могут хранить дублируемые значения. Если же нормализована, то каждая таблица хранит информацию лишь об одной сущности.
При нормализации предполагается использование нормальных форм по отношению к структуре имеющихся данных. Есть несколько правил нормализации. Каждое из них носит название «нормальная форма» (НФ). Каждая такая форма, кроме первой, предполагает, что к данным уже применили предыдущую нормальную форму. При выполнении первого правила БД представлено в первой нормальной форме (1НФ), при выполнении трех правил — в третьей нормальной форме (3НФ).
Таких форм (уровней) — семь, однако на практике для большей части приложений вполне достаточно нормализовать БД до третьей нормальной формы (строго говоря, БД и будет считаться нормализованной, когда к ней применяется 3НФ и выше).
Да, обеспечить полное соответствие правилам и спецификациям — задача не всегда выполнимая, ведь для нормализации придется создавать дополнительные таблицы, а это не всегда приемлемо или не находит отклика у клиентов. Но если правила приходится нарушать, надо понимать, что все, связанные с этим проблемы, включая несогласованные зависимости и избыточность, будут учтены, и что это допустимо для приложения, не нарушит его работоспособность.
Мы с вами разберем три первых НФ
Нормализация проходит через ряд этапов, называемых нормальными формами. Нормальные формы применяются к индивидуальным отношениям. Говорят, что отношение находится в определенной нормальной форме, если оно удовлетворяет ограничениям. Нормальная форма — требование, предъявляемое к структуре таблиц в теории реляционных баз данных для устранения из базы избыточных функциональных зависимостей между атрибутами (полями таблиц).
Требование первой нормальной формы (1NF) очень простое и оно заключается в том, чтобы таблицы соответствовали реляционной модели данных и соблюдали следующие реляционные принципы:
- В таблице не должно быть дублирующих строк
- В каждой ячейке таблицы хранится атомарное значение (одно не составное значение)
- В столбце хранятся данные одного типа Пример приведения таблицы к первой нормальной форме Следующая таблица не находится даже в первой нормальной форме, так как в некоторых ячейках хранятся списки значений (каждый номер телефона — это одно значение) и есть повторяющиеся строки.
EMP_ID | EMP_NAME | EMP_PHONE | EMP_STATE |
---|---|---|---|
14 | John | 7272826385, 9064738238 | UP |
20 | Harry | 8574783832 | Bihar |
12 | Sam | 7390372389, 8589830302 | Punjab |
20 | Harry | 8574783832 | Bihar |
Чтобы привести эту таблицу к первой нормальной форме, необходимо в ячейках хранить один номер телефона, а не список, и убрать одинаковые строки.
EMP_ID | EMP_NAME | EMP_PHONE | EMP_STATE |
---|---|---|---|
14 | John | 7272826385 | UP |
14 | John | 9064738238 | UP |
20 | Harry | 8574783832 | Bihar |
12 | Sam | 7390372389 | Punjab |
12 | Sam | 8589830302 | Punjab |
Чтобы база данных находилась во второй нормальной форме (2NF), необходимо чтобы ее таблицы удовлетворяли следующим требованиям:
- Таблица должна находиться в первой нормальной форме
- Таблица должна иметь первичный ключ
- Все неключевые столбцы таблицы должны зависеть от первичного ключа (в случае если он составной)
Первичный ключ – это столбец или набор столбцов, по которым гарантировано можно отличить строки друг от друга, т.е. ключ идентифицирует каждую строку таблицы. По ключу мы можем обратиться к конкретной строке данных в таблице..
Если ключ составной, т.е. состоит из нескольких столбцов, то все остальные неключевые столбцы должны зависеть от всего ключа, т.е. от всех столбцов в этом ключе. Если какой-то атрибут (столбец) зависит только от одного столбца в ключе, значит, база данных не находится во второй нормальной форме.
Иными словами, в таблице не должно быть данных, которые можно получить, зная только половину ключа, т.е. только один столбец из составного ключа.
Главное правило второй нормальной формы (2NF) звучит следующим образом:
Таблица должна иметь правильный ключ, по которому можно идентифицировать каждую строку
Пример приведения таблицы ко второй нормальной форме У нас имеется таблица сотрудников в 1NF.
ФИО | Должность | Подразделение | Описание подразделения |
---|---|---|---|
Булыкин И.И. | Программист | Отдел разработки | Разработка и сопровождение приложений и сайтов |
Григорьев С.С. | Бухгалтер | Бухгалтерия | Ведение бухгалтерского и налогового учета финансово-хозяйственной деятельности |
Анджелина Джоли | Продавец | Отдел реализации | Организация сбыта продукции |
Григорьев С.С. | Программист | Бухгалтерия | Ведение бухгалтерского и налогового учета финансово-хозяйственной деятельности |
Мы видим, что она удовлетворяет условиям первой нормальной формы, т.е. в ней нет дублирующих строк и все значения атомарны.
Но мы не можем однозначно идентифицировать одну строку от другой. У нас есть сотрудники с одинаковыми именами, но при этом имеющие разные должности.
Для решения этой проблемы мы можем присвоить каждому сотруднику уникальный табельный номер, который никогда не будет изменен. Табельный номер, в нашем случае и будет являться первичным ключом, зная который мы можем четко идентифицировать каждого сотрудника, т.е. каждую строку нашей таблицы.
Таким образом, чтобы привести эту таблицу ко второй нормальной форме, мы должны добавить в нее еще один атрибут, т.е. столбец с табельным номером.
Табельный номер | ФИО | Должность | Подразделение | Описание подразделения |
---|---|---|---|---|
1 | Булыкин И.И. | Программист | Отдел разработки | Разработка и сопровождение приложений и сайтов |
2 | Григорьев С.С. | Бухгалтер | Бухгалтерия | Ведение бухгалтерского и налогового учета финансово-хозяйственной деятельности |
3 | Анджелина Джоли | Продавец | Отдел реализации | Организация сбыта продукции |
4 | Григорьев С.С. | Программист | Бухгалтерия | Ведение бухгалтерского и налогового учета финансово-хозяйственной деятельности |
В данном случае используется целочисленный тип данных, который автоматически увеличивался в случае добавления новый записи в таблицу. Тем самым мы бы точно также четко идентифицировали каждую строку в таблице |
Чтобы база данных находилась во третьей нормальной форме (3NF), необходимо чтобы ее таблицы удовлетворяли следующим требованиям:
- Таблица должна находиться во второй нормальной форме
- Каждый не ключевой атрибут нетранзитивно зависит от первичного ключа
Проще говоря, второе правило требует выносить все не ключевые поля, содержимое которых может относиться к нескольким записям таблицы в отдельные таблицы.
У нас имеется таблица сотрудников в 2NF.
Табельный номер | ФИО | Должность | Подразделение | Описание подразделения |
---|---|---|---|---|
1 | Булыкин И.И. | Программист | Отдел разработки | Разработка и сопровождение приложений и сайтов |
2 | Григорьев С.С. | Бухгалтер | Бухгалтерия | Ведение бухгалтерского и налогового учета финансово-хозяйственной деятельности |
3 | Анджелина Джоли | Продавец | Отдел реализации | Организация сбыта продукции |
4 | Григорьев С.С. | Программист | Бухгалтерия | Ведение бухгалтерского и налогового учета финансово-хозяйственной деятельности |
Чтобы определить, находится ли эта таблица в 3NF, мы должны проверить все неключевые столбцы. Каждый из них должен зависеть только от первичного ключа, и никаким образом к другим неключевым столбцам он не должен относиться.
В результате проверки мы выясняем, что столбец «Описание подразделения» не зависит напрямую от первичного ключа. Мы это выяснили, когда задали себе один вопрос «Каким образом описание подразделения связано с сотрудником?». И наш ответ звучит следующим образом: «Атрибут описание подразделения содержит детальные сведения того подразделения, в котором работает сотрудник».
Отсюда следует, что столбец «Описание подразделения» не связан на прямую с сотрудником, он связан напрямую со столбцом «Подразделение», который напрямую связан с сотрудником, ведь сотрудник работает в каком-то конкретном подразделении. Это и есть транзитивная зависимость, когда один неключевой столбец связан с первичным ключом через другой неключевой столбец.
Для решения этой проблемы мы должны выполнить декомпозицию - разбить нашу таблицу на две. В одной таблице будем хранить сотрудников, а во второй - подразделения. В подразделениях мы обязаны создать первичный ключ, в нашем случае это будет колонка «Идентификатор подразделения».
Таблица подразделений в третьей нормальной форме.
Идентификатор подразделения | Подразделение | Описание подразделения |
---|---|---|
1 | Отдел разработки | Разработка и сопровождение приложений и сайтов |
2 | Бухгалтерия | Ведение бухгалтерского и налогового учета финансово-хозяйственной деятельности |
3 | Отдел реализации | Организация сбыта продукции |
И затем для реализации связи в таблице сотрудников создать ссылку на таблицу подразделений, т.е. добавить внешний ключ.
Таблица сотрудников в третьей нормальной форме.
Табельный номер | ФИО | Должность | Подразделение |
---|---|---|---|
1 | Булыкин И.И. | Программист | 1 |
2 | Григорьев С.С. | Бухгалтер | 2 |
3 | Анджелина Джоли | Продавец | 3 |
4 | Григорьев С.С. | Программист | 2 |
Внешний ключ связывает значения столбца «Идентификатор подразделения» таблицы подразделений со столбцом «Подразделение» таблицы сотрудников
Один-к-одному - одна запись из левой таблицы связана с одной записью из правой таблицы Один-ко-многим - одной записи из левой таблицы может соответствовать много записей из правой таблицы. Но у одной записи из первой таблицы только одна запись из левой. Многие-ко-многим - одной записи из левой таблицы соответствует много записей из правой таблицы. Одной записи из правой таблицы может соответствовать много записей из левой таблицы.
# models.py
class Movie(models.Model)
...
director = models.ForeignKey(Director, on_delete=models.PROTECT, null=True,)
class Director(models.Model):
pass
С этим полем также можно работать в админке, как и другими полями модели.
PROTECT
- нельзя удалить запись, пока хоть одна запись в другой таблице ссылается на нееCASCADE
- при удалении запись удаляются все запись, которые на неё ссылаютсяSET_NULL
- при удалении запись, запись в строках, ссылающиеся на неё устанавливаются вNULL
# models.py
from django.db import models
class Movie(models):
actors = models.ManyToManyField(to='Actor',)
class Actor(models.Model):
MALE = 'm'
FEMALE = 'f'
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
GENDER_TYPES = {MALE: "м", FEMALE: 'ж'}.items()
gender = models.CharField(max_length=1, choices=GENDER_TYPES)
#admin.py
class MovieAdmin(admin.ModelAdmin):
...
filter_horizontal = ['actors'] # более удобный фильтр выбора актеров
# filter_vertical = ['actors'] # другой вертикальный фильтр
Нельзя размещать это поле в list_display
в админке.
Находим режиссёра и актёров по фильму:
movie = Movie.objects.filter(director__isnull=False)
movie_director = movie.director # режиссёр фильма
movie_actors = movie.actors.all() # QuerySet всех актёров
{% for actor in movie.actors.all %}
<li>
{{ actor }}
</li>
{% endfor %}
Каждый раз, когда мы связываем таблицы при помощи внешнего ключа в отношении многие-ко-многим, джанго автоматически создаёт поле обратной связи.
Название поля обратной связи выглядит как названиеМодели_set
. Оно представляет собой объект обратной связи. У этого объекта есть метод all()
, который позволяет получить все записи.
Название поля обратной связи можно изменить.
Находим список фильмов режиссера
class Movie(models.Model):
...
director = models.ForeignKey(to=Director)
director = Director.objects.all[0]
movies = director.movie_set.all()
Находим список фильмов по актёру через связь многие-ко-многим:
class Movie(models.Model):
actors = models.ManyToManyField(to=Actor)
...
class Actor(models.Actor):
...
actor = Actor.objects.all()[0]
movies = actor.movie_set.all()
Для изменения поля обратной связи в модели служит атрибут related_name
class Movie(models.Model):
...
actors = models.ManyToManyField(to-Actor, related_name="movies")
actor = Actor.objects.all()[0]
actor_movies = actor.movies.all()
python manage.py shell_plus --print-sql
ts2 = Movie.objects.filter(name__icontains="Toy Story 2")
ts2.director = None
ts2.save()
d = Director.objects.get(id=0)
ts2.director = d
ts2.save()
Если хотим создать нового режиссёра и добавить его к фильму, то после добавить можно уже существующий объект в базе данных. Т.е. его надо после создания сохранить
d_voody = Director(
first_name="Вуди",
last_name="Ален",
)
voody.save()
ts3 = Movie(
name="Toy story 3",
rating=65,
director=d_voody,
)
ts3.save()
При использовании метода create
объект сразу сохраняется в бд, и нет необходимости вызывать метод save
.
d_voody = Director.objects.create(
first_name="Вуди",
last_name="Ален",
)
ts3 = Movie.objects.create(
name="Toy story 3",
rating=65,
director=d_voody,
)
Т.е. связи можно образовывать между реально существующими объектами в базе данных.
Связи также надо делать между существующими объектами в базе данных. Например нельзя добавлять актёров для фильма, который ещё не был сохранён в базе данных.
actor1 = Actor.objects[1]
actor2 = Actor.objects[2]
ts3.actors.add(actor1) # добавляем актёра1
ts3.actors.add(actor2) # добавляем актёра2
ts3.actors.remove(actor1) # удаляем актёра
ts3.actors.remove(actor1) # ещё раз удаляем ошибки не будет
class DressingRoom(models.Model):
floor = models.PositiveIntegerField()
number = models.PositiveIntegerField()
def __str__(self):
return f"Гримёрка #{self.number} на ${self.floor} этаже."
class Actor(models.Model):
...
dressing_room = models.OneToOneField(
to=DressingRoom,
on_delete=models.SET_NULL,
null=True,
blank=True,
)
Параметр related_name
в атрибуте модели можно не менять, т.к. для записи один-к-одному django создаёт название поля, полностью совпадающее с названием модели.
d = DressingRoom.objects.all()[0]
dressing_room_actor = d.actor # получаем объект актера по гримёрке
a = Actor.objects.all()[0]
a.dressing_room = d
a.save() # поолучим ошибку, т.к. это связь 1-к-1, а гримерка уже связана
# admin.py
@admin.register(models.DressingRoom)
class DressingRoomAdmin(admin.ModelAdmin):
list_display = ['floor', 'number', 'actor']
Предположим надо создать комментарий к посту
# models.py
class Comment(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
text = models.TextField()
user = models.ForeignKey(
to=User, on_delete=models.CASCADE, related_name="user_comments"
)
post = models.ForeignKey(
to=PostModel, on_delete=models.CASCADE, related_name="comments"
)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return f'{self.user.username}: "{self.text}"'
related_name
позволяет задать атрибут у связанной записи. Теперь у поста можно получить все комментарии обратившись по атрибуту comments
, а не comment_model_set
:
slug = 'this-post-about-butterflies'
post_comments = Post.objects.get(slug=slug).comments.all()
Выведем её в шаблоне
{% for comment in post.comments.all %}
{{comment.user.username }}: "{{comment.text}}"
{% endfor %}
Добавим модель формы, основанную на модели комментария
#forms.py
from django import forms
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ("text",)
widgets = {
"text": forms.Textarea(
attrs={
"class": "form-control",
"name": "text",
"rows": 4,
"placeholder": "Оставьте ваш комментарий",
}
)
}
В мете зададим необходимое представление для html полей и те поля, которые нам нужны для комментария.
Теперь надо изменить views.py для добавления формы
#views.py
class PostView(View):
def get(self, request, slug: str):
post = get_object_or_404(klass=PostModel, slug=slug)
recent_posts = PostModel.objects.order_by("-created_at").exclude(
slug=slug
)[:3]
form = CommentForm()
return render(
request,
"myblog/post_page.html",
{"post": post, "recent_posts": recent_posts, "form": form},
)
def post(self, request, slug: str):
post = get_object_or_404(klass=PostModel, slug=slug)
form = CommentForm(data=request.POST)
user = self.request.user
if form.is_valid and user is not None:
recent_posts = PostModel.objects.order_by("-created_at").exclude(
slug=slug
)[:3]
form_data = {"text": request.POST.get("text"), "user": user, "post": post}
Comment.objects.create(**form_data)
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
else:
form.add_error("text", "Сообщение должно быть не пустое")
return render(
request,
"myblog/post_page.html",
{"post": post, "recent_posts": recent_posts, "form": form},
)
return HttpResponseRedirect(reverse("myblog:post_page", args=(slug,)))
Теперь мы выводим форму в методе get. Также получаем данные в методе пост. Проверяем что пост и пользователь существуют.
Затем сохраняем объект комментария и перенаправляем на ту страницу, с который был запрос при нажатии кнопки submit - HTTP_REFERER.
{% if user.is_authenticated %}
<div class="card w-100">
<div class="card-header">Оставьте комментарий</div>
<div class="card-body">
<form class="d-flex flex-column align-items-end gap-2 container-fluid" action="" method="POST">
{% csrf_token %}
{% for error in form.text.errors %}
<p class="alert alert-danger">{{ error|escape }}</p>
{% endfor %}
{{ form.text }}
<button type="submit" class="btn btn-primary">Отправить</button>
</form>
</div>
</div>
{% else %}
<small class="text-body-secondary">
<a href="{% url 'myblog:login' %}">Войдите</a>
или
<a href="{% url 'myblog:register' %}">зарегистрируйтесь</a>
, чтобы оставлять комментарии.
</small>
{% endif %}
для удаления комментария нужен роут для удаления и его id. Ид будем получать у тех комментариев, которые соответствуют текущему пользователю и который совпадает с владельцем комментария
{% if user.is_authenticated and comment.user == user %}
<a href="{% url 'myblog:comment_delete' comment.id %}">
<button class="btn btn-sm btn-danger">Удалить</button>
</a>
{% endif %}
Т.е. в шаблоне причем кнопку тем пользователям, у кого она будет работать.
Во вь.хе пишем обработчик get запроса.
# views.py
class CommentDelete(View):
def get(self, request, comment_id):
user = self.request.user
comment = get_object_or_404(klass=Comment, id=comment_id)
if user == comment.user:
comment.delete()
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
В нём мы получаем ид комментария, достаем пользователя, проверяем их соответствие и удаляем комментарий.
Форма отправляет запросы двух видов: GET и POST.
По-умолчанию форма отправляет GET запрос с данными проименованных полей в виде query параметров.
Для извлечения query параметров из запроса надо обратиться к полю GET
объекта request
:
def index(request):
query_params = request.GET # QuerySet - аналог словаря
pass
# извлечение определенного значения
name_param: str = request.GET.get('name')
- GET запросы могут кэшироваться
- GET запросы остаются в истории браузера
- GET запросы могут быть закладками
- GET запросы никогда не должны использоваться при работе с конфиденциальными данными
- GET запросы имеют ограничения по длине
- GET запросы должны использоваться только для извлечения данных
- POST запросы никогда не кэшируются
- Запросы POST не сохраняются в журнале обозревателя
- Запросы POST не могут быть закладками
- Запросы POST не имеют ограничений по длине данных
Чтобы изменить тип запроса на POST в форме нужно прописать параметр method
:
<form method="post">
{% csrf_token %}
<input name="feedback">
<button type='submit'>
</form>
def index(request):
post_parameters_dict = request.POST
...
При отправке методом post
надо установить csrf_token
чтобы защититься от межсайтовой подделки запросов.
Поэтому надо вставить этот токен в форму, чтобы он отправлялся вместе с данными формы.
Для извлечения пост параметров из запроса используется конструкция data: QueryDict = request.POST
.
по умолчанию форма отправляет данные на тот адрес, с которого она открыта. Чтобы изменить его
CSRF (англ. cross-site request forgery перевод межсайтовая подделка запроса) — вид атак на посетителей веб-сайтов, использующий недостатки протокола HTTP. Это атака, которой может подвергаться любой веб-ресурс или веб-приложение. В первую очередь это касается сайтов, которые используют cookies, сертификаты авторизации и браузерную аутентификацию. В результате атаки страдают клиенты и репутация ресурса.
Причина CSRF кроется в том, что браузеры не понимают, как различить, было ли действие явно совершено пользователем (как, скажем, нажатие кнопки на форме или переход по ссылке) или пользователь неумышленно выполнил это действие (например, при посещении bad.com, ресурсом был отправлен запрос на good.com/some_action, в то время как пользователь уже был залогинен на good.com).
Атака осуществляется путём размещения на веб-странице ссылки или скрипта, пытающегося получить доступ к сайту, на котором атакуемый пользователь заведомо (или предположительно) уже аутентифицирован. Например, пользователь Алиса может просматривать форум, где другой пользователь, Боб, разместил сообщение. Пусть Боб создал тег , в котором в качестве источника картинки указал URL, при переходе по которому выполняется действие на сайте банка Алисы, например:
Боб: Привет, Алиса! Посмотри, какой милый котик:
<img src="http://bank.example.com/?account=Alice&amount=1000000&for=Bob">
Если банк Алисы хранит информацию об аутентификации Алисы в куки, и если куки ещё не истекли, при попытке загрузить картинку браузер Алисы отправит куки в запросе на перевод денег на счёт Боба, чем подтвердит аутентификацию Алисы. Таким образом, транзакция будет успешно завершена, хотя её подтверждение произойдет без ведома Алисы.
Эффективным и общепринятым на сегодня способом защиты от CSRF-Атаки является токен. Под токеном имеется в виду случайный набор байт, который сервер передает клиенту, а клиент возвращает серверу.
Защита сводится к проверке токена, который сгенерировал сервер, и токена, который прислал пользователь.
В общем понимании токен — это механизм, который позволяет идентифицировать пользователя или конкретную сессию для безопасного обмена информацией и доступа к информационным ресурсам. Токены помогают проверить личность пользователя (например, клиента, который онлайн получает доступ к банковскому счёту). Их используют как вместо пароля, так и вместе с ним. Токен — это в каком-то смысле электронный ключ.
CSRF-token — это максимально простой и результативный способ защиты сайта от CSRF-мошенников. Он работает так: сервер создаёт случайный ключ (он же токен) и отправляет его браузеру клиента. Когда браузер запрашивает у сервера информацию, сервер, прежде чем дать ответ, требует показать ключ и проверяет его достоверность. Если токен совпадает, сессия продолжается, а если нет — прерывается. Токен действителен только одну сессию — с новой сессией он обновляется.
Чтобы получить ответ от сервера, используются разные методы запроса. Условно они делятся на две категории: те, которые не изменяют состояние сервера (GET, TRACE, HEAD), и те, которые изменяют (PUT, PATCH, POST и DELETE). Последние имеют большую CSRF-уязвимость и поэтому должны быть защищены в первую очередь.
При создании и использовании токена должны соблюдаться следующие условия:
- нахождение в скрытом параметре;
- генерация с помощью генератора псевдослучайных чисел;
- ограниченное время жизни (одна сессия);
- уникальность для каждой транзакции;
- устойчивый к подбору размер (в битах);
- невозможно переиспользовать.
Сессия - это установленное tcp connection (status "CONNECTED"). Текущие можно посмотреть так:
netstat -na | grep "CONNECTED"
# views.py
def index(request):
if request.method == 'POST':
name = request.POST.get('name')
if not name:
return render(
request,
'feedback/feedback.html',
{'error-name': True}
)
return HttpResponseRedirect(
reverse(viewname='feedback:done')
)
return render(..., {'error-name': False})
def done(request):
return render(request, '/templates/feedback/done/html')
Не очень удобно выполнять проверку на все поля формы вручную.
Формы для джанго пишутся в отдельном файле app_name/forms.py
# forms.py
from django import forms
class FeedbackForm(forms.Form):
name = forms.CharField(
max_length=100,
label="имя",
)
surname = forms.CharField(
max_length=100,
min_length=2,
required=False,
label="фамилия",
)
feedback = forms.CharField(
label="отзыв",
widget=forms.Textarea(attrs={
"rows": "3",
"cols": "40",
"class": "my-class"
}),
)
# необязательный метод
# вызывается при валидации формы is_valid
def clean(self):
password = self.cleaned_data.get("password")
confirm_password = self.cleaned_data.get("repeat_password")
if password != confirm_password:
raise forms.ValidationError("Пароли не совпадают")
Затем в шаблон вставляется отдельный объект формы через контекст:
# views.py
from .forms import FeedbackForm
def index(request):
if request.method == "POST"
form = FeedbackForm(request.POST)
if form.is_valid:
print(form.cleaned_data) # словарь со значениями формы
return HttpResponseRedirect(reverse("feedback:done"))
form = FeedbackForm()
return render_template(
request,
'feedback/feedback.html',
{'forms': form},
)
<form action="" method="post">
{% csrf_token %}
{{ form }}
<input type="submit">
</form>>
https://docs.djangoproject.com/en/5.0/ref/forms/widgets/
Виджет - задаёт представление поля в HTML.
Мы можем сказать, что CharField(max_length) == VARCHAR(n)
. По сути это одно и тоже. Это тип поля в базе данных. Поля формы обеспечивают логику проверки вводимой информации и используются в шаблонах.
Виджет – это представление поля в виде HTML кода. Виджеты обеспечивают генерацию HTML. Виджеты отвечают за рендеринг форм на веб-странице и обработку переданных данных. Виджеты следует назначать на поля формы.
Т.е. у нас есть CharField - это текстовое поле. Но на вебстранице текстовое поле может быть разным, хотя оно выглядит одинаково. Например текстовое поле может быть предназначено для
PasswordInput
- текстовое поле для ввода пароля (вместо символов звездочки).EmailInput
- для ввода почты.NumberInput
- только для ввода цифр. и т.д.
Помимо этого, внутри виджета вы можете самостоятельно настраивать все атрибуты поля, как в html. Вот так наше поле выглядит в html:
<input type="username" class="form-control" id="inputUsername" aria-describedby="userName" placeholder="Имя пользователя">
У нас есть атрибуты у тега input - type, class, id, placeholder. Чтобы добавить эти же атрибуты к нашему полю в Django, нам надо просто запихнуть их в атрибуты виджета:
attrs={
'class': "form-control",
'id': "inputUsername",
'type': 'username',
'placeholder': 'Имя пользователя',
}
Для того чтобы удобнее работать с полями форм и их атрибутами, если библиотека под названием Crispy Forms.
метод clean
вызывается при валидации is_valid()
.
В нашей форме мы валидируем то, что бы первый пароль и второй совпадали, а в cлучае если нет, то чтобы возвращалось сообщение "Пароли не совпадают"
def clean(self):
password = self.cleaned_data['password']
confirm_password = self.cleaned_data['repeat_password']
if password != confirm_password:
raise forms.ValidationError(
"Пароли не совпадают"
)
Отвечает за сохранение формы в базу данных.
def save(self):
user = User.objects.create_user(
username=self.cleaned_data.get('username'),
password=self.cleaned_data.get('password'),
)
user.save()
auth = authenticate(**self.cleaned_data)
return auth
При сохранении формы создаётся объект user.
Далее производим аутентификацию пользователя методом authenticate
, которому передаем логин и пароль. И возвращаем аутентифицированного пользователя auth
.
# forms.py
class FeedbackForm(forms.Form):
name = forms.CharField(
min_length=2,
max_length=10,
label="Имя",
error_messages = {
"min_length": "Мало символов",
"max_length": "Много символов",
"required": "Должно что-то быть",
},
)
rating = forms.IntegerField(
min_value=1,
max_value=5,
initial=1,
)
В данном примере, мы отправляем невалидную форму обратно клиенту с уже заполненными полями, а не пустую форму.
# views.py
def index(request):
if request.method == 'POST':
form = FeedbackForm(data=request.POST)
if form.is_valid:
pass
# ...
else:
# ...
pass
else:
form = FeedbackForm()
return render(
request,
'feedback.html',
{'form': form},
)
Таким образом сначала у клиента форма будет валидироваться через html атрибуты тегов, затем в джанго через параметры поля формы.
Можно выводить не все поля формы целиком, а вручную поэлементно
<form>
{% csrf_token %}
{{ form.name.label_tag }}
{{ form.name.errors }}
{{ form.name }}
{{ form.surname.label_tag }}
{{ form.surname.errors }}
{{ form.surname }}
</form>
Чтобы вручную не писать каждое поле, то структуру всех полей записывают в цикле:
{% load static %}
<head>
<link rel="stylesheet" href="{% static 'feedback/css/form.css' %}">
</head>
<body>
<form>
{% csrf_token %}
{% for field in form %}
<div class="form-style {%if field.errors %}error-class{% endif %}">
{{ field.label_tag }}
{{ field.errors }}
{{ field }}
</div>
{% endfor %}
</form>
</body>
Можно динамически добавлять класс тем полям, которые с ошибками написав условие прямо в коде.
Создадим модель данных для формы
# models.py
class Feedback(models.Model):
name = models.CharField(max_length=100)
surname = models.CharField(max_length=100)
feedback = models.TextField()
rating = models.PositiveIntegerField()
ВО вьюшке сохраним объект модели из формы после валидации и сохраним его в базу данных.
# views.py
from .forms import FeedbackForm
from .models import Feedback
...
if form.is_valid:
feed = Feedback(
name=form.cleaned_data.get('name'),
surname=form.cleaned_data.get('surname'),
feedback=form.cleaned_data.get('feedback'),
rating=form.cleaned_data.get('rating'),
)
feed.save()
или более короче, если поля формы соответствуют модели
...
if form.is_valid:
Feedback.objects.create(**form.cleaned_data)
...
Описание модели для формы очень похоже на форму. Чтобы не повторять все записи, в джанго можно создать форму из модели.
# forms.py
from django import forms
from .models import Feedback
class FeedbackForm(forms.ModelForm):
class Meta:
model = Feedback
# fields = ['name', 'surname', 'feedback', 'rating']
exclude = []
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'surname': forms.TextInput(attrs={'class': 'form-control'}),
'feedback': forms.Textarea(attrs={'class': 'form-control'}),
'rating': forms.NumberInput(attrs={'class': 'form-control'}),
}
Т.к. есть прямая связь между формой и моделью, уже не надо в объект модели передавать все поля формы, мы сразу получаем объект модели из формы.
# views.py
def index(request) -> HttpResponse:
if request.method == "POST":
form = FeedbackForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse("feedback:done"))
else:
form = FeedbackForm()
return render(
request,
"feedback/feedback.html",
{"form": form, "feedback_id": 3},
)
class IntegerRangeField(models.IntegerField):
def __init__(self, verbose_name=None, name=None, min_value=None, max_value=None, **kwargs):
self.min_value, self.max_value = min_value, max_value
models.IntegerField.__init__(self, verbose_name, name, **kwargs)
def formfield(self, **kwargs):
defaults = {'min_value': self.min_value, 'max_value': self.max_value}
defaults.update(kwargs)
return super(IntegerRangeField, self).formfield(**defaults)
def update_feedback(request, feedback_id: int) -> HttpResponse:
feed = get_object_or_404(klass=Feedback, id=feedback_id)
if request.method == "POST":
form = FeedbackForm(data=request.POST, instance=feed)
if form.is_valid():
feed.save()
return HttpResponseRedirect(
reverse(
viewname="feedback:update_feedback", args=(feedback_id,)
)
)
else:
form = FeedbackForm(instance=feed)
return render(request, "feedback/feedback.html", {"form": form})
При создании формы из класса ModelForm можно её сразу привязывать к существующему объекту из базы данных.
feed = get_object_or_404(klass=Feedback, id=feedback_id)
form = FeedbackForm(data=request.POST, instance=feed)
Это представление на классах. Можно реализовывать логику внутри views.py
при помощи классов а не функций.
Существует несколько вариантов. Будет рассмотрен только один - самый простой.
При использовании CBV название методов должны совпадать с именем http методов
# views.py
from django.views import View
class FeedbackView(View):
def get(self, request):
form = FeedbackForm()
return render(
request, 'feedback/feedback.html', {'form': form},
)
def post(self, request):
form = FeedbackForm(**request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse("feedback:done"))
return render(
request, 'feedback/feedback.html', {'form': form},
)
Для привязки роутов к CBV используется метод класса .as_view()
from . import views
urlpatterns = [
path('', views.FeedbackView.as_view(), name="feedback_index")
]
# views.py
class FeedbackView(View):
def get(self, request):
form = FeedbackForm()
return render(
request,
"feedback/feedback.html",
{"form": form},
)
def post(self, request):
form = FeedbackForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse(viewname="feedback:done"))
return render(
request,
"feedback/feedback.html",
{"form": form},
)
class DoneView(View):
def get(self, request) -> HttpResponse:
return HttpResponse("Спасибо, ваш отзыв получен")
class FeedbackUpdateView(View):
def get(self, request, feedback_id: int) -> HttpResponse:
feed = get_object_or_404(klass=Feedback, id=feedback_id)
form = FeedbackForm(instance=feed)
return render(
request,
"feedback/feedback.html",
{"form": form},
)
def post(self, request, feedback_id: int) -> HttpResponse:
feed = get_object_or_404(klass=Feedback, id=feedback_id)
form = FeedbackForm(data=request.POST, instance=feed)
if form.is_valid():
form.save()
return render(
request,
"feedback/feedback.html",
{"form": form},
)
def update_feedback(request, feedback_id: int) -> HttpResponse:
feed = get_object_or_404(klass=Feedback, id=feedback_id)
if request.method == "POST":
form = FeedbackForm(data=request.POST, instance=feed)
if form.is_valid():
print(f"{form = }", request.POST)
feed.save()
return HttpResponseRedirect(
reverse(
viewname="feedback:update_feedback", args=(feedback_id,)
)
)
else:
form = FeedbackForm(instance=feed)
return render(request, "feedback/feedback.html", {"form": form})
# urls.py
urlpatterns = [
path(
"feedback/<int:feedback_id>/",
views.FeedbackUpdateView.as_view(),
name="update_feedback",
),
path("feedback/", views.FeedbackView.as_view(), name="index"),
path("done/", views.DoneView.as_view(), name="done"),
]
# models.py
from django.db import models
class Feedback(models.Model):
name = models.CharField(max_length=100, verbose_name="имя")
surname = models.CharField(max_length=100, verbose_name="фамилия")
feedback = models.TextField(verbose_name="отзыв")
rating = models.PositiveIntegerField(verbose_name="рейтинг")
# forms.py
from django import forms
from django.forms import ModelForm
from .models import Feedback
class FeedbackForm(ModelForm):
class Meta:
model = Feedback
exclude = []
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'surname': forms.TextInput(attrs={'class': 'form-control'}),
'feedback': forms.Textarea(attrs={'class': 'form-control'}),
'rating': forms.NumberInput(attrs={'class': 'form-control'}),
}
Специальный класс, чтобы сразу отдавать шаблон а не наследоваться от метода View и писать метод get с выдачей шаблона через render.
# views.py
from django.views.generic.base import TemplateView
# class DoneView(View):
# def get(self, request) -> HttpResponse:
# return render(
# request,
# "feedback/done.html",
# )
class DoneView(TemplateView):
template_name = 'feedback/done.html'
Т.к. этот класс наследуется от View, то у него также есть метод .as_view()
для создания представления.
# urls.py
urlpatterns = [
path('done/', views.DoneView.as_view(), name='done')
]
Чтобы передать параметры в шаблон, нужно у класса переопределить родительский метод get_context_data
class DoneView(TemplateView):
template_name = 'feedback/done.html'
def get_context_data(self, **kwargs):
context: dict = super().get_context_data(**kwargs)
context['name'] = 'ivanov'
context['data'] = '23.03.23'
return context
Таким образом эти переменные будут доступны внутри шаблона. Вот пример как можно получать динамический параметр в таком классе:
# views.py
# class FeedbackInfoView(View):
# def get(self, request, feedback_id: int) -> HttpResponse:
# feedback = get_object_or_404(klass=Feedback, id=feedback_id)
# return render(
# request,
# "feedback/feedback_info.html",
# {"feedback": feedback},
# )
class FeedbackInfoView(TemplateView):
template_name = "feedback/feedback_info.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
feedback_id: int = kwargs.get("feedback_id")
feedback = get_object_or_404(klass=Feedback, id=feedback_id)
context["feedback"] = feedback
return context
Задача этого шаблона - отображать данные из бд, т.е. несколько объектов модели данных.
По-умолчанию данный шаблон список объектов помещает в context['object_list']
.
Чтобы переопределить имя этого атрибута надо переопределить context_object_name
from django.views.generic import ListView
# class FeedbackListView(TemplateView):
# template_name = "feedback/list_feedback.html"
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# context["feedbacks"] = Feedback.objects.all()
# return context
class FeedbackListView(ListView):
template_name = 'feedback/list_feedback.html'
model = Feedback
context_object_name = 'feedbacks'
# не обязательынй параметр пагинации
paginate_by = 5
Также доступен полный перечень стандартных методов, например get_context_data
.
С помощью переопределения родительского метода get_queryset
(возвращает множество объектов модели тип QuerySet
) можно фильтровать значения.
from django.views.generic import ListView
class FeedbackList(ListView):
template_name = 'feedback/list_feedback.html'
model = Feedback
context_object_name = 'feedbacks'
def get_queryset(self):
# получаем от родительского метода список всех объектов
queryset = super().get_queryset()
# возвращает записи с рейтингом >= 4
qs = queryset.filter(rating__gte=4)
return qs
Если хотите дополнительно передать другие аргументы из views в шаблон, то можете использовать переменную extra_context, где передать словарь. Например,
extra_context = {'agg': Actor.objects.aggregate(Count('id'))}
https://docs.djangoproject.com/en/5.0/topics/pagination/ https://proproprogs.ru/django/postranichnaya-navigaciya-paginaciya
<nav aria-label="page navigation">
<ul class="pagination d-flex justify-content-center">
<li class="page-item {% if not page_obj.has_previous %}disabled{% endif %}">
<a href="" class="page-link">
<span aria-hidden="true">«</span>
</a>
</li>
{%for p in paginator.page_range %}
{% if p == page_obj.number %}
<li class="page-item">
<a href="" class="page-link active">{{ p }}</a>
</li>
{% elif p >= page_obj.number|add:-2 and p <= page_obj.number|add:2 %}
<li class="page-item">
<a href="?page={{ p }}" class="page-link">
{{ p }}
</a>
</li>
{% endif %}
{% endfor %}
<li class="page-item {% if not page_obj.has_next %}disabled{% endif %}">
<a href="?page={{ page_obj.paginator.num_pages }}" class="page-link" aria-label="next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
Paginator
- специальный класс для пагинации страниц.
p = Paginator(objects, per_page_number)
- создание объекта пагинатора
p.count
- всего объектовp.num_pages
- число страницp.page_range
- итератор по страницам-
page_obj = p.page(1)
- первая страница
page_obj.object_list
- список объектов на 1й страницеpage_obj.has_next: bool
- есть ли страница ещёpage_obj.has_previous: bool
page_obj.has_other_pages()
- # имеются ли вообще страницы
#views.py
from django.core.paginator import Paginator
class ObjectList(View):
def get(self, request):
page_number = request.GET.get('page', 1)
# все объекты
objects = MuModel.objects.all()
num_per_page = 4
paginator = Paginator(objects, num_per_page)
# список объектов на странице
page_obj = paginator.get_page(page_number)
context = {
'paginator': paginator,
'page_obj': page_obj,
}
...
Если необходимо при пагинации учитывать дургие query параметры, то надо их вынести в условие шаблона
{% if paginator.count > 0 %}
<nav aria-label="Page navigation example" class="d-flex justify-content-center">
<ul class="pagination d-flex">
{% if paginator.num_pages > 1 %}
<li class="page-item disabled">
<a class="page-link" href="?page=1{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% endif %}
{% for p in paginator.page_range %}
{% if p == page_obj.number %}
<li class="page-item active"><a class="page-link" href="#">{{ p }}</a></li>
{% elif p >= page_obj.number|add:-3 and p <= page_obj.number|add:3 %}
<li class="page-item"><a class="page-link" href="?page={{ p }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">{{ p }}</a></li>
{% endif %}
{% endfor %}
{% if paginator.num_pages > 1 %}
<li class="page-item">
<a class="page-link {% if not page_obj.has_next %}disable{% endif %}" href="?page={{paginator.num_pages}}{% if request.GET.q %}&q={{request.GET.q}}{%endif%}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
Для отображения одной записи из базы данных.
Для того, чтобы класс DetailView
понял как ему получать элемент из базы данных, надо определённым образом задавать имя аргумента во вьюхе:
pk
- поиск по первичному ключуslug
- поиск по слагу
# urls.py
urlpatterns = [
path('feedback/<int:pk>', views.FeedbackInfoView.as_view()),
path('movies/<slug:slug>', views.MovieInfoView.as_view()),
]
# views.py
from django.views.generic import DetailView
class FeedbackInfoView(DetailView):
template_name = 'feedback/'
model = Feedback
# context_object_name = 'feedback' # по умолчанию и так будет таким же
Имя переменной в контексте, в которой данный класс сохраняет значение объекта - это имя класса в нижнем регистре. Например для класса Feedback
будет существовать соответствующий ключ в context['feedback']
.
Через context_object_name
можно изменить это поведение.
Можно сразу реализовать это действие через потомков базового класса, просто создав инстанс от DetailView
и передав в него необходимые параметры:
# urls.py
urlpatterns = [
path(
'feedbacks/<int:pk>',
DetailView(
template_name='feedback/feedback_info.html',
model=Feedback,
context_object_name='feedback',
),
)
]
# views.py
from django.views.generic import FormView
# class FeedbackView(View):
# def get(self, request):
# form = FeedbackForm()
# return render(
# request,
# "feedback/feedback.html",
# {"form": form},
# )
# def post(self, request):
# form = FeedbackForm(request.POST)
# if form.is_valid():
# form.save()
# return HttpResponseRedirect(reverse(viewname="feedback:done"))
# return render(
# request,
# "feedback/feedback.html",
# {"form": form},
# )
class FeedbackView(FormView):
# для метода get достаточно определить модель и шаблон
model = FeedbackForm
template_name = 'feedback/feedback.html'
# post
success_url = '/done'
Для определения метода get
достаточно в наследуемом классе определить template_name
и модель формы model
.
Для отработки отправки формы в классе формы надо определить success_url
- путь по которому будет отправляться форма при сабмите.
И указать классу что далеть с данными формы.
Для этого надо переопределить метод FormView.form_valid
.
class FeedbackView(FormView):
template_name = ...
model = FeedbackForm
success_url = '/done'
def form_valid(self, form):
form.save()
return super(FeedbackView, self).form_valid(form)
Предназначены для создания и изменения элементов
from django.views.generic import CreateView
class FeedbackView(CreateView):
template_name = 'feedback/feedback.html'
# класс модели
model = Feedback
# класс формы для модели
from_class = FeedbackForm
# урл успешного перехода
success_url = '/done/'
Если не указать класс с формой, то джанго может сформировать форму на основе модели, если указать атрибут fields:
class FeedbackView(CreateView):
model = Feedback
fields = ['name', 'surname', ] # отобразить не все поля
fields = '__all__' # отобразить все поля
success_url = '/done/'
template_name = 'feedback/feedback.html'
При этом может столкнуться с ограничениями модели при отправки не всех полей в форме.
Цель - изменение данных в бд
# views.py
from django.views.generic import UpdateView
# class FeedbackUpdateView(View):
# def get(self, request, feedback_id: int) -> HttpResponse:
# feed = get_object_or_404(klass=Feedback, id=pk)
# form = FeedbackForm(instance=feed)
# return render(
# request,
# "feedback/feedback.html",
# {"form": form},
# )
# def post(self, request, feedback_id: int) -> HttpResponse:
# feed = get_object_or_404(klass=Feedback, id=pk)
# form = FeedbackForm(data=request.POST, instance=feed)
# if form.is_valid():
# form.save()
# return render(
# request,
# "feedback/feedback.html",
# {"form": form},
# )
class FeedbackUpdateView(UpdateView):
model = Feedback
form_class = FeedbackForm
success_utl = '/done/'
template_name = 'feedback/feedback.html'
Ну забыть поменять ключ в urls.py на pk
или slug
.
Также, как и с CreateView
можно не указывать класс формы, а указать список полей, например fields = '__all__'
.
# views.py
from django.views.generic import DeleteView
class FeedbackDeleteView(DeleteView):
model = Feedback
# куда будет выполнен переход после удаления
# если ошиюка, писать строку
success_url = reverse('feedback:all_feedbacks')
template_name = 'feedback/feedback_delete.html'
Добавим путь, по которому будет работать этот класс.
# urls.py
urlpatterns = [
...,
path(
'feedback/delete/<int:pk>/',
FeedbackDeleteView.as_view(),
name='delete_feedback'
)
]
Далее можно создать форму, которая отправляет POST
запрос на url, к которому привязан этот класс. Сработает метод пост, который удалит ассоциированную запись по pk
или по slug
.
Форму можно создать на любой странице, т.о. переход на форму удаления производиться не будет. Если ну указать имя шаблона, то метод GET
не будет реализован для этого url.
Создадим форму дял добавления изображения
{% extends 'feedback/base.html' %}
{% block title %} Добавление файла в галерею {% endblock %}
{% block content %}
<h2>Загрузите файл</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" name="image">
<button type="submit">Загрузить</button>
</form>
{% endblock %}
https://docs.djangoproject.com/en/5.0/ref/files/uploads/#django.core.files.uploadedfile.UploadedFile
При отправке форме файлы появляются в request.FILES
. Главное не забыть прописать параметр у формы enctype="multipart/form-data"
, чтобы файлы перевелись в двоичный вид и были отправлены.
request.FILES.get('image'): InMemoryUploadFile # специальный тип данных
Напишем функцию, которая будет принимать данный объект и записывать его в файловую систему:
from werkzeug.utils import secure filename
from uuid import uuid4
class UploadView(View):
@staticmethod
def save_file(file):
filename = str(uuid4()) + "-" + secure_filename(file.name)
with open(file=f"gallery_tmp/{filename}", mode="wb+") as f:
for chunk in file.chunks():
f.write(chunk)
def get(self, request):
form = ImageUploadForm()
return render(request, "gallery/upload.html", {"form": form})
def post(self, request):
file = request.FILES.get("file")
if not file:
return HttpResponseRedirect(reverse("gallery:upload"))
self.__class__.save_file(file)
return HttpResponseRedirect(reverse("gallery:index"))
Записываем не весь файл целиком, а кусками (чанками), чтобы можно было работать с большими файлами.
https://stepik.org/lesson/730393/step/2?discussion=6567371&unit=731895 Довольно важно тут сказать про directory traversal attack и защититься от неё (пример), чтобы те кто следуют курсу потом не выкатили приложение с допущенной этой ошибкой и не перезаписали себе системные файлы :)
- https://ru.stackoverflow.com/questions/587855/%D0%98%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-werkzeug-secure-filename-%D1%81-%D1%80%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%BC%D0%B8-%D1%81%D0%B8%D0%BC%D0%B2%D0%BE%D0%BB%D0%B0%D0%BC%D0%B8
- https://stackoverflow.com/questions/45188708/how-to-prevent-directory-traversal-attack-from-python-code
# forms.py
from django import forms
class ImageUploadForm(forms.Form):
file = formsFileField(
required=True,
)
# views.py
from .forms import ImageUploadForm
class UploadView(View):
@staticmethod
def save_file(file):
filename = str(uuid4()) + "-" + secure_filename(file.name)
with open(file=f"gallery_tmp/{filename}", mode="wb+") as f:
for chunk in file.chunks():
f.write(chunk)
def get(self, request):
form = ImageUploadForm()
return render(request, 'gallery/upload.html', {'form': form})
def post(self, request):
form = ImageUploadForm(
data = request.POST,
files = request.FILES,
)
if nor form.is_valid():
return render(request, 'gallery/upload.html', {'form': form})
self.__class__.save_file(form.cleaned_data['file'])
В модели FileField хранятся только ссылки на файлы, а не сами файлы.
# settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads")
# models.py
from form_project import settings
class ImageModel = (models.Model):
image = models.FileField(
upload_to=settings.MEDIA_ROOT
)
Путь указанный в upload_to
строится от каталога проекта. Если такого пути нет, то он создастся. По этому пути будут сохраняться файлы, которые были переданы в модель, а в базе сохраняться пути на файлы.
Чтобы файлы хранились не в каталоге, а в указанной папке, в settings.py
создается переменная MEDIA_ROOT
и тогда уже в ней будет создаваться каталог, а не в корне проекта.
Если джанго встретятся повторяющиеся имена файлов, то будут добавлены случайные символы к имен файла, чтобы избежать дубликатов. также все пробелы в названии будут заменены на _.
# views.py
class UploadView(View):
def get(self, request):
form = ImageUploadForm()
return render(request, "gallery/upload.html", {"form": form})
def post(self, request):
form = ImageUploadForm(data=request.POST, files=request.FILES)
if not form.is_valid():
return render(request, "gallery/upload.html", {"form": form})
object = ImageModel(image=form.cleaned_data['file'])
object.save()
return HttpResponseRedirect(reverse("gallery:index"))
Обращаясь к полю FileField
мы получаем набор методов, которые поддерживает файл.
- read() - прочитать содержимое
ImageModel.objects.all()[0].image.read()
Можно создать форму загрузки на основе модели и использовать её в при создании вьюхи через класс CreateView
# forms.py
from django import forms
from .models import ImageModel
# class ImageUploadForm(forms.Form):
# file = forms.FileField(
# allow_empty_file=False,
# required=True,
# label="файл",
# )
class ImageUploadForm(forms.ModelForm):
class Meta:
model = ImageModel
exclude = []
# views.py
class UploadView(CreateView):
template_name = 'gallery/upload.html'
model = ImageModel
form_class = ImageUploadForm
# fields = "__all__"
success_url = '/gallery'
# views.py
from django.view.generic import ListView
class IndexView(ListView):
template_name = 'gallery/index.html'
model = ImageModel
context_object_name = 'items'
# не обязательный параметр
paginate_by = 5 # список записей на одной странице
<img src="{{ image.image.url }}" width="100">
У объекта из списка, передаваемого в шаблон есть атрибуты .path
и .url
.
.path
- абсолютный путь по которому файл хранится на сервере. Нету доступа у браузера..url
- путь от корня проекта. Нету доступа по-молчанию. По дефолту есть доступ только к статическим файлам. Как получить доступ?
Где забирать? - Путь по которому джанго будет отдавать статику.
# setting.py
# адрес для браузера, по которому сервер отдаёт статику
MEDIA_URL = "/my-media/"
Что отдавать? В головном файле urls.py
:
# project_folder/project_name/urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Данная функция мапит внешнее расположение MEDIA_ROOT и путь MEDIA_URL Таким образом добавляется новый роут.
Во первых, создаем отдельное приложение.
Предположим назовем его - users
:
python manage.py startapp users
Добавляем его в settings.py
:
# settings.py
INSTALLED_APPS = [
#...,
'users',
]
Подключаем в urls.py
приложение users
# urls.py
urlpatterns = [
#...,
path('', include('users.urls')),
]
Создаем файл urls.py
в папке нового приложения users
.
Создаем только два пути в данном файле:
# urls.py
urlpatterns = [
path('', include('django.contrib.auth.urls')),
path('register/', views.RegisterView.as_view(), name='register'),
]
Создаем шаблоны в данном приложении, для регистрации и логина.
Внимание!
- шаблон для login
нужно расположить в папке users/templates/registration/
Шаблон register
расположить по пути users/templates/users/
Создаем view
# views.py
from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import login
from django.views import View
class RegisterView(View):
def get(self, request):
form = UserCreationForm()
return render(request, 'users/registration.html', {'form': form})
def post(self, request):
form = UserCreationForm(data=request.POST)
if form.is_valid():
new_user = form.save()
login(request, new_user)
return redirect('/requisites') # указываем куда перейдём после регистрации и входа
return render(request, 'users/registration.html', {'form': form})
Регистрация
{% extends 'bitbucket_app/base.html' %}
{% block main %}
<form method="POST" action="{% url 'auth_app:register' %}">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Registration</button>
</form>
{% endblock %}
Логин
{% extends 'bitbucket_app/base.html' %}
{% block main %}
{% if form.errors %}
<p>Ваше имя пользователя и пароль не найдены. Пожалуйста попробуйте еще</p>
{% endif %}
<p>Пожалуйста, введите свое имя пользователя и пароль</p>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Login</button>
</form>
{% endblock %}
# forms.py
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
class SignUpForm(forms.Form):
username = forms.CharField(
max_length=100,
required=True,
widget=forms.TextInput(attrs={
'class': "form-control",
'id': "inputUsername",
'type': 'username',
'placeholder': 'Имя пользователя'
}),
)
password = forms.CharField(
required=True,
widget=forms.PasswordInput(attrs={
'class': "form-control",
'id': "inputPassword",
'type': 'password',
'placeholder': 'Пароль'
}),
)
repeat_password = forms.CharField(
required=True,
widget=forms.PasswordInput(attrs={
'class': "form-control",
'id': "ReInputPassword",
'type': 'password',
'placeholder': 'Повторите пароль'
}),
)
def clean(self):
# super(SignUpForm, self).clean()
password = self.cleaned_data['password']
confirm_password = self.cleaned_data['repeat_password']
if password != confirm_password:
raise forms.ValidationError(
"Пароли не совпадают"
)
def save(self):
user = User.objects.create_user(
username=self.cleaned_data['username'],
password=self.cleaned_data['password'],
)
user.save()
auth = authenticate(**self.cleaned_data)
return auth
В методе save
создается новый пользователь, затем происходит аутентификация этого пользователя методом authenticate
через логин и пароль. И возвращается уже аутентифицированный пользователь auth
.
Метод clean вызывается при валидации формы и получении значений cleaned_data.
При этом он вызывается после метода очистки каждого поля формы. Clean, который вы имеете ввиду относится к полям формы (если бы мы наследовали класс от forms.Field), а мы переписываем метод, относящийся ко всей форме (наследуемся от forms.Form). К моменту вызова метода clean() формы, методы поля - .to_python() .validate() run_validators() совместно с clean()
уже отработали для каждого поля)
Можно в нём явно это указать через super().
# views.py
from django.contrib.auth import login
from .forms import SignUpForm
class SignUpView(View):
def get(self, request)
form = SignUpForm()
return render(request, 'myblog/signup.html', {'form': form})
def post(self, request):
form = signUpForm(data=request.POST)
if form.is_valid():
user = form.save()
if user is not None:
login(request, user)
return HttpResponseRedirect(reverse('myblog:index'))
return render(request, 'myblog/singup.html', {'form': form})
При сохранении формы у нас происходит создание нового пользователя и его аутентификация. Если создание прошло успешно и удалось аутентифицировался, то пользователь возвращается из form.save()
и происходит его логин.
Документация mdn: https://developer.mozilla.org/ru/docs/Learn/Server-side/Django/Authentication
А так логин выглядит похоже на регистрацию, только проще.
- Создаём форму для логина
# forms.py
class LoginForm(forms.Form):
username = forms.CharField(
max_length=100,
required=True,
widget=forms.TextInput(
attrs={
"name": "username",
"type": "text",
"class": "form-control",
"placeholder": "Имя пользователя",
}
),
)
password = forms.CharField(
max_length=100,
required=True,
widget=forms.TextInput(
attrs={
"name": "password",
"type": "password",
"class": "form-control",
"placeholder": "Пароль",
}
),
)
- Добавляем представление views
# views.py
class LoginView(View):
def get(self, request, *args, **kwargs):
form = LoginForm()
return render(request, "myblog/signin.html", {"form": form})
def post(self, request, *args, **kwargs):
form = LoginForm(data=request.POST)
if form.is_valid():
user = authenticate(**form.cleaned_data)
# user = authenticate(
# request,
# username=form.cleaned_data.get('username'),
# password=form.cleaned_data.get('password')
# )
if user is not None:
login(request, user)
return HttpResponseRedirect(reverse("myblog:index"))
else:
form.add_error(None, "Неправильный пароль или логин")
return render(request, "myblog/signin.html", {"form": form})
- Добавляем роут в
urls.py
- Шаблон
<form action="{% url 'myblog:login' %}" method="POST" class="d-flex flex-column gap-3 col-md-6 col-12">
{% csrf_token %}
{% for field in form %}
{% for error in field.errors %}
<div class="alert alert-danger">{{ error|escape }}</div>
{% endfor %}
{{ field}}
{% endfor %}
<input type="submit" class="col-auto btn btn-primary" value="Отправить">
</form>
Ну в общем сейчас нельзя использовать get запрос с LogoutView
, Джанго запрещает. Можно заменить на простенькую функцию во views.
# urls.py
urlpatterns = [
...,
path('logout/', LogoutView.as_view(), name='logout'),
]
# settings.py
LOGOUT_REDIRECT_URL = "/"
Собственная вьюха:
# views.py
from django.contrib.auth import logout
class LogoutView(View):
def get(self, request):
logout(request)
HttpResponseRedirect(settings.LOGOUT_URL_REDIRECT)
https://docs.djangoproject.com/en/5.0/topics/auth/default/
Можно сделать кнопку активной если мы находимся на странице - можно сравнить текущий путь пользователя с тем, что нам нужен. Текущий путь пользователя есть в request.get_full_path
Проверить авторизован ли пользователь можно через
{% if user.is_authenticated %}
...
{% endif %}
Данные аттрибуты передаются автоматически в шаблон. Данные шаблоны задаются в файле settings.py
через переменную TEMPLATES
. В ней есть ключ context_processors
, благодаря этому мы и можем пользоваться объектом пользователя и реквестом.
# setting.py
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
https://docs.djangoproject.com/en/5.0/topics/email/#module-django.core.mail
создаем форму и вьюху
from django.core.mail import send_mail, BadHeaderError
class ContactsView(View):
def get(self, request):
form = ContactsForm()
return render(request, "myblog/contacts.html", {"form": form})
def post(self, request):
form = ContactsForm(data=request.POST)
if form.is_valid():
data = form.cleaned_data
try:
send_mail(
f"От {data.get('name')} [{data.get('email')}]: {data.get('subject')}",
message=data.get("content"),
from_email=data.get("email"),
recipient_list=["admin@admin.com"],
)
except BadHeaderError:
return HttpResponse("Неправильный заголовок")
return HttpResponseRedirect(reverse("myblog:thanks"))
return render(request, "myblog/contacts.html", {"form": form})
Можно не отправлять емеилы, а форму сохранять в модель и помещать сообщения в базу данных.
# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST_USER = 'Ваша почта'
EMAIL_HOST_PASSWORD = 'Пароль который вы только что получили'
Позволяет редактировать текст в джанго сразу с учётом разметки как в WYSIWYG редакторе. https://github.com/django-ckeditor/django-ckeditor
Устанавливаем
pip3 install django-ckeditor
Добавляем в установленные приложения
# setting.py
INSTALLED_APPS = [
...,
'ckeditor',
'ckeditor_uploader',
]
...
MEDIA_URL = 'media/'
MEDIA_ROOT = os.path.join(BAE_DIR, 'media')
####################################
## CKEDITOR CONFIGURATION ##
####################################
CKEDITOR_UPLOAD_PATH = 'uploads/'
CKEDITOR_IMAGE_BACKEND = "pillow"
CKEDITOR_CONFIGS = {
"default": {
"removePlugins": "stylesheetparser",
'allowedContent': True,
'toolbar_Full': [
['Styles', 'Format', 'Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ],
['Image', 'Flash', 'Table', 'HorizontalRule'],
['TextColor', 'BGColor'],
['Smiley','sourcearea', 'SpecialChar'],
[ 'Link', 'Unlink', 'Anchor' ],
[ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock', '-', 'BidiLtr', 'BidiRtl', 'Language' ],
[ 'Source', '-', 'Save', 'NewPage', 'Preview', 'Print', '-', 'Templates' ],
[ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo' ],
[ 'Find', 'Replace', '-', 'SelectAll', '-', 'Scayt' ],
[ 'Maximize', 'ShowBlocks' ]
],
}
}
###################################
Добавляем ckeditor в urls.py
# project_app/urls.py
urlpatterns = [
...,
path('ckeditor/', include('ckeditor_uploader.urls')),
] + static(settings.MEDIA_URL, document.root=settings.MEDIA_ROOT)
Теперь добавим изменения в модель в которой будет текст
# models.py
...
from ckeditor_uploader.fields import RichTextUploadingField
class Post(models.Model):
...
description = RichTextUploadingField()
content = RichTextUploadingField()
github: https://github.com/jazzband/django-taggit
docs: https://django-taggit.readthedocs.io/en/latest/
pip install django-taggit
Добавляем в setting.py
# settings.py
INSTALLED_APPS = [
...,
'taggit',
]
...
# опция нечувствительности к регистру
TAGGIT_CASE_INSENSITIVE = True
Добавим(изменим) специальное поле для тагов в модели
# models.py
from taggit.managers import TaggableManager
class Post(models.Model):
...
tag = TaggableManager()
Это специальное поле, в котором будет храниться список тагов, получаемых из строку путем её разделения по пробелам или запятым.
Для того, чтобы отобразить список тэгов в админке
# admin.py
class PostModelAmdin(admin.ModelAdmin):
list_display = ['tag_list']
def tag_list(self, obj):
return u", ".join(o.name for o in object.tag.all())
{% for tag_name in post.tag.names %}
<li>#{{tag_name}}</li>
{% endfor %}
или
{% for tag in post.tag.all %}
<a href="{% url 'tag' slug=tag.slug %}">#{{ tag }}</a>
{%% endfor }
# views.py
from taggit.models import Tag
class TagView(View):
def get(self, request, slug):
# ищем запись в таблице Tag по слагу
tag = get_object_or_404(klass=Tag, slug=slug)
# ищем все записи с данным тагом по внешнему ключу
posts = PostModel.objects.filter(tag=tag)
# список самых популярных тагов
common_tags = Post.tag.most_common()
...
https://github.com/glemmaPaul/django-taggit-serializer
Для работы taggit
с drf нужен taggit-serializer
:
pip3 install django-taggit-serializer
добавляем в settings.py
# settings
INSTALLED_APPS = [
...,
'taggit',
'taggit_serializer',
]
ImportError: cannot import name ‘ugettext_lazy’ from ‘django.utils.translation’
Вылечилась заменой from django.utils.translation import ugettext_lazy as _
на from django.utils.translation import gettext_lazy as _
в файле myvenv\lib\site-packages\taggit_serializer\serializers.py
.
REST (Representational State Transfer — «передача состояния представления») — архитектурный стиль взаимодействия компонентов распределённого приложения в сети. REST представляет собой согласованный набор ограничений, учитываемых при проектировании распределённой гипермедиа-системы. В определённых случаях (интернет-магазины, поисковые системы, прочие системы, основанные на данных) это приводит к повышению производительности и упрощению архитектуры.
django-admin startproject api
cd api
python manage.py startapp core
вставляем в settings py:
INSTALLED_APPS = [
...,
"core",
]
LANGUAGE_CODE = "ru-ru"
TIME_ZONE = "Europe/Moscow"
STATIC_URL = "/static/"
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
создаём миграции и супервользователя
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
Установим
- https://www.django-rest-framework.org/ - сам фреймворк
- https://django-rest-framework-simplejwt.readthedocs.io/en/latest/ - simplejwt
- https://github.com/adamchainz/django-cors-headers - cors headers
pip install djangorestframework
pip install djangorestframework-simplejwt
pip install django-cors-headers
После установки добавим rest_framework
в INSTALLED_APPS
и остальные настройки
from datetime import timedelta
INSTALLED_APPS = [
'core',
'rest-framework',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware', # <- добавьте это именно сюда
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# ниже настройки JWT токена
LOGIN_URL = "/api/v1/signin"
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
"REFRESH_TOKEN_LIFETIME": timedelta(days=2),
}
# CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = ["http://localhost:3000", "http://127.0.0.1:3000"]
# конец настроек JWT токена
# настройки rest framework
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication"
],
"DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"],
"TEST_REQUEST_DEFAULT_FORMAT": "json",
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.DjangoModelPermissions",
),
}
# конец настроек rest-framework
django-cors-headers - данная библиотека позволяет обращаться к вашему django api из других доменов.
Тут суть в том, что на одной локальной машине сразу 2 приложения запущено на разных портах (это как будто 2 совершенно разных адреса). Одно по адресу http://127.0.0.1:8000/
с портом 8000 (назовем его api
) и второе по адресу http://127.0.0.1:8080/
. Так вот по умолчанию браузер не даст вам обратиться от одного к другому и вы в консоли увидите такую ошибку.
Для того чтобы их связать нужен Access-Control-Allow-Origin заголовок. Именно его и обеспечивает данная библиотека.
У нас на 3000 порту будет запускаться фронтэнд, который должен обращаться к api.
https://django-rest-framework-simplejwt.readthedocs.io/en/latest/getting_started.html
# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.views import TokenRefreshView
urlpatters = [
path('api/token/', TokenObtainPairView.as_view(), name='token'),
path('api/token_refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
На url api/token/
пользователь будет отправлять логин и пароль и получать jwt токен для авторизации.
Адрес api/token_refresh/
отвечает за обновление токена, согласно настройкам времени жизни токена в секции settings.SIMPLE_JWT
.
Система генерирует два токена access
и refresh
. У каждого своё время жизни. Обычно у рефреш токена она длиннее. Когда время жизни access
токена подходит к концу, система отправляет запрос на адрес api/refresh_token
отправляя в поле авторизации токен refresh
и получает обновленный access
токен.
# models.py
from django import models
class Post(models.Model):
pass
https://habr.com/ru/companies/yandex_praktikum/articles/561696/
https://www.django-rest-framework.org/api-guide/serializers/
Сериализаторы нужны для преобразования моделей их бд в json и обратно
# app_name/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from taggit_serializer.serializers import TaggitSerializer
from taggit_serializer.serializers import TagListSerializerField
from .models import Post
class PostSerializer(TaggitSerializer, serializers.ModelSerializer):
tags = TagListSerializerField()
author = serializers.SlugRelatedField(
slug_field="username",
queryset=User.objects.all()
)
class Meta:
model = Post
# fields = "__all__" # или так
fields = (
"id",
"title",
"description",
"content",
"image",
"created_at",
"author",
"tags",
)
lookup_field = 'slug'
extra_kwargs = {
'url': {'lookup_field': 'slug'}
}
в объекте rest_framework.serializers
находятся разные вариации сериализаторов.
ModelSerializer
- класс сериализатора на основе класса модели.
Создание сериализатора очень похоже на создание формы на основе модели.
PostSerializer
- наш класс сериализатора на основе TaggitSerializer
и serializers.ModelSerializer
.
Порядок классов от которых наследуемся тут важен.
tags = TaggitSerializerField()
- добавляет поле специального типа
Для поля автора, если мы будем получать просто данные из модели то мы получим в ответ id
объекта. А нам нужен username
. Это так называемые вложенные отношения,когда у нашей модели есть поля, которые ссылаются на другие модели https://www.django-rest-framework.org/api-guide/relations/#nested-relationships.
author = serializers.SlugRelatedField(
slug="username", queryset=User.objects.all()
)
В extra_kwargs для каждого поля задаётся набор опций.
lookup_field
определяет имя для поля, по которому будет искаться конкретная запись.
По умолчанию поиск ведётся по id
, но мы изменим его на slug
lookup_field = 'slug'
extra_kwargs = {
'url': {'lookup_field': 'slug'},
}
Ключами выступают любые поля не запрещенные в (exclude) поля модели.
Значение для каждого ключа - словарь с атрибутами, которыми нужно дополнить то или иное поле сериализатора.
Т.е. тут мы создаем в сериализаторе новое генерирумое поле url
, которое получается из поля slug
. Т.е. фактически получается переименования поля slug в url.
Мы обращаемся по какому-то url
к нашему api, этот url
обрабатывает какая-nj вьюха. А во вьюхе определён сериализатор, которые берёт данные из базы данных в соответствии с настройками сериализатора возвращает нам json.
Дополнительные свойства
write_only
- поле используется только при валидации, т.е. при получении и проверки данных. При отправки данных пользователю данное поле не фигурирует.
Есть несколько способов создавать вьюхи
- class Based View (https://www.django-rest-framework.org/tutorial/3-class-based-views/#tutorial-3-class-based-views)
- функция с декоратором
@api_view
- на основе ViewSets
view на основе viewsets.ModelViewSet позволяет создавать не только роуты для одного экземпляра сущности, но сразу и роут для списка элементов. Таким образом через ViewSet можно получить как один элемент (один Post), так и сразу несколько постов.
class PostViewSet(viewsets.ModelViewSet):
Далее мы указываем сериализатор для работы с моделью Post.
serializer_class = PostSerializer
И определяем queryset из множества, которое будем возвращать. И указываем поле, по которому будет происходить поиск модели в queryset
lookup_field = 'slug'
Итого
# app_name/views.py
from rest_framework import viewsets
from rest_framework.response import Response
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
queryset = Post.objects.all()
lookup_field = 'slug'
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Post
from .serializers import PostSerializer
class Post(APIView):
def get(self, request):
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True)
return Response({"posts": serializer.data})
from rest_framework.decorators import api_view
from rest_framework.decorators import permission_classes
from rest_framework import permissions
@api_view(["GET"])
@permission_classes([permissions.AllowAny])
def get_index(request):
return Response({"message": "Hello, api_view decorator index!"})
Наследуется от класса APIView
и добавляет обычный функционал для списка и детального вида.
from rest_framework.generics import APIView
https://www.django-rest-framework.org/api-guide/generic-views/#genericapiview
Basic settings:
queryset
- The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the get_queryset() method. If you are overriding a view method, it is important that you call get_queryset() instead of accessing this property directly, as queryset will get evaluated once, and those results will be cached for all subsequent requests.serializer_class
- The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the get_serializer_class() method.lookup_field: str
- The model field that should be used for performing object lookup of individual model instances. Defaults to 'pk'. Note that when using hyperlinked APIs you'll need to ensure that both the API views and the serializer classes set the lookup fields if you need to use a custom value.lookup_url_kwarg: str
- 'pk' - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as lookup_field.
Pagination:
pagination_class
- The pagination class that should be used when paginating list results. Defaults to the same value as the DEFAULT_PAGINATION_CLASS setting, which isrest_framework.pagination.PageNumberPagination
. Settingpagination_class=None
will disable pagination on this view.
Filtering:
filter_backends
- A list of filter backend classes that should be used for filtering the queryset. Defaults to the same value as the DEFAULT_FILTER_BACKENDS
setting.
https://www.django-rest-framework.org/api-guide/generic-views/#listapiview
from rest_framework.generics import ListAPIView
пригодится, когда надо вывести список объектов, например результаты поиска Наследуется от GenericApiView и реализует функционал метода get по отношению к списку элементов.
Всю магию работы с роутами берёт на себя DefaultRouter.
# app_name/urls.py
from django.urls import path
from django.urls import include
from rest_framework.routers import DefaultRouter
from .views import PostViewSet
router = DefaultRouter()
router.register('posts', PostViewSet, basename='posts')
urlpatterns = [
path("", include(router.urls),)
]
можно тестировать при помощи Talend API tester - расширения для браузера или Постмана или Thunder Client (VSCode plugin).
Сначала делаем post запрос на http://127.0.0.1:8000/api/token
- путь для получения токена.
В ответ получим, что нам нужен обязательно username и password.
Хорошо, отправляем в теле запроса логин и пароль.
В итоге в ответе от сервера получим и токен доступа и токен для обновления access
и refresh
:
{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcwOTE1NTYyNiwiaWF0IjoxNzA4OTgyODI2LCJqdGkiOiJhZTk1YzQ4MTZlYmU0YTg4YjM0YWYzYjYwNDNlNjk1OCIsInVzZXJfaWQiOjF9.wsmd28lpW_OLtqMshCkn9rf1_krS4xQS8jS0PWjvGDI",
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA4OTg2NDI2LCJpYXQiOjE3MDg5ODI4MjYsImp0aSI6ImZhMDJmNzNkZDUxNTRjOGY4MTZhYzE1Y2JiODlkNjY1IiwidXNlcl9pZCI6MX0.iNUdgmwco8N6QMx4Mv_naCdR8Ey2M9lMvcaBIg1C2HY"
}
В постмане можно писать скрипты в разделе Tests, которые заполняет созданные переменные.
const requests = pm.request.json();
pm.environment.set('access', request.access);
pm.environment.set('refresh', request.refresh);
Таким образом после выполнения запроса по получению двух токенов их можно установить в переменные окружения env и затем использовать в проектах.
https://github.com/Ulbwaa/YandexImagesParser
from datetime import datetime
from YandexImagesParser.ImageParser import YandexImage
from bs4 import BeautifulSoup as bs
from random import randint, sample
from transliterate import translit
import os
import django
import requests
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Django_blog_project_REST_API.settings")
django.setup()
from django.contrib.auth.models import User
from core.models import Post
list_abstract = []
parser = YandexImage()
images = iter(parser.search("astronomy", sizes=parser.size.medium))
while len(list_abstract) < 20:
URL_TEMPLATE = f"https://yandex.ru/referats/?t=astronomy&s={randint(10000, 99999)}"
r = requests.get(URL_TEMPLATE)
if r.status_code != 200:
continue
soup = bs(r.text, "html.parser")
referat = soup.find('div', class_='referats__text')
text = referat.find_all('p')
img_b = ''
refer_image = images.__next__()
while True:
if refer_image.url[-4:] != '.jpg':
refer_image = images.__next__()
continue
try:
img_b = requests.get(refer_image.url)
except:
refer_image = images.__next__()
continue
break
with open(f'media/{str(refer_image.url).split("/")[-1]}', 'wb') as img:
img.write(img_b.content)
list_abstract.append(
{
'h1': referat.find('strong').get_text()[7:-1],
'title': referat.find('strong').get_text()[7:-1],
'slug': translit(referat.find('strong').get_text()[7:-1], language_code='ru', reversed=True).replace(
' ',
'-'),
'description': text[0].get_text(),
'content': ''.join([p.get_text() for p in text]),
'created_at': str(datetime.now().date()),
'image': str(refer_image.url).split('/')[-1],
'author': User.objects.get(username='root'),
}
)
tags_list = ['astronomy', 'asteroid', 'dark matter', 'gas giant', 'hypernova', 'mass', 'nova', 'meteor', 'pulsar', 'planetoid']
for post in list_abstract:
b = Post(**post)
b.save()
b.tags.add(*sample(tags_list, 2))
b.save()
https://www.django-rest-framework.org/api-guide/permissions/
На нашем сайте не нужно авторизоваться, чтобы получить список постов. Хотя по-умолчанию апи запрашивает авторизацию. Надо к вьюхе добавить список прав, чтобы любой пользователь могу получить доступ к нейЖ
# app_name/views.py
from rest_framework import viewsets
from rest_framework import permissions
from .serializers import PostSerializer
from .models import Post
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
queryset = Post.objects.all()
lookup_field = 'slug'
permission_classes = [permissions.AllowAny]
IsAuthenticatedOrReadOnly
AllowAny
IsAuthenticated
IsAdminUser
DjangoModelPermissions
- ...
Можно прописать дефолтные разрешения для всех вью сразу в файле settings.py
(по умолчанию и применяется AllowAny, если нет иного значения)
# setting.py
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny",]
}
from rest_framework.pagination import PageNumberPagination
class PageNumberSetPagination(PageNumberPagination):
page_size = 4
page_size_query_name = "page_size"
ordering = "created_at"
class Posts(viewsets.ModelViewSet):
serializer_class = PostSerializer
queryset = Post.objects.all()
lookup_field = "slug"
permission_classes = [permissions.AllowAny]
pagination_class = PageNumberSetPagination
После этого будет доступно два параметра для этой вь.хи - page
и page_size
.
# urls.py
urlpatterns = [
...,
path("tag/<slug:slug_key>/", TagView.as_view(),)
]
# views.py
from rest_framework import permissions
from rest_framework.generics import ListAPIView
from .models import Post
from .serializers import PostSerializer
class PageNumberSetPagination:
...
class TagView(ListAPIView):
serializer_class = PostSerializer
permission_classes = [permissions.AllowAny]
pagination_class = PageNumberSetPagination
def get_queryset(self):
slug_text = self.kwargs.get("slug_key")
if slug_text is None:
return []
tag = Tag.objects.get(slug=slug_text.lower())
posts = Post.objects.filter(tags=tag)
return posts
Здесь мы переопределяем стандартный метод get_queryset
.
Создадим сериализатор
#serializers.py
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
lookup_field = "name"
fields = ("name", "slug",)
extra_kwargs = {"url": {"lookup_field": "name"}}
Cоздадим вьюху, которая будет отдавать список тэгов через сериализатор
# views.py
class TagListView(ListAPIView):
serializer_class = TagSerializer
permissions = [permissions.AllowAny]
queryset = Tag.objects.all()
Создадим роут
urlpatterns = [
...,
path("tags/", TagListView.as_view(), name="tags")
]
Эта api будет использоваться для вывода последних 5ти постов. Можно явно указать что не будет пагинации. Но все равно выводим всего 5 записей.
class AsideView(ListAPIView):
serializer_class = PostSerializer
permission_classes = [permission.AllowAny]
# pagination_class = None
queryset = Post.objects.all().order_by("-created_at")[:5]
Добавим роут и готово.
Для создания формы обратной связи нужно создать почтовый ящик и разрешить через него отправку писем. Можно посмотреть мануал настройки gmail по ссылке.
После этого надо добавить в файл settings.py
следующие константы
# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com' # smtp сервер
EMAIL_USE_TLS = True
EMAIL_PORT = 587 # smtp порт
EMAIL_HOST_USER = 'Ваша почта' # логин от почты
EMAIL_HOST_PASSWORD = 'Пароль который вы только что получили' # пароль
!Важно Нужно производить валидацию всех данных, присланных от пользователя. Тут это будет опущено.
Его можно создать на основе класса ModelSerializer, если есть модель. Предположим, что мы будем сохранять все отзывы в бд, а потом их отправлять. Тогда создадим также и модель для бд:
# serializers.py
class FeedbackSerializer(serializers.Serializer):
name = serializers.CharField(max_length=50)
email = serializers.EmailField()
subject = serializers.CharField(max_length=100)
message = serializers.CharField(max_length=300)
created_at = serializers.DateTimeField(read_only=True)
# models.py
class Feedback(models.Model):
name = models.CharField(max_length=50)
email = models.EmailField()
subject = models.CharField(max_length=100)
message = serializers.TextField()
created_at = serializers.DateTimeField(auto_now_add=True)
# views.py
from django.core.mail import send_mail
from rest_framework.response import Response
class FeedbackView(APIView):
permission_classes = [permissions.AllowAny]
serializer_class = Feed
def post(self, request):
serializer = FeedbackSerializer(request.data)
if serializer.is_valid():
feedback = Feedback.objects.create(**serializer.validated_data)
name = feedback.get("name")
from_email = feedback.get("email")
subject = feedback.get("subject")
message = feedback.get("message")
send_mail(
f"От {name} | {subject}",
message,
from_email,
['adminemail@admin.com']
)
return Response(
{
"status": "success",
"data": FeedbackSerializer(feedback).data,
}
)
return Response({"status": "error", "message": "wrong data"})
https://www.django-rest-framework.org/api-guide/filtering/
Хотя можно выполнять фильтрацию вручную, у DRF есть замечательные фильтры.
Фильтры можно использовать с классами GenericAPIView.
Можно написать класс на основе APIView, который будет получать из response query параметр и выдавать ответ:
class SearchPost(APIView):
def get(self, request):
query = self.request.query_params.get("q")
if query is None:
return Response([])
posts = Post.objects.filter(
Q(title__iregex=query)
| Q(content__iregex=query)
| Q(author__username__iregex=query)
)
return Response(PostSerializer(posts, many=True).data)
https://www.django-rest-framework.org/api-guide/filtering/#searchfilter
Можно добавить несколько параметров уже существующей вьюхе на основе ModelViewSet.
# views.py
from rest_framework import filters
from rest_framework import viewsets
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
queryset = Post.objects.all()
lookup_field = "slug"
permission_classes = [permissions.AllowAny]
pagination_class = PageNumberSetPagination
search_fields = ["$title", "$content", "$author__username"]
filter_backends = [filters.SearchFilter]
Здесь SearchFilter
- это фильтр, который ищет по query запросам https://www.django-rest-framework.org/api-guide/filtering/#searchfilter.
Для поиска по-умолчанию используется ключевое слово search
. Чтобы его изменить нужно в settings.py внести изменения:
# settings.py
REST_FRAMEWORK = {
'SEARCH_PARAM': 'q'
}
Поведение поиска может быть ограничено добавлением различных символов к search_fields.
^
Начинается с поиска.=
Точные совпадения.@
Полнотекстовый поиск. (В настоящее время поддерживается только серверная часть Django PostgreSQL .)$
Поиск по регулярному выражению. Погуглив немного, пришел к выводу, что добавив$
, можно решить такую проблему:
search_fields = ['$content', '$h1']
- тогда проблема регистра решена и поиск отдает результаты с любым регистром.
Для создания регистрации надо создать сериалайзер, вьюху для регистрации и url.
# serializers.py
from django.contrib.auth.models import User
from rest-framework import serializers
class RegisterSerializer(serializers.ModelSerializer):
password2 = serializers.CharField(
max_length=128,
min_length=6,
write_only=True,
required=True,
)
class Meta:
model = User
fields = ('username', 'email', 'password', 'password2')
extra_kwargs = {
'password': {'write_only': True},
'email': {'required': True},
}
def create(self, validated_data):
username = validated_data.get("username")
email = validated_data.get("email")
password = validated_data.get("password")
password2 = validated_data.get("password2")
if password != password2:
raise serializers.ValidationError({
'password': "Passwords do not match"
})
user = User(
username=username,
email=email,
)
user.set_password(password)
user.save()
return user
Здесь создаем сериализатор, Который принимает дополнительное поле password2, являющееся обязательным. Затем для полей паролей выключается сериализация (остаётся только валидация write_only=True). И добавляется условие обязательности для поля email
.
Создаем в сериализаторе отдельный метод create
, который будем вызывать во вьюхе для создания записи в базе данных.
Если мы захотим создать сериализатор для вывода вех данных пользователя, то наш сериализатор будет выглядеть вот так:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
class UserView(ListAPIView):
serializer_class = UserSerializer
permission_classes = [permissions.isAdminUser]
queryset = User.objects.all()
# views.py
from rest_framework import generics
from rest_framework.response import Response
from rest_framework import permissions
class RegisterView(generics.GenericAPIView):
serializer_class = RegisterSerializer
permission_classes = [permissions.AllowAny]
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=self.request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
return Response(
{
"user": UserSerializer(
user, context=self.get_serializer_context()
).data,
"message": "пользователь успешно создан",
}
)
serializer.is_valid(raise_exception=True)
- проверяет данные на валидность и в случае ошибки появляется исключение.
или мой вариант на APIView
# views.py
class RegisterView(APIView):
def post(self, request):
serializer = RegisterSerializer(data=self.request.data)
if serializer.is_valid():
serializer.create(validated_data=serializer.validated_data)
return Response(serializer.data)
return Response(serializer.errors)
Для создания комментариев к постам нужно создать модель комментария
#models.py
# views.py
class UserProfileView(generic)
https://realpython.com/getting-started-with-django-channels/
https://testdriven.io/blog/django-and-celery/#workflow
version: "3"
services:
redis:
docker pull redis:7.2.4-alpine3.19