【IT】Djangoでのカスタムユーザ
皆さま
こんにちは
Djangoでは、標準でユーザー管理モデルがあり
ユーザ認証や、登録、管理が行えます。
しかしながら公式サイトでは、そのまま使うことは
推奨されずカスタマイズすること推奨されております。
今回は、カスタムユーザモデルを使う上ので準備を行います。
認証ユーザをemailへ変更した場合の例となります。
(accoutnsアプケーションが作成済みであることを前提します)
カスタムモデルの作成の流れ
プロジェクトフォルダー配下の仮想環境フォルダーのmodels.pyより
・UserManagerクラス
・AbstractUserクラス
をaccoutns/models.pyへコピーして使用します。
元ファイルは、
仮想環境をpoetryでプロジェクト配下の.venv 配下に設定している場合
「.venv/lib/python3.xx/site-packages/django/contrib/auth/models.py」
となります。
最後に初期DBの削除を一旦削除して再度マイグレーションを行います。
今回の環境
OS:Ubuntu 22.04.2 LTS
Python:3.11.2
仮想環境:Poetry
道入モジュール:
[tool.poetry.dependencies]
python = "^3.11"
django = "^4.1.7"
django-allauth = "^0.52.0"
django-widget-tweaks = "^1.4.12"
python-dotenv = "^1.0.0"
カスタムモデル作成手順
コピーする元ファイルのクラス(UserManager /AbstractUser)
以下のファイルよりコピーします。
「.venv/lib/python3.xx/site-packages/django/contrib/auth/models.py」
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, username, email, password, **extra_fields):
"""
Create and save a user with the given username, email, and password.
"""
if not username:
raise ValueError("The given username must be set")
email = self.normalize_email(email)
# Lookup the real model class from the global app registry so this
# manager method can be used in migrations. This is fine because
# managers are by definition working on the real model.
GlobalUserModel = apps.get_model(
self.model._meta.app_label, self.model._meta.object_name
)
username = GlobalUserModel.normalize_username(username)
user = self.model(username=username, email=email, **extra_fields)
user.password = make_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(username, email, password, **extra_fields)
def create_superuser(self, username, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self._create_user(username, email, password, **extra_fields)
def with_perm(
self, perm, is_active=True, include_superusers=True, backend=None, obj=None
):
if backend is None:
backends = auth._get_backends(return_tuples=True)
if len(backends) == 1:
backend, _ = backends[0]
else:
raise ValueError(
"You have multiple authentication backends configured and "
"therefore must provide the `backend` argument."
)
elif not isinstance(backend, str):
raise TypeError(
"backend must be a dotted import path string (got %r)." % backend
)
else:
backend = auth.load_backend(backend)
if hasattr(backend, "with_perm"):
return backend.with_perm(
perm,
is_active=is_active,
include_superusers=include_superusers,
obj=obj,
)
return self.none()
class AbstractUser(AbstractBaseUser, PermissionsMixin):
"""
An abstract base class implementing a fully featured User model with
admin-compliant permissions.
Username and password are required. Other fields are optional.
"""
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_("username"),
max_length=150,
unique=True,
help_text=_(
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
validators=[username_validator],
error_messages={
"unique": _("A user with that username already exists."),
},
)
first_name = models.CharField(_("first name"), max_length=150, blank=True)
last_name = models.CharField(_("last name"), max_length=150, 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 = UserManager()
EMAIL_FIELD = "email"
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["email"]
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
abstract = True
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def get_full_name(self):
"""
Return 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):
"""Return the short name for the user."""
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)
カスタムユーザモデルの追加
accounts/models.py
に元ファイルよりコピーしたクラスを貼り付けます。
変更例は以下の通りとなります。
usernameでの認証をemailへ変更して行きます。
from django.db import models
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import UserManager, PermissionsMixin
from django.utils import timezone
from django.contrib.auth.hashers import make_password
from django.contrib import auth
from django.utils.translation import gettext_lazy as _
class UserManager(UserManager):
##use_in_migrations = True
##def _create_user(self, username, email, password, **extra_fields):
def _create_user(self, email, password, **extra_fields):
"""
Create and save a user with the given username, email, and password.
"""
#if not username:
## raise ValueError("The given username must be set")
email = self.normalize_email(email)
# Lookup the real model class from the global app registry so this
# manager method can be used in migrations. This is fine because
# managers are by definition working on the real model.
##GlobalUserModel = apps.get_model(
## self.model._meta.app_label, self.model._meta.object_name
##)
##username = GlobalUserModel.normalize_username(username)
##user = self.model(username=username, email=email, **extra_fields)
user = self.model(email=email, **extra_fields)
user.password = make_password(password)
user.save(using=self._db)
return user
##def create_user(self, username, email=None, password=None, **extra_fields):
def create_user(self, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
##return self._create_user(username, email, password, **extra_fields)
return self._create_user(email, password, **extra_fields)
##def create_superuser(self, username, email=None, password=None, **extra_fields):
def create_superuser(self, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
##return self._create_user(username, email, password, **extra_fields)
return self._create_user(email, password, **extra_fields)
def with_perm(
self, perm, is_active=True, include_superusers=True, backend=None, obj=None
):
if backend is None:
backends = auth._get_backends(return_tuples=True)
if len(backends) == 1:
backend, _ = backends[0]
else:
raise ValueError(
"You have multiple authentication backends configured and "
"therefore must provide the `backend` argument."
)
elif not isinstance(backend, str):
raise TypeError(
"backend must be a dotted import path string (got %r)." % backend
)
else:
backend = auth.load_backend(backend)
if hasattr(backend, "with_perm"):
return backend.with_perm(
perm,
is_active=is_active,
include_superusers=include_superusers,
obj=obj,
)
return self.none()
##class AbstractUser(AbstractBaseUser, PermissionsMixin):
class CustomUser(AbstractBaseUser, PermissionsMixin):
"""
An abstract base class implementing a fully featured User model with
admin-compliant permissions.
Username and password are required. Other fields are optional.
"""
##username_validator = UnicodeUsernameValidator()
##username = models.CharField(
## _("username"),
## max_length=150,
## unique=True,
## help_text=_(
## "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
## ),
## validators=[username_validator],
## error_messages={
## "unique": _("A user with that username already exists."),
## },
##)
##first_name = models.CharField(_("first name"), max_length=150, blank=True)
##last_name = models.CharField(_("last name"), max_length=150, blank=True)
##email = models.EmailField(_("email address"), blank=True)
email = models.EmailField('メールアドレス', unique=True)
first_name = models.CharField(('名前'), max_length=30)
last_name = models.CharField(('名字'), max_length=30)
department = models.CharField(('部門'), max_length=30, blank=True)
created = models.DateTimeField(('作成日'), default=timezone.now)
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 = UserManager()
EMAIL_FIELD = "email"
##USERNAME_FIELD = "username"
USERNAME_FIELD = "email"
##REQUIRED_FIELDS = ["email"]
REQUIRED_FIELDS = []
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
##abstract = True
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
##def get_full_name(self):
## """
## Return 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):
## """Return the short name for the user."""
## return self.first_name
##
##def email_user(self, subject, message, from_email=None, **kwargs):
## """Send an email to this user."""
## send_mail(subject, message, from_email, [self.email], **kwargs)
管理画面での表示登録
accounts/admin.py
CustomUserを追加します。
from django.contrib import admin
from .models import CustomUser
admin.site.register(CustomUser)
AUTH_USER_MODELの指定
認証をemailへ変更した場合は、setting.pyに設定を追加します。
AUTH_USER_MODEL = 'accounts.CustomUser'
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
カスタムユーザモデルの反映
makemigrationsを実施してDBの元定義を作成します。
$ python manage.py makemigrations
Migrations for 'accounts':
accounts/migrations/0001_initial.py
- Create model CustomUser
次にmigreteを行ってDBへ反映しますが、
最初にデフォルトのUserモデルを使用した状態でmigrate 済みですと
以下の様に不整合がでてエラーとなります。
(test-custom-py3.11) testpy@test~/test-custom$ python manage.py migrate
Traceback (most recent call last):
File "/home/testpy/test-custom/manage.py", line 22, in <module>
main()
File "/home/testpy/test-custom/manage.py", line 18, in main
execute_from_command_line(sys.argv)
File "/home/testpy/test-custom/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 446, in execute_from_command_line
utility.execute()
File "/home/testpy/test-custom/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 440, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/home/testpy/test-custom/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 402, in run_from_argv
self.execute(*args, **cmd_options)
File "/home/testpy/test-custom/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 448, in execute
output = self.handle(*args, **options)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/testpy/test-custom/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 96, in wrapped
res = handle_func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/testpy/test-custom/.venv/lib/python3.11/site-packages/django/core/management/commands/migrate.py", line 117, in handle
executor.loader.check_consistent_history(connection)
File "/home/testpy/test-custom/.venv/lib/python3.11/site-packages/django/db/migrations/loader.py", line 327, in check_consistent_history
raise InconsistentMigrationHistory(
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependency accounts.0001_initial on database 'default'.
いろいろと直す方法はあるようですが、
初期構築段階ですので一旦DBは削除します。
今回は、sqlite3を使用しておりますのでプロジェクト配下の
「db.sqlite3」を削除します。
あらためてmigrateを実施します。
(test-custom-py3.11) testpy@test~/test-custom$ rm db.sqlite3
(test-custom-py3.11) testpy@test~/test-custom$ python manage.py migrate
Operations to perform:
Apply all migrations: account, accounts, admin, auth, contenttypes, sessions, sites, socialaccount
Running migrations:
Applying contenttypes.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0001_initial... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying accounts.0001_initial... OK
Applying account.0001_initial... OK
Applying account.0002_email_max_length... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying sessions.0001_initial... OK
Applying sites.0001_initial... OK
Applying sites.0002_alter_domain_unique... OK
Applying socialaccount.0001_initial... OK
Applying socialaccount.0002_token_max_lengths... OK
Applying socialaccount.0003_extra_data_default_dict... OK
稼働確認
管理ユーザがemailで作成できるか確認します。
(test-custom-py3.11) testpy@test~/test-custom$ python manage.py createsuperuser
メールアドレス: xxxxxxxx@example.com
Password:
Password (again):
このパスワードは一般的すぎます。
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
次にアプリを起動し、メールアドレスでログインできるか確認します。
メールアドレスとパスワードを入力します。
無事ログイン出来ました。
では
この記事が気に入ったらサポートをしてみませんか?