Всё-таки при начале проекта нужно создавать свою модель пользователя даже если смысл в этом на начальном этапе не очевиден. Но если достался проект с штатной моделью пользователя, и пользователя необходимо кастомизировать, то готовимся к большому количеству ручной работы. В этом рецепте предполагается, что используется MySQL.
Подготовка
Первым делом надо избавиться от явного обращения к модели auth.User
. Модель пользователя нужно вписать в settings.py
:
AUTH_USER_MODEL = 'auth.User'
И нужно пройтись по всему проекту, по всем моделям, вьюхам, тэгам, везде, где встретится явное обращение к модели пользователя, заменить на переменную из настроек:
# models.py
from django.conf import settings
class SomeModel(models.Model):
…
this_item_owner = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name="Владелец")
…
Делая изменения, следует регулярно проверять, что ничего в проекте не поломалось. Совсем замечательно, если проект хорошо покрыт тестами. Можно делать перерывы на чашечку чая, прогоняя полный набор тестов после каждого значимого набора изменений.
Почти создание катомной модели
Прежде, чем создавать свою собственную модель, лучше создать собственную копию стандартной модели:
class CustomUserManager(BaseUserManager):
def _create_user(self, email, password,
is_staff, is_superuser, **extra_fields):
"""
Creates and saves a User with the given email and password.
"""
now = timezone.now()
if not email:
raise ValueError('The given email must be set')
email = self.normalize_email(email)
user = self.model(email=email,
is_staff=is_staff, is_active=True,
is_superuser=is_superuser, last_login=now,
date_joined=now, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False,
**extra_fields)
def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True,
**extra_fields)
class CustomUser(AbstractBaseUser, PermissionsMixin):
username = models.CharField(
_('username'),
max_length=30,
unique=True,
help_text=_('Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.'),
validators=[
validators.RegexValidator(
r'^[\w.@+-]+$',
_('Enter a valid username. This value may contain only '
'letters, numbers ' 'and @/./+/-/_ characters.')
),
],
error_messages={
'unique': _("A user with that username already exists."),
},
)
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=30, blank=True)
email = models.EmailField(_('email address'), blank=True)
is_staff = models.BooleanField(_('staff status'), default=False,
help_text=_('Designates whether the user can log into this admin site.'))
is_active = models.BooleanField(_('active'), default=True,
help_text=_('Designates whether this user should be treated as '
'active. Unselect this instead of deleting accounts.'))
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
objects = CustomUserManager()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = []
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def get_absolute_url(self):
return "/users/%s/" % urlquote(self.email)
def get_full_name(self):
"""
Returns the first_name plus the last_name, with a space in between.
"""
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
"""
Returns the short name for the user.
"""
return self.first_name
def email_user(self, subject, message, from_email=None):
"""
Sends an email to this User.
"""
send_mail(subject, message, from_email, [self.email])
Следует обратить внимание на порядок полей. Лучше, если он будет таким же, как в стандартной модели.
Теперь следует заменить в settings.py
модель на свою:
AUTH_USER_MODEL = 'myapp.CustomUser'
и можно делать первую миграцию и сразу же её применять:
python manage.py makemigrations
python manage.py migrate
Модель создана, но пока абсолютно бесполезна. В таблице БД, соответствующей этом модели, пусто. К тому же в этот момент в проекте поломались связи. Это всё потребуется исправить.
Перенос пользователей в новую таблицу
Для переноса пользователей следует создать миграцию, в которой описать действия по переносу:
python manage.py makemigrations myapp --empty
Эта команда создаст файл с заготовкой миграции примерно такого содержания:
# -*- coding: utf-8 -*-
# Generated by Django 1.9a1 on 2015-10-16 12:56
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
]
Здесь operations
и есть список операций, которые будут выполнены по ходу применения этой миграции. Можно писать чистый SQL:
operations = [
migrations.RunSQL("""
INSERT INTO `myapp_customuser` SELECT * FROM `auth_user`;
INSERT INTO `myapp_customuser_groups` SELECT * FROM `auth_user_groups`;
INSERT INTO `myapp_customuser_user_permissions` SELECT * FROM `auth_user_user_permissions`;
"""),
]
Однако, вспомним ещё раз предупреждение про порядок полей. Просмотреть его можно так: describe auth_user; describe myapp_customuser;
Если порядок не соответствует, то копирование таблиц не сработает, нужно привести порядок полей в соответствие, вписав в миграцию нужные sql-запросы перед запросами копирования. Пример запроса, изменяющего порядок полей:
ALTER TABLE `myapp_customuser` MODIFY COLUMN `email` VARCHAR(254) AFTER `last_name`;
Исправление связей
В базе данных есть внешние ключи, обеспечивающие привязку записей к записям другой таблицы. Автоматически миграции, меняющие эти ключи, не создаются.
К примеру, есть модель комментариев, привязанная к пользователю:
class Comment(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL)
…
Можно посмотреть, каким запросом такая таблица создаётся:
show create table comments_comment;
И внимательно изучим ответ:
CREATE TABLE `comments_comment` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`add_time` datetime(6) NOT NULL,
`message` longtext NOT NULL,
`author_id` int(11) NOT NULL,
`article_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `comments_comment_author_id_2ce6707a0808e94_fk_auth_user_id` (`author_id`),
KEY `comments_comment_article_id_b476e4fcb67b5f8_fk_articles_article_id` (`article_id`),
CONSTRAINT `comments_comment_author_id_2ce6707a0808e94_fk_auth_user_id` FOREIGN KEY (`author_id`) REFERENCES `auth_user` (`id`),
CONSTRAINT `comments_comment_article_id_b476e4fcb67b5f8_fk_articles_article_id` FOREIGN KEY (`article_id`) REFERENCES `articles_article` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
Здесь определён внешний ключ comments_comment_author_id_2ce6707a0808e94_fk_auth_user_id
на таблицу auth_user
. Этот ключ более не нужен. Вместо него нужен другой, на новую таблицу. Значит, после копирования таблиц пользователей нужно в миграцию добавить два запроса: удаления ненужного ключа и создания нужного:
ALTER TABLE `comments_comment`
DROP FOREIGN KEY `comments_comment_author_id_2ce6707a0808e94_fk_auth_user_id`;
ALTER TABLE `comments_comment`
ADD CONSTRAINT `comment_author_id_customuser_id`
FOREIGN KEY (`author_id`) REFERENCES `myapp_customuser` (`id`);
Такие пары запросов нужно создать для всех полей типа ForeignKey
.
ManyToManyField
В случае связей многие ко многим всё несколько сложнее. В БД для таких связей создаётся промежуточная таблица с уникальными парами айдишников связываемых таблиц. Нужно изменить таблицу так, чтобы:
- изменить имя поля, содержащего id пользователя;
- изменить внешние ключи для этого поля;
- изменить уникальный ключ для пары полей, чтобы в пару входило теперь новое поле пользователя.
К примеру, есть такая таблица:
CREATE TABLE `projects_project_customers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`project_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `project_id` (`project_id`,`user_id`),
KEY `projects_project_custom_user_id_7ffa77516b3fe98c_fk_auth_user_id` (`user_id`),
CONSTRAINT `projects_project_custom_user_id_7ffa77516b3fe98c_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
CONSTRAINT `projects_proj_project_id_22708df89e3461df_fk_projects_project_id` FOREIGN KEY (`project_id`) REFERENCES `projects_project` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
В этой таблице поле user_id
нужно переименовать в customuser_id
и заменить ключи.
Делается так:
ALTER TABLE `projects_project_customers`
DROP FOREIGN KEY `projects_project_custom_user_id_7ffa77516b3fe98c_fk_auth_user_id`;
ALTER TABLE `projects_project_customers`
DROP FOREIGN KEY `projects_proj_project_id_22708df89e3461df_fk_projects_project_id`;
ALTER TABLE `projects_project_customers` DROP INDEX `project_id`;
ALTER TABLE `projects_project_customers` CHANGE `user_id` `customuser_id` INT;
ALTER TABLE `projects_project_customers`
ADD CONSTRAINT `projects_project_id`
FOREIGN KEY (`project_id`) REFERENCES `projects_project` (`id`);
ALTER TABLE `projects_project_customers`
ADD CONSTRAINT `projects_customuser_id`
FOREIGN KEY (`customuser_id`) REFERENCES `myapp_customuser` (`id`);
ALTER TABLE `projects_project_customers`
ADD CONSTRAINT `project_id`
UNIQUE (`project_id`, `customuser_id`);
А вот теперь действительно кастомизированная модель пользвоателя
Вышеописанными операциями мы перевели проект на использование своей собственной модели пользователя. Ничего не поломалось, и это хорошо. Но новая модель точно такая же, как штатная.
Теперь самое время изменить модель так, как того хочется. Начиная именно с этого момента в своей модели пользователя можно менять поля, методы, создавать миграции и применять их. Полная свобода. Ради этого момента и приходилось исполнять все эти пляски с бубном.
… и прочее
Возможно, вы удивитесь, зайдя в админку и не увидев там пользователей. В рецепте ни слова о том, как вывести кастомного юзера в админку. Но эта информация общеизвестна, в том числе об этом уже написано в другом рецепте.
Описанный подход пригоден только в случае, если на всех серверах и рабочих станциях используется один тип базы данных.
И ещё раз: начиная новый проект, создавайте свою модель пользователя, пока это просто. Даже если необходимость этого действия сразу не очевидна.