見出し画像

【IT】Djangoでのカスタムユーザ

皆さま
こんにちは

Djangoでは、標準でユーザー管理モデルがあり
ユーザ認証や、登録、管理が行えます。

しかしながら公式サイトでは、そのまま使うことは
推奨されずカスタマイズすること推奨されております。

プロジェクトの開始時にカスタムのユーザーモデルを使用する¶

新しくプロジェクトを始める場合は、デフォルトの User で十分である場合でも、カスタムユーザーモデルを作成することを強く推奨します。このモデルはデフォルトのユーザーモデルと同様に動作しますが、必要に応じて将来的にカスタマイズすることができます:


今回は、カスタムユーザモデルを使う上ので準備を行います。
認証ユーザを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.

次にアプリを起動し、メールアドレスでログインできるか確認します。

メールアドレスとパスワードを入力します。

無事ログイン出来ました。

では

この記事が気に入ったらサポートをしてみませんか?