0
0

[Django] カスタムユーザーモデルを使ってみる

Last updated at Posted at 2024-04-13

はじめに

自分用の個人開発のメモ/備忘録として記録していきます。

環境

django5.0
↓ dockerでの開発環境作成はこちら

やりたいこと

E-mailとPasswordでの認証に変更し、IDをULIDへ変更したい。

ULIDについて

UUIDと同様に一意性保証されていて、生成時刻のミリ秒単位で時系列ソートが可能なULIDを選択

パッケージインストール

pip install ulid-py

django-ulidを使おうと思ったが、フィールドを
ULIDField(default=default, primary_key=True, editable=False)
にするとなぜか badly formed hexadecimal UUID string のエラーが発生する。

カスタムユーザークラス作成

usersアプリを作成する

名前は任意なのでusersではなくてもよい。
python manage.py startapp users

models.pyにカスタムユーザークラスを作成

・認証時にemailを利用
・ユーザー名(認証用ではなく任意)としてnameを追加
・created_atとupdated_atを追加
・is_staff : ユーザーが admin サイトへのアクセス権を持っているか
・is_active : ユーザーアカウントが現在アクティブか
・is_superuser : ユーザーが全てのパーミッションを持っているか

models.py
from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
import ulid

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None):
        if not email:
            raise ValueError("Users must have an email address")
        user = self.model(
            email=self.normalize_email(email),
        )
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None):
        user = self.create_user(
            email,
            password=password,
        )
        user.is_staff = True
        user.is_superuser = True
        user.save(using=self._db)
        return user


class CustomUser(AbstractBaseUser, PermissionsMixin):
    id = models.CharField(default=ulid.new, max_length=26, primary_key=True, editable=False)
    email = models.EmailField(
        verbose_name="email address",
        max_length=255,
        unique=True,
    )
    name = models.CharField(
        verbose_name="user name",
        max_length=30,
        null=True,
        blank=True
    )
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    last_login = models.DateTimeField(null=True, blank=True)
    created_at  = models.DateTimeField(auto_now_add=True)
    updated_at  = models.DateTimeField(auto_now=True)

    objects = CustomUserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    def __str__(self):
        return self.email

settings.pyの設定変更

customauthをアプリとして追加

INSTALLED_APPS = [
    ...
    # Local apps
    'users.apps.UsersConfig',
]

認証ユーザーモデルを作成したカスタムユーザークラスへ変更

# Custom settings
AUTH_USER_MODEL = 'users.CustomUser'

admin.pyを作成

カテゴリ is_superuser is_staff  閲覧   新規追加  編集   PW変更  権限変更
スーパーユーザー TRUE TRUE
一般スタッフ FALSE TRUE × × ×
それ以外 FALSE FALSE × × × × ×

※基本的にはスーパーユーザーのみが、ユーザー管理を行うというもの
※一般スタッフは基本情報確認のみという考えで設定

class UserCreationForm

・新規登録画面
・forms.ModelFormをベースにしている
・passwordとpassword_confirmフィールドは入力内容を隠すためforms.PasswordInputとする
・作成画面で、パスワード確認欄を追加して一致しない場合はエラーを表示

class UserChangeForm

・一般スタッフ用編集画面
・forms.ModelFormをベースにしている
・パスワードは読み取れないフィールドとしている

class UserChangeFormOnlySuperuser

・スーパーユーザー用の編集画面
・UserCreationFormをベースにパスワード入力のrequiredをfalseにした

class UserAdmin

save_modelをオーバーライド

・スーパーユーザーかつパスワードの入力がある場合は、入力値を保存
※新規作成はスーパーユーザーのみを想定
・それ以外は元のパスワードを保持

get_formをオーバーライド

・スーパーユーザーとそれ以外で編集用フォームを切り替え

add_viewをオーバーライド

・新規登録はスーパーユーザーのみとなり、権限のないアクセスはパーミッションエラーを出す

get_fieldsetsをオーバーライド

・スーパーユーザーとそれ以外で表示するフィールドを切り替え

admin.py
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError
from users.models import CustomUser
from django.core.exceptions import PermissionDenied

class UserCreationForm(forms.ModelForm):
    password = forms.CharField(label="Password", widget=forms.PasswordInput)
    password_confirm = forms.CharField(
        label="Password confirmation", widget=forms.PasswordInput
    )

    class Meta:
        model = CustomUser
        fields = ["email", "password", "name", "is_staff", "is_superuser", "is_active"]

    def clean_password_confirm(self):
        password = self.cleaned_data.get("password")
        password_confirm = self.cleaned_data.get("password_confirm")
        if (password or password_confirm) and password != password_confirm:
            raise ValidationError("Passwords don't match")
        return password_confirm

class UserChangeForm(forms.ModelForm):
    password = ReadOnlyPasswordHashField()
    class Meta:
        model = CustomUser
        fields = ["email", "name"]

class UserChangeFormOnlySuperuser(UserCreationForm):
    password = forms.CharField(label="Password", widget=forms.PasswordInput, required=False)
    password_confirm = forms.CharField(
        label="Password confirmation", widget=forms.PasswordInput, required=False
    )

class UserAdmin(BaseUserAdmin):
    form = UserChangeForm
    add_form = UserCreationForm
    list_display = ['id', 'email', 'name', "is_staff", 'is_superuser', 'is_active', 'last_login', 'created_at', 'updated_at']
    list_filter = ["is_staff", "is_superuser","is_active"]
    search_fields = ["email", "name"]
    ordering = ["created_at"]
    filter_horizontal = []

    def save_model(self, request, obj, form, change):
        if request.user.is_superuser and obj.password:
            obj.set_password(obj.password)
        else:
            obj.password = form.initial.get("password")
        super().save_model(request, obj, form, change)

    def get_form(self, request, obj=None, **kwargs):
        if request.user.is_superuser:
            kwargs["form"] = UserChangeFormOnlySuperuser
        else:
            kwargs["form"] = UserChangeForm
        return super().get_form(request, obj, **kwargs)

    def add_view(self, request, form_url='', extra_context=None):
        if not request.user.is_superuser:
            raise PermissionDenied
        return super().add_view(request, form_url, extra_context)

    def get_fieldsets(self, request, obj=None):
        if request.user.is_superuser:
            return (
                (None, {"fields": ["email", "password", "password_confirm"]}),
                ("Personal info", {"fields": ["name"]}),
                ("Permissions", {"fields": ["is_staff", "is_superuser", "is_active" ,"groups"]}),
            )
        else:
            return (
                (None, {"fields": ["email", "password"]}),
                ("Personal info", {"fields": ["name"]}),
            )

# Now register the new UserAdmin...
admin.site.register(CustomUser, UserAdmin)

マイグレーション

※必要に応じてDBリセット
python manage.py makemigrations
python manage.py migrate

管理画面に入ってみる

スーパーユーザーを作成

python manage.py createsuperuser

管理画面へログイン

http://localhost:*****/admin

image.png

おまけ

Documentでは、アプリでは User を直接参照する代わりに、django.contrib.auth.get_user_model() を使ってユーザモデル参照を推奨されていた。

from django.conf import settings
from django.db import models


class Article(models.Model):
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0