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

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

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

https://qiita.com/mkthrkw/private/588226d30690c707d2b8

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

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

https://qiita.com/sotaheavymetal21/items/ef05372462dd79086a57

## パッケージインストール

``` pip install ulid-py ```

https://pypi.org/project/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　：　ユーザーが全てのパーミッションを持っているか

``` python: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をオーバーライド
・スーパーユーザーとそれ以外で表示するフィールドを切り替え

``` python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680973/d8d67fde-3f4b-8427-e539-52ac7dfa6366.png)

## おまけ
Documentでは、アプリでは User を直接参照する代わりに、django.contrib.auth.get_user_model() を使ってユーザモデル参照を推奨されていた。
``` python
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,
    )
```