DRF, часть 1 - Проектирование структуры API
Введение
Раз уж у меня не получается написать полноценную огромную статью про Django REST Framework, следует публиковать хотя бы небольшие заметки. Начать следует с прописных истин.
- 1. Дуб - дерево.
- 2. Олень - животное.
- 3. Смерь - неизбежна.
- 4.
api
- отдельное приложение в нашем проекте - 5. Следует придерживаться общепринятых правил именования частей API
- 6. API должен быть версионным
- 7. API должен быть простым
С первыми тремя пунктами всё ясно, поэтому перейду сразу к четвёртому и нижеследующим.
Структура каталогов и версионность
Сначала я пытался запихнуть API в разные части основного проекта, создавал в каталоге с приложениями файлы api.py
, serializers.py
и permissions.py
.
О том, насколько это плохая идея, я узнал почти сразу же, когда начал путаться с тем, что и где лежит. Стоило только переименовать один из каталогов, как сразу же всё начинало сыпаться. В итоге я пришёл к тому, что API - не просто отдельное приложение, содержащее только файлы __init__.py
и urls.py
(со ссылками на нужные файлы в приложениях Django), а полноценный модуль со множеством вложенных модулей. В общем, почувствуйте разницу:
/api/
__init__.py
urls.py
/news/
__init__.py
admin.py
api.py
models.py
serializers.py
tests.py
urls.py
views.py
/comments/
__init__.py
admin.py
api.py
models.py
serializers.py
tests.py
urls.py
views.py
/api/
/v1/
/news/
/comments/
__init__.pt
api.py
permissions.py
serializers.py
urls.py
__init__.py
api.py
permissions.py
serializers.py
urls.py
__init__.py
urls.py
/core/
/comment/
__init__.py
api.py
permissions.py
serializers.py
urls.py
/news/
/comments/
__init__.py
admin.py
models.py
tests.py
views.py
__init__.py
admin.py
models.py
tests.py
views.py
Надеюсь, структура понятна. Во-первых, всё, что связано с API, переехало в одноимённое приложение. Во-вторых, API теперь поддерживает версионность. Для этого нужно всего ничего - создать соответствующие каталоги и правильно описать файл urls.py
в самом начале. Я сделал так:
from django.conf.urls import include
from django.conf.urls import url
urlpatterns = [
url(r'', include('api.v2.urls')),
url(r'^v1/', include('api.v1.urls')),
url(r'^v2/', include('api.v2.urls')),
]
Естественно, где-то в главном файле urls.py
есть строка, в которой конфигурация URL для API присоединяется к общей конфигурации URL через всё тот же include()
.
Как видно из этого файла, если пользователь нашего API не указывает версию, он использует последнюю доступную. В то же время, при необходимости можно указать любую из имеющихся. В одной из статей я видел тезис, согласно которому компании, которые заботятся о своих клиентах и хотят сделать свой сервис успешным, крайне редко или вообще никогда не меняют API. Естественно, не меняеть его вообще никогда вряд ли получится, но можно сделать различия минимальными или предоставить возможность в течение длительного времени использовать старый API, т.к. разработчикам интересней писать что-то новое, а не переписывать старое только потому, что теперь какой-то метод в нашем API перестал работать.
Именование API
Сначала хочу сказать, как делать НЕ НАДО:
http://example.org/api/
Это что угодно, но не API. К сожалению, по ночам мне всё ещё снятся кошмары, в которых я вижу, как на одном из сайтов общение с сервисами сделано именно так.
GET /api/news/get/?id=1 - получить новость с id=1
GET /api/news/get/all/ - получить все новости
POST /api/news/add/ - добавить новость
POST /api/news/update/?id=1 - обновить новость с id=1
POST /api/news/delete/?id=1 - удалить новость с id=1
GET /api/news/?id=1 Получение записи с id=1
POST /api/news/?id=1?action=update Обновление записи с id=1
POST /api/news/?id=1?action=delete Удаление записи с id=1
Это API? Конечно же, нет! Суть REST-сервисов в том, что требуемое действие определяется HTTP-заголовком!
URL - /api/news/:id
GET /api/news/ - получить список всех новостей
POST /api/news/ - создать новость
GET /api/news/12/ - вернёт новость с id=12
PATCH /api/news/12/ - обновить новость с id=12
DELETE /api/news/12/ - удалить новость с id=12
Разница, как говорится, налицо.
В Django REST Framework обновление записи может быть произведено двумя способами - полностью или частично. В первом случае вызывается запрос с заголовком PUT
, во втором - PATCH
.
При полном обновлении сериализатор проверяет заполнение всех полей модели, у которых не указаны свойства null=True
. Не будем забывать, что для модели пользователя обязательными для заполнения являются поля "Пароль" и "Имя пользователя". Но мы же всего лишь хотели указать новую дату рождения! Почему DRF проверяет все поля? Потому что надо было вызывать метод PATCH
.
Теперь пора поговорить, как делать лучше (моё мнение по данному вопросу актуально только на момент написания статьи и в будущем может быть пересмотрено).
/api/articles/:id/ - статьи
/api/friends/:id/ - друзья
/api/news/:id/ - новости
/api/users/:id/ - пользователи
/api/videos/:id/ - видео
GET /api/acticles/:id/comments/ - получить комментарии к статье
GET /api/news/:id/comments/ - получить комментарии к новости
POST /api/articles/:id/comments/ - добавить комментарий к статье
POST /api/news/:id/comments/ - добавить комментарий к новости
DELETE /api/comments/articles/:id/ - удалить комментарий к статье
DELETE /api/comments/news/:id/ - удалить комментарий к новости
Сериализаторы и всё остальное.
Я считаю, что при написании новой версии API могут измениться поля, с которыми работают сериализаторы, поэтому для каждой версии их лучше создавать заново. Кроме того, сами методы для работы с данными могут стать другими. Отсюда следует простой вывод (до которого мне пришлось доходить своим умом пару месяцев):
Простота API - почти недостижимый идеал. Чтобы сделать его простым, придётся проделать очень сложную работу. Не удивляйтесь. Кнопка включения на корпусе компьютера - очень простой интерфейс, однако, по одному нажатию на неё выполняется огромное количество самых разнообразных и сложных действий, на глубокое понимание которых некоторые люди тратят большую часть своей жизни. Таким же должен быть и ваш API - всегда оставаться простым для внешнего наблюдателя, не смотря на то, какая бы сложная работа не происходила внутри системы.
Итоги
- Разработчики хотят работать с сущностями, а не с URL. Сделайте интуитивно понятными все URL вашего API.
- Разработчики ленивы и не хотят изучать 34 параметра, влияющие на результат вызова одного единственного метода (из 56, имеющихся в наличии). Старайтесь избегать реализации поведения API через параметры.
limit
,offset
,filter
иsorting
- необходимое зло, от них никуда не деться. - Действие, выполняемое методом API, должно зависеть от HTTP-заголовка.
GET
для чтения,POST
для создания,PUT/PATCH
для обновления иDELETE
для удаления записей. - Пишите документацию к вашему API. Даже самая лучшая структура URL для API неспособна описать различные тонкости и нюансы его работы.
CSRFToken в Angular 1.4
В новом Angular 1.4 одно из приятных изменений - возможность задавать куку и заголовок для CSRF-токена. Раньше делали так:
angular.module('app', ['ngCookies']).run(['$http', '$cookies', function($http, $cookies){
var token = $cookies.csrftoken;
$http.defaults.headers.common['X-CSRFToken'] = token;
$http.defaults.headers.post['X-CSRFToken'] = token;
}]);
Сейчас на этапе конфигурирования приложения можно сделать такой финт:
angular.module('app', []).config(['$httpProvider', function($httpProvider){
$httpProvider.defaults.xsrfCookieName = 'csrftoken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
}]);
Оригинальная документация - здесь. Проверял работу на Django 1.8.
EMACS - начало
Введение
Я не буду здесь рассказывать в деталях, чем плоха та или иная IDE или чем мне не угодили VIM и Sublime Text. Начнём лучше с того, чем EMACS хорош.
EMACS хорош в первую очередь тем, что это не просто текстовый редактор, а настоящий конструктор, из которого можно сделать как полноценную IDE для разработки практически на любом языке, так и издательскую систему или текстовый процессор. Основная фишка - возможность расширять возможности редактора путём написания сценариев на языке EMACS Lisp. К счастью, всё уже написано до нас, и лично мне ни разу не приходилось писать что-то своё, однако при этом я смог превратить EMACS в IDE для WEB- и Python-разработки. Да, эту статью я тоже пишу в нём.
Во-вторых, он быстр. Он не требует JRE для своей работы, написан на C и EMACS Lisp, запускается и работает очень быстро.
Это свободное ПО, что для обычного российского пользователя означает как раз "Free beer".
Он не перегружен меню, диалоговыми окнами, кучей разных панелей и т.д. Лаконичный интерфейс легко может быть расширен или сведён к пугающему минимализму - выбор за вами.
Огромная база готовых пакетов. Там есть практически всё, что нужно, от поддержки редких языков до веб-браузера и игр. Серьёзно!
Установка
Установка в Windows предельно проста - нужно зайти на этот сайт и скачать архив с последней версией. Распаковываем её в любое нужное место, находим в каталоге bin файл runemacs.exe и создаём для него ярлык на Рабочем столе.
Установка в Linux может быть чуть сложнее. Как правило, в Debian и Ubuntu LTS идёт устаревшая версия дистрибутива. Я рекомендую использовать не ниже 24.0, в противном случае часть пакетов (можно считать их плагинами) не будет работать. Если в официальном репозитории слишком старая версия, придётся собирать EMACS из исходных кодов, что заслуживает отдельной статьи. Здесь на этом останавливаться не буду.
Помимо самого EMACS необходимо иметь в системе установленную систему контроля версий Git. Пользователи Windows могут скачать здесь.
Комбинации клавиш
Оригинальные комбинации клавиш EMACS устарели. Нажимать [Ctrl]
(в любом руководстве по EMACS эта клавиша обозначается как [C]
) мизинцем неудобно. Когда-то [Ctrl]
располагался на месте сегодняшнего [Alt]
, и пользоваться им было удобно, но времена мэйнфреймов давно прошли... Подробный разбор того, почему комбинации клавиш в EMACS такие, какие есть, и почему это плохо, производится здесь [EN]. Нет смысла изучать дефолтные настройки комбинаций клавиш, всё равно придётся потом переучитваться. Однако, в экстренных ситуациях может пригодиться как минимум две команды:
Комбинация | Действие |
---|---|
[M-X] |
Вход в минибуфер для ввода команд |
[C-X, C-C] |
Завершение работы редатора |
После установки и активации пакета ergoemacs-mode комбинации клавиш будут переопределены. Рекомендую сделать это как можно быстрее, т.к. отвыкнуть от Ctrl+X
, Ctrl+C
и Ctrl+V
очень тяжело, да и нет смысла этого делать.
Терминология
Буфер - аналог вкладки с документом в других тектовых редакторах и IDE. Так же это область, куда выводятся сообщения плагинов или самого редактора.
Минибуфер - самый маленький буфер, расположенный в нижней части окна EMACS. В него вводятся различные команды. Для входа в него следует использовать комбинацию клавиш [M-X]
без установленного и включенного пакета ergoemacs-mode, либо [M-A]
. После ввода команда запускается на выполнение нажатием [Enter]
(в документации EMACS эта клавиша называется [RET]
)
Пакет - аналог плагина в других редаторах и IDE. Представляет собой сценарий или набор сценариев на языке EMACS Lisp. Часть пакетов является встроенной в редактор и поставляется вместе с ним, другие нужно скачивать из репозитриев. Отметим, что сейчас практически не используется способ установки пакета ручным клонированием репозитория с GitHub, далее под репозиториями будут пониматься специализированные хранилища пакетов.
Основной режим - определяет поведение EMACS для буфера. От этого режима зависит поведение различных клавиатурных комбинаций и набор доступных дейстий. Например, при написании этой статьи я использую web-mode
, что даёт подсветку синтаксиса, автоматическое форматирование и применение Emmet для создания разметки. Основной режим может быть лишь один (имеется пакет, позволяющий включить сразу несколько основных режимов для одного буфера, но я им ни разу не пользовался).
Дополнительный режим - в то время, как основной режим может быть лишь один, дополнительных режимов можно включить столько, сколько нужно. Например, сюда относятся средства проверки орфографии, возможность автокомплита, автоформатирование кода и т.д. Большая часть пакетов для EMACS работает именно в дополнительном режиме.
Первоначальная настройка
Настройка EMACS может производиться как минимум двумя способами.
Настройка через правку файла конфигурации .emacs
Основной файл настроек EMACS называется .emacs
и располагается в домашнем каталоге пользователя. В случае с Windows это как правило каталог вроде этого:
C:\Users\xPhoenix\AppData\Roaming\.emacs
Однако, можно переопределить домашний каталог для хранения настроек (важно понимать, что файл .emacs
станет вашей прелессссстью, которую вы будете оберегать и лелеять), создав для текущего пользователя системную переменную под именем HOME
, и указав в ней путь к каталогу, который следует считать домашним для EMACS (возможно, работает и для других программ из мира Linux, не проверял). У меня эта переменная выглядит так:
D:\xphoenix\
Кроме этого файла EMACS при работе создаёт так же создаёт в домашнем каталоге папку .emacs.d
. В ней будут храниться скачанные пакеты, сниппеты, файлы .desktop
и т.д. Не надо его удалять!
В любом случае, в наш .emacs
следует поместить как минимум вот эти строки:
(defalias 'yes-or-no-p 'y-or-n-p)
(setq package-archives '(
("elpy" . "http://jorgenschaefer.github.io/packages/")
("gnu" . "http://elpa.gnu.org/packages/")
;; ("melpa" . "http://melpa.milkbox.net/packages/")
("melpa-stable" . "http://melpa-stable.milkbox.net/packages/")
("org" . "http://orgmode.org/elpa/")
))
Первая строка указыает на то, что вместо ввода строк yes
и no
в ответы на запросы системы можно просто нажать [y]
или [n]
соответственно.
Чуть ниже идёт указание репозиториев для поиска пакетов. Репозиторий MELPA очень популярен и в Интернете, вы будете часто встречать отсылки именно к нему, но использовать его опасно, т.к. пакеты туда попадают прямо из ветки master с GitHub. Поэтому я предпочитаю MELPA-STABLE. Пусть пакеты старые и их гораздо меньше, зато ничего не сломается при очередном обновлении. Впрочем, в редких случаях я пользуюсь MELPA, например, если пакета вообще нет в STABLE или тот, что есть, слишком старый. Так же существует репозиторий MARMELADE, но я не вижу смысла его использовать, т.к. некоторые пакеты в нём даже более старые, чем в MELPA-STABLE.
Настройка через customize
В одной из версий EMACS в стандартную поставку стал входить пакет sustomize
. Просто выполните одноименную команду, и попадете в буфер, где будет поле для поиска и множество гиперссылок для перехода к нужным настройкам. Отмечу лишь, что настройка некоторых параметров через customize
конфликтует с ручной правкой .emacs
, однако, в ряде случаев является более удобной. Некоторые параметры EMACS можно настроить только ручным редактированием .emacs
, в customize
вы просто не найдёте нужного раздела.
Установка пакетов
Указав в вашем .emacs
репозитории так, как указано выше, перезапустите редактор (можно сделать и без перезапуска, но не буду усложнять) и выполните команду list-packages
. Откроется буфер, содержащий список пакетов в доступных репозиториях. Выбор пакета для установки осуществляется нажатием клавиши [I]
, отмена выбора - [U]
, пометка на удаление - [D]
. После того, как будет выбрано, что делать с пакетами, следует нажать клавишу X
для запуска операций. EMACS установит и при необходимости выполнит компиляцию нужных пакетов.
Есть несколько пакетов, которые я настоятельно рекомендую к установке.
- auto-complete - предназначение ясно из названия. Через него работают многие другие пакеты, например,
jedi
. - autopair - автоматически закрывает скобки
- company - ещё одно средство для автокомплита, через него работают некоторые пакеты, которые не работают с
auto-complete
- emmet-mode - незаменимое средство для верстальщиков. Рекомендую ставить из MELPA, т.к. в STABLE очень старая версия, которая работает хуже и многого не умеет.
- ergoemacs-mode - пакет, устанавливающий комбинации клавиш, удобные для использования людьми, а не Ричардом Столлманом. Подробное описание клавиш здесь.
- flycheck - модуль проверки чего угодно на лету. На самом деле по-тихому вызывает имеющиеся в системе средства проверки и статического анализа кода. Имеет возможность конфигурирования того, какие средства и с какими параметрами следует использовать. Является более новым и прогрессивным в сравнении с загнивающим
flymake
. - jedi - автокомплит для Python-разработчиков. Настройка данного пакета достойна отдельной статьи.
- js2-mode - расширенный по сравнению с
js-mode
режим правки JavaScript-кода. Ставитьjs3-mode
не рекомендую, т.к. проект давно загнулся. - less-css-mode - не вижу смысла объяснять назначение данного пакета.
- monokai - тема из Sublime Text 2, пожалуй, лучшая, что мне приходилось видеть.
- neotree - порт плагина NerdTree из VIM, отображает слева (или справа, настраивается) дерево каталогов и файлов. Лучше ставить из MELPA.
- rainbow-mode, rainbow-delimiters - в паре подсвечивают скобки и другие элементы разными цветами, позволяет легко находить ошибки типа "Забыл закрыть скобку".
- web-beautify - доступен только из MELPA, позволяет автоматически форматировать JS, CSS и HTML-файлы. Для работы требует установленный в системе NodeJS и его пакет
web-beautify
. - web-mode - добавляет в EMACS возможность редактировать файлы XML, HTML, XHTML и др. Обеспечивает подсветку синтаксиса и т.д.
Получение аргументов URL в сериализаторах Django REST Framework
Небольшая заметка о том, как в ClassBasedView Django REST Framework получить значение параметра из URL.
Допустим, наши URL сконфигурированы таким образом:
urls.pyfrom django.conf.urls import url
from .api import ArticleListCreateView
from .api import ArticleDetailView
from .api import ArticleCommentListCreateView
urlpatterns = [
url(r'^article/$', ArticleListCreateView.as_view()),
url(r'^article/((\d+))/$', ArticleDetailView.as_view()),
url(r'^article/((\d+))/comment/$', ArticleCommentListCreateView.as_view()),
]
Делать какую-либо работу для вида ArticleDetailView
не приходится - достаточно создать класс, унаследованный от RetrieveUpdateDestroyAPIView
.
Получить доступ к параметру pk
в виде не проблема, достаточно переопределить метод get_queryset()
, например, так:
from rest_framework.generics import RetrieveUpdateDestroyAPIView
from article.models import Article
from .models import ArticleComment
class ArticleCommentListCreateView(RetrieveUpdateDestroyAPIView):
model = ArticleComment
# Возвращаем только комментарии к указанной статье
def get_queryset(self):
return ArticleComment.objects.filter(article=self.kwargs['pk])
def get_serializer_class(self):
if self.request.method == 'GET':
return ArticleCommentReadSerializer
return ArticleCommentWriteSerializer
А вот получить в сериализаторе значение параметра pk
можно не вполне очевидным способом:
from django.shortcuts import get_object_or_404
from rest_framework import serializers
from .models import ArticleComment
from article.models import Article
# ArticleReadSerializer не описан из-за своей простоты
class ArticleCommentWriteSerializer(serializers.ModelSerializer):
article = serializers.PrimaryKeyRelatedField(
queryset=Article.objects.all(),
default=None
)
# Заменяем валидатор поля article таким образом, чтобы всегда
# подставлялось значение из URL
def validate_article(self, value):
# Кто бы мог подумать???
article_id = self.context['view'].kwargs['pk']
return get_object_or_404(Article, pk=article_id)
class Meta:
model = ArticleComment
Расширение контроллеров в Angular
Краткое содержание
В статье рассказывается о том, как в Angular можно реализовать наследование контроллеров.
Суть проблемы
Мне приходится писать довольно много кода на Angular, при этом заметил, что от контроллера к контроллеру меняются, порой, лишь незначительные части. Например, везде, где я использую ngInfiniteScroll, в $scope
приходится помещать переменные allLoaded
и loading
, а так же обработчик loadMore()
, выполняющий загрузку новых элементов. По незнанаю, приходилось многократно дублировать практически одинаковый код. Однако, один из пользователей StackOverflow нашёл очень хорошее решение данной проблемы.
Решение
Решение оказалось на удивление простым и заключается в использовании сервиса $controller
.
(function (A){
"use strict";
// Базовый контроллер, который содержит код, общий для всех других контроллеров,
// которые будут наследоваться от него
A.module('App').controller('ListController', ['$scope', function($scope){
$scope.allLoaded = false;
$scope.items = [];
$scope.loading = false;
$scope.loadMore = function(){
if ($scope.loading){
return;
}
// Тут какая-то работа по загрузке данных
};
$scope.refresh = function(noPlease){
if (noPlease){
return;
}
$scope.items.length = 0; // Более правильное решение, нежели $scope.items = [];
// т.к. в этом случае сохраняется ссылка на оригинальный
// объект
};
}]);
}(this.angular));
ChildClass.js
(function(A){
"use strict";
A.module('App').controller('UserListController', [ '$controller', '$scope', function($controller, $scope){
// Расширяем контроллер
$controller('ListController', {
$scope: $scope
});
// Теперь можно собственные свойства описывать
}]);
}(this.angular));
Схема БД или таблицы в PostgreSQL
Всё время забываю, как в PostgreSQL сделать дамп схемы БД, без данных. Команда на удивление проста:
pg_dump -s -d %DATABASE_NAME% -U %USER_NAME% > %FILE_NAME%
Параметр | Назначение |
---|---|
-s |
Ключ указывает на то, что нужно сделать копию только схемы БД |
-d |
Ключ, задающий имя БД, с которой нужно работать. |
-U |
Ключ, указывающий пользователя, от имени которого будет делаться копия схемы. Пользователь должен иметь доступ хотя бы на чтение данной БД. |
%FILE_NAME% |
Файл, в который следует сохранить вывод. В противном случае вся схема будет выведена на экран. |
А если вместо -s
указать -t
и потом имя таблицы, то будет снята схема только с неё, например:
pg_dump -d project -U xphoenix -t auth_users > auth_users_shema.sql
Проблемы с кириллицей в имени пользователя Windows
При использовании Atom столкнулся с тем, что некоторые плагины отказывались работать, выдавая странные сообщения в консоль. Сначала я решал проблему тем, что правил переменную PATH на уровне системы и на уровне пользователя, однако, это особого эффекта не имело, т.к. стоило только поставить более новую версию io.js, как PATH тут же сбрасывалась к примерно такому виду:
C:\Python34\;C:\Python34\Scripts;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Program Files (x86)\Git\cmd;C:\Program Files (x86)\iojs\;C:\Users\%30o$321\AppData\Roaming\npm
Из этого видно, что портится путь к текущей системной папке пользователя. Изменение свойств папок "Видео", "Изображения", "Документы" и т.д. тут не может ничем помочь. Я использую учётную запись Microsoft со всеми её удобствами вроде OneDrive, WindowsPhone и т.д. и отказываться от неё в пользу локальной учётной записи не хотел (не за то деньги плачены). Поиск привёл меня к решению.
На первом этапе нам нужно создать дополнительную учётную запись типа "Администратор". Способ создания значения не имеет, я рекомендую через "Панель управления" (пользователи Windows 8.1 непрофессиональной редакции выбора не имеют). Теперь нужно загрузиться в безопасном режиме и войти в систему под новосозданной учётной записью. После этого нужно запустить редактор реестра - Regedit. Следует открыть раздел реестра HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList
. В нём содержится несколько подразделов. Если щёлкнуть на одном из них, то появится список ключей, одним из которых будет ProfileImagePath
. Заменяем имеющееся значение на нужное, например, вот так:
C:\Users\Максим -> C:\Users\xPhoenix
Так же следует обязательно переименовать имеющуюся папку профиля. После этого следует перезагрузить компьютер и при необходимости удалить созданную ранее дополнительную учётную запись администратора.
Проблема с кириллицей в имени пользователя наблюдается и в некоторых других программах. Насколько помню, из-за неё среда MATLAB 7.5 вообще отказывалась сохранять файлы.
Собственная модель пользователя Django (>=1.8)
Введение
На данную тему в Интернете уже написано огромное множество статей, и моя станет лишь очередной попыткой описать то, что уже и так широко известно. Не претендуя на оригинальность, я попробую описать тот способ, которым пользуюсь сам. В статье пойдёт речь о расширении стандартной модели User
, имеющейся в Django.
В настоящее время так же широко используются ещё два способа:
- Создание связанной с пользователем 1 к 1 модели профиля.
- Полная замена стандартной модели пользователя на свою с последующим переписыванием бэкэндов авторизации.
Первый способ лично мне не нравится своей устарелостью. Ещё в Django 1.5 была проделана огромная работа по рефакторингу существующих модулей, связанных с пользователями, что дало возможность расширять имеющуюся модель. К тому же, способ с дополнительной моделью заставляет вас писать дополнительный код, например, на отлов события создания или удаления модели пользователя.
Второй способ плох опять же массой дополнительной работы. Нужно переопределить методы-обработчики событий авторизации и многих других действий.
Создание расширенной модели
Создавать новую модель пользователя мы будем на основе уже имеющейся в Django, т.е. будем использовать наследование. Первым делом следует создать новое приложение Django:
djangoadmin.py startapp extuser
Мне нравится использовать имя extuser
для решения поставленной задачи потому, что, оно полностью передаёт суть и назначение данного приложения - расширение стандартной модели пользователя.
Модифицируем файл models.py
нового приложения.
extuser/models.py
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.models import BaseUserManager
from django.db import models
class UserManager(BaseUserManager):
def create_user(self, email, password=None):
if not email:
raise ValueError('Email непременно должен быть указан')
user = self.model(
email=UserManager.normalize_email(email),
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password):
user = self.create_user(email, password)
user.is_admin = True
user.save(using=self._db)
return user
class ExtUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(
'Электронная почта',
max_length=255,
unique=True,
db_index=True
)
avatar = models.ImageField(
'Аватар',
blank=True,
null=True,
upload_to="user/avatar"
)
firstname = models.CharField(
'Фамилия',
max_length=40,
null=True,
blank=True
)
lastname = models.CharField(
'Имя',
max_length=40,
null=True,
blank=True
)
middlename = models.CharField(
'Отчество',
max_length=40,
null=True,
blank=True
)
date_of_birth = models.DateField(
'Дата рождения',
null=True,
blank=True
)
register_date = models.DateField(
'Дата регистрации',
auto_now_add=True
)
is_active = models.BooleanField(
'Активен',
default=True
)
is_admin = models.BooleanField(
'Суперпользователь',
default=False
)
# Этот метод обязательно должен быть определён
def get_full_name(self):
return self.email
# Требуется для админки
@property
def is_staff(self):
return self.is_admin
def get_short_name(self):
return self.email
def __str__(self):
return self.email
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
objects = UserManager()
class Meta:
verbose_name = 'Пользователь'
verbose_name_plural = 'Пользователи'
Рассмотрим этот код.
Менеджер моделей
Сначала идёт описание менеджера для данной модели. Описывать его нужно для того, чтобы правильно работали методы создания нового пользователя. Чуть ниже в коде будет указано, что для работы с объектами типа ExtUser
нужно использовать именно его. Можно определить несколько менеджеров при необходимости, каждый из которых будет отвечать за свою часть работы.
Модель ExtUser
В этой модели как ключевое указано поле email
. Я считаю, что это один из лучших способов для авторизации пользователей. Лучше может быть только двухфакторная авторизация через SMS. Создав ключевое поле, следуте обязательно указать, что оно используется в качестве имени пользователя:
USERNAME_FIELD = 'email'
Делали бы мы авторизацию через номер телефона - указали бы так:
phone = CharField(
'Номер телефона'
max_length=20,
unique=True,
db_index=True
)
#Чуть ниже в этом же классе:
USERNAME_FIELD = 'phone'
Надеюсь, общий принцип понятен. Дальше уже идёт отсебятина, которую можно не писать. Например, аватары, отчество и т.д. Список полей в каждом проекте будет разным. Не забудьте только описать методы проверки прав has_perm()
и has_module_perms()
соответственно.
Формы
Наш класс не будет работать, если не создать для него формы админки. Создадим в приложении файл forms.py
.
extuser/forms.py
from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.contrib.auth import get_user_model
class UserCreationForm(forms.ModelForm):
password1 = forms.CharField(
label='Пароль',
widget=forms.PasswordInput
)
password2 = forms.CharField(
label='Подтверждение',
widget=forms.PasswordInput
)
def clean_password2(self):
password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError('Пароль и подтверждение не совпадают')
return password2
def save(self, commit=True):
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data['password1'])
if commit:
user.save()
return user
class Meta:
model = get_user_model()
fields = ('email',)
class UserChangeForm(forms.ModelForm):
'''
Форма для обновления данных пользователей. Нужна только для того, чтобы не
видеть постоянных ошибок "Не заполнено поле password" при обновлении данных
пользователя.
'''
password = ReadOnlyPasswordHashField(
widget=forms.PasswordInput,
required=False
)
def save(self, commit=True):
user = super(UserChangeForm, self).save(commit=False)
password = self.cleaned_data["password"]
if password:
user.set_password(password)
if commit:
user.save()
return user
class Meta:
model = get_user_model()
fields = ['email', ]
class LoginForm(forms.Form):
"""Форма для входа в систему
"""
username = forms.CharField()
password = forms.CharField()
Здесь описаны две формы - для создания нового пользователя и для смены пароля. Т.к. я использую Django REST Framework, я сделал эти формы для того, чтобы корректно работало обновление модели пользователя, т.к. поле password
является обязательным. Если отправить запрос на указание, например, нового отчества, будет возвращена ошибка, т.к. поле пароля должно быть обязательно заполнено. Дополнительная форма решает эту проблему.
PATCH
вместо PUT
.Пожалуй, единственное, на что тут нужно обратить внимание - это описание связи наших форм с моделью пользователя. Если в будущем понадобится создать новую модель или переименовать её, переписывать код не придётся, т.к. используется функция get_user_model()
, возвращающая класс модели пользователя, используемый для авторизации. Если проще, то эта функция возвращает класс, указанный в параметре AUTH_USER_MODEL
в файле settings.py
нашего приложения.
Админка
Изменения придётся внести и в файл admin.py
:
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group
from .forms import UserChangeForm
from .forms import UserCreationForm
from .models import ExtUser
class UserAdmin(UserAdmin):
form = UserChangeForm
add_form = UserCreationForm
list_display = [
'date_of_birth',
'email',
'firstname',
'is_admin',
'lastname',
'middlename',
]
list_filter = ('is_admin',)
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {
'fields': (
'avatar',
'date_of_birth',
'firstname',
'lastname',
'middlename',
)}),
('Permissions', {'fields': ('is_admin',)}),
('Important dates', {'fields': ('last_login',)}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'date_of_birth',
'email',
'password1',
'password2'
)}
),
)
search_fields = ('email',)
ordering = ('email',)
filter_horizontal = ()
# Регистрация нашей модели
admin.site.register(ExtUser, UserAdmin)
admin.site.unregister(Group)
Настройка Django
Пришло время переустановить Windows внести изменения в файл settings.py
.
settings.py
# Тут должен быть импорт нужных модулей.
SECRET_KEY = # Какой-то ключ, автоматически созданный Django
DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []
EXTERNAL_APPS = (
# У меня, например, здесь разные внешние библиотеки
)
INTERNAL_APPS = (
# Тут список наших приложений
'extuser',
# А тут его продолжение
)
DJANGO_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
)
INSTALLED_APPS = EXTERNAL_APPS + INTERNAL_APPS + DJANGO_APPS
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware',
)
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.csrf',
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.request',
)
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend",
)
LOGIN_URL = r"/login/"
AUTH_USER_MODEL = 'extuser.ExtUser'
Миграции
Заключительный этап настройки нашей модели - проведение миграций. Запустите скрипт:
python manage.py migrate
Для создания нового пользователя-администратора используйте команду createsuperuser
:
python manage.py createsuperuser
6 комментариев :
Отправить комментарий