PUROGU LADESU

ポエムがメインのブログです。

【Django】カスタムユーザーモデルを使う方法

User

名前はCustomUserとし、単純のためBaseUserManagerを継承することとします。
だだ含まれるフィールドが受け継がれてしまうので、すべてカスタムしたい場合はAbstractBaseUserを継承しましょう。

settings.pyにUserモデルの指定を追加する

AUTH_USER_MODEL = 'accounts.CustomUser'

Manager

これはデータベースにアクセスするためのものです。Userのobjectsにオーバーライドします。
ManagerもデフォルトのUserManagerでなくCustomUserManagerに変更します。
CustomUserManagerはBaseUserManagerを継承します。
ベースはUserManagerからcreate_superuser, create_user, _create_userをコピペして修正しましょう。

_create_userの修正点として、外部参照であるDepartment, UserPowerにデフォルト値を設定します。
上記にはあらかじめデータを作成しておくこと。
必須項目であるemployee_nameも入力チェックをしたいのでextra_fieldsの中をチェックしてエラーを出します。

またpasswordの入力がない場合も、エラーを出すようにします。
なぜかデフォルトのmake_password関数ではパスワード無しでもエラーにならず、
Noneが渡されることでログインできないパスワードが作成されてしまう可能性があります。
なぜこの仕様になっているのかよくわかりません。

Noneが渡されると!SamgQKLYDaqs87093・・のようなパスワードが生成され、
デフォルトハッシュアルゴリズムのpbkdf2_sha256のハッシュ、ストレッチ回数、ソルトを含むパスワードにならないです。

class Department(models.Model):
    code = models.IntegerField("コード", unique=True)
    name = models.CharField("名称", max_length=50)

    def __str__(self) -> str:
        return f"{self.code} - {self.name}"


class UserPower(models.Model):
    code = models.IntegerField("コード", unique=True)
    name = models.CharField("名称", max_length=50)

    def __str__(self) -> str:
        return f"{self.code} - {self.name}"


class CustomUserManager(BaseUserManager):
    def _create_user(self, username, email, password, **extra_fields):
        """
        Create and save a user with the given username, email, and password.
        """
        # 必須フィールドが未入力の場合エラーにする
        if extra_fields.get('employee_name') is None:
            raise ValueError("employee_name must be set")
        if not username:
            raise ValueError("The given username must be set")
        
        # エラーにすべき
        if not password:
            raise ValueError("The given password must be set")
        
        # デフォルト値の設定
        extra_fields.setdefault("department", Department.objects.get(code=0))
        extra_fields.setdefault("power", UserPower.objects.get(code=0))

        email = self.normalize_email(email)
        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)


class CustomUser(AbstractUser):
    employee_name = models.CharField("社員名", max_length=50)
    department = models.ForeignKey(
        "Department", verbose_name="部署", on_delete=models.PROTECT
    )
    power = models.ForeignKey(
        "UserPower", verbose_name="権限", on_delete=models.PROTECT
    )
    entry_date = models.DateField("入社日", null=True)
   
    REQUIRED_FIELDS = ["employee_name"]

    objects = CustomUserManager()

管理画面の編集

管理画面では必須項目や外部参照キーが入力できないので登録するとエラーになります。
ユーザー追加画面項目を定義しているadd_fieldsetsに追加します。

class CustomUserAdmin(UserAdmin):
    list_display = ["id", "username", "employee_name", "department", "power", "is_superuser", "is_staff"]
    add_fieldsets = (
        (
            None,
            {
                "classes": ("wide",),
                "fields": (
                    "username",
                    "password1",
                    "password2",
                    # 追加必須フィールド
                    "employee_name",
                    "department",
                    "power",
                ),
            },
        ),
    )


admin.site.register(CustomUser, CustomUserAdmin)


いろいろハマったけど、authenticateやパスワードハッシュの仕様について勉強になった。
複数バックエンドを用意して順番に認証していくこともできるし、パスワードハッシュの変更の仕組みもあるようだ。