Делюсь опытом в описанных технологиях. Блог в первую очередь выполняет роль памяток для меня самого.

Собственная модель пользователя Django (>=1.8)

24 комментария

Введение

Статья была обновлена 5 июня 2015 года и содержит исправление некоторых ошибок и дополнительную информацию.
На GitHub был опубликован репозиторий с исходными кодами, которые содержат ряд исправлений и дополнений. Нашли ошибку? Создайте pull-request или issue, я обязательно посмотрю.

На данную тему в Интернете уже написано огромное множество статей, и моя станет лишь очередной попыткой описать то, что уже и так широко известно. Не претендуя на оригинальность, я попробую описать тот способ, которым пользуюсь сам. В статье пойдёт речь о расширении стандартной модели 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 является обязательным. Если отправить запрос на указание, например, нового отчества, будет возвращена ошибка, т.к. поле пароля должно быть обязательно заполнено. Дополнительная форма решает эту проблему.

Как правило, ошибка обновления модели в DRF происходит при полном обновлении модели. Для частитчного обновления нужно указывать в заголовке HTTP-запроса метод 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

24 комментария :

  1. Полностью повторил все действия автора, получаю ошибку"django.core.exceptions.ImproperlyConfigured: AUTH_USER_MODEL refers to model 'extuser.User' that has not been installed"

    ОтветитьУдалить
    Ответы
    1. Проблему давно решили, он мне в личку ВК писал, потому я и правил статью.
      P. S. Когда-нибудь мне будет не лень, и DJANGO_APPS у меня будут подключаться раньше остальных, но не сегодня.

      Удалить
  2. list_filter = ('is_admin',)

    fieldsets = (
    (None, {'fields': ('email', 'password')}),
    ('Personal info', {
    'fields': (
    'avatar'
    'date_of_birth',

    Пропущена запятая после 'avatar'

    ОтветитьУдалить
  3. Спасибо, отличная статья. Не могли бы вы объяснить, как реализовать валидацию через почту, с отправкой письма подтверждения. Понимаю, что нужно прописать в setings но не совсем понял, как реализовать это в модели.

    ОтветитьУдалить
  4. При смене каких либо параметров (не password) у пользователя (superuser) через админку (/admin/) меняется еще и пароль.
    Что бы повторить: Заходите в /admin/ -> Пользователи -> для наглядности выберем пользователя superuser(под которым и зашли) -> изменим параметр к примеру дата рождения и нажмем "Сохранить" . Результат: Выкидывает на авторизацию, при попытке авторизоваться со старым паролем - не пускает. Приходится менять пароль python manage.py changepassword

    ОтветитьУдалить
    Ответы
    1. Сейчас проверить не могу, т.к. админкой не пользуюсь, пилю свою. Если у Вас будут какое-то решение данной проблемы, пожалуйста, опубликуйте его в комментарии.

      Удалить
    2. Вы не предложили решение изменение пароля при изменении данных.
      Если не трудно, можете его опубликовать?

      Удалить
  5. Присоединяюсь к Keeper_keys, а то как то не айс)))

    ОтветитьУдалить
  6. Да, и было бы не плохо увидеть представление)

    ОтветитьУдалить
    Ответы
    1. Хорошо, всё будет, но не в ближайшее время, т.к. я переехал в другой город и у меня здесь пока нет компьютера.

      Удалить
  7. На Git бы код выложили

    ОтветитьУдалить
    Ответы
    1. https://github.com/dunmaksim/django-extuser
      Пересилил лень и выложил.

      Удалить
    2. Спасибо вам конечно большое. Кстати как там дела обстоят с подтверждением аккаунта? на почту приходит ссылка активации?

      Удалить
  8. Нашел как исправить баг с изменением пароля : в файле forms.py удаляем функцию clean_password.После этого проблема исчезла.

    ОтветитьУдалить
    Ответы
    1. Спасибо! Не проверял, но полагаю, что должно работать.

      Удалить
    2. Подскажите какую функцию нужно удалить? функции clean_password в коде нет. есть clean_password2 но она по всей видимости не имеет отношение к проблемме.

      Удалить
  9. Этот комментарий был удален автором.

    ОтветитьУдалить
  10. Здравствуйте, делал все также как автор, но когда вхожу в админку суперюзером пишет что недостаточно прав для редактирования. Кто-нибудь сталкивался с подобным? django 1.9.1

    ОтветитьУдалить
    Ответы
    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

      Удалить
  11. Этот комментарий был удален автором.

    ОтветитьУдалить
  12. Проблема: при входе в django admin пишет: You don't have permission to edit anything.

    ОтветитьУдалить
  13. Вначале параграфа Модель ExtUser исправьте "следуте"

    ОтветитьУдалить
  14. В статье есть косяки (с паролем в админке, а также проблема с is_admin (не влияет на доступ к админке Django), также в современной версии Django возникает еще ряд косяков.
    Рекомендую данную статью: https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html

    Также рекомендую не переписывать существующую модель пользователя без особой необходимости, а использовать собственную модель Profile c OneToOneField к стандартной модели Users

    ОтветитьУдалить