Debian, Dojo, Django, Python

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

Показаны сообщения с ярлыком модель. Показать все сообщения
Показаны сообщения с ярлыком модель. Показать все сообщения

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

Введение

Статья была обновлена 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