Собственная модель пользователя 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
Полностью повторил все действия автора, получаю ошибку"django.core.exceptions.ImproperlyConfigured: AUTH_USER_MODEL refers to model 'extuser.User' that has not been installed"
ОтветитьУдалитьПроблему давно решили, он мне в личку ВК писал, потому я и правил статью.
УдалитьP. S. Когда-нибудь мне будет не лень, и DJANGO_APPS у меня будут подключаться раньше остальных, но не сегодня.
list_filter = ('is_admin',)
ОтветитьУдалитьfieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {
'fields': (
'avatar'
'date_of_birth',
Пропущена запятая после 'avatar'
Спасибо за замечание, поправил.
УдалитьСпасибо, отличная статья. Не могли бы вы объяснить, как реализовать валидацию через почту, с отправкой письма подтверждения. Понимаю, что нужно прописать в setings но не совсем понял, как реализовать это в модели.
ОтветитьУдалитьПри смене каких либо параметров (не password) у пользователя (superuser) через админку (/admin/) меняется еще и пароль.
ОтветитьУдалитьЧто бы повторить: Заходите в /admin/ -> Пользователи -> для наглядности выберем пользователя superuser(под которым и зашли) -> изменим параметр к примеру дата рождения и нажмем "Сохранить" . Результат: Выкидывает на авторизацию, при попытке авторизоваться со старым паролем - не пускает. Приходится менять пароль python manage.py changepassword
Сейчас проверить не могу, т.к. админкой не пользуюсь, пилю свою. Если у Вас будут какое-то решение данной проблемы, пожалуйста, опубликуйте его в комментарии.
УдалитьВы не предложили решение изменение пароля при изменении данных.
УдалитьЕсли не трудно, можете его опубликовать?
Присоединяюсь к Keeper_keys, а то как то не айс)))
ОтветитьУдалитьДа, и было бы не плохо увидеть представление)
ОтветитьУдалитьХорошо, всё будет, но не в ближайшее время, т.к. я переехал в другой город и у меня здесь пока нет компьютера.
УдалитьНа Git бы код выложили
ОтветитьУдалитьhttps://github.com/dunmaksim/django-extuser
УдалитьПересилил лень и выложил.
Спасибо вам конечно большое. Кстати как там дела обстоят с подтверждением аккаунта? на почту приходит ссылка активации?
УдалитьНашел как исправить баг с изменением пароля : в файле forms.py удаляем функцию clean_password.После этого проблема исчезла.
ОтветитьУдалитьСпасибо! Не проверял, но полагаю, что должно работать.
УдалитьПодскажите какую функцию нужно удалить? функции clean_password в коде нет. есть clean_password2 но она по всей видимости не имеет отношение к проблемме.
УдалитьЭтот комментарий был удален автором.
ОтветитьУдалитьЗдравствуйте, делал все также как автор, но когда вхожу в админку суперюзером пишет что недостаточно прав для редактирования. Кто-нибудь сталкивался с подобным? django 1.9.1
ОтветитьУдалитьdef create_superuser(self, email, password):
Удалитьuser = self.create_user(email, password)
user.is_admin = True
user.is_superuser = True
user.save(using=self._db)
return user
Этот комментарий был удален автором.
ОтветитьУдалитьПроблема: при входе в django admin пишет: You don't have permission to edit anything.
ОтветитьУдалитьВначале параграфа Модель ExtUser исправьте "следуте"
ОтветитьУдалитьВ статье есть косяки (с паролем в админке, а также проблема с is_admin (не влияет на доступ к админке Django), также в современной версии Django возникает еще ряд косяков.
ОтветитьУдалитьРекомендую данную статью: https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html
Также рекомендую не переписывать существующую модель пользователя без особой необходимости, а использовать собственную модель Profile c OneToOneField к стандартной модели Users