djangoのCustomUserについてまとめる

今回のお題
-
今回はdjangoのCustomUserについてまとめます。

CustomUserとは、djangoのユーザー管理機能を自分のプロジェクト用にカスタマイズすることを言います。

djangoにはallauthというユーザー認証パッケージがありますが、ここにフィールドを追加したりバリデーションの条件を変更したりすることで、より実践的なアプリになるというわけです。

なお、今回はallauth自体の導入手順は省略します。

そちらに関しては[以前の記事](https://qiita.com/keishi04hrikzira/items/54b32bca4596ca9820da)をご参照ください。

目次
-

- はじめに
- settings.pyの編集
- CustomUserModelの作成
- CustomUserFormの作成
- adapterの作成
- ルーティング、viewsの作成
- テンプレートの作成
- マイグレーション
- 終わりに

はじめに
-
CustomUserの実装に取り掛かかる前にまず、最も大事なことを2つ話しておきます。

まず一つ目は、　
>フィールドの追加などをしない場合であっても、とりあえずはCustomUserを作成しておいた方が良い

ということです。

そもそもCustomUserの実装とは具体的に何なのかというと、DBが参照するUserモデルやUserFormをデフォルトのものから自分で定義したもの(CustomUser)に切り替えるという作業になります。

もしデフォルトのUserモデルやUserFormを参照した状態で開発を進めたあとで何らかの事情によりフィールドやバリデーションの追加が必要になったときに、CustomUserを作って参照先をそちらに切り替えて、というのは非常に手間がかかります。

しかしあらかじめCustomUserを参照するようにしておけば、後から必要になったフィールドなりバリデーションなりを追加するだけですみます。

この違いは非常に大きいので、基本的には既存のUserモデルで事足りる場合であってもとりあえずCustomUserを作成しておくことになっているというわけです。


大事なことの二つ目は、
>CustomUserの実装が完了するまではマイグレーションを実行しない
という

ことです。

一つ目の注意事項とも関連しますが、一度マイグレーションを実行した時点でDBの参照先が既存のUserモデルに固定されます。

これを後からCustomUserに切り替えるのは骨が折れるため、中途半端な状態でマイグレーションをするなとあちこちのサイトで書かれています。

では、前置きはこれぐらいにして本題に入ります。

やるべきことは先述の通りでCustomUserとCustomUserFormを作りそちらを参照できるようにすることです。

settings.pyの編集
-
まずはsettings.pyに以下の用に追記してください。

このようにすることで、DBが参照するモデルやサインアップ時に利用するフォームを既存のものから切り替えることができます。

```python:settings.py
# CustomUserの利用
# 値は"ユーザー管理用アプリ名.自分で作成するユーザーモデル"
AUTH_USER_MODEL = "accounts.CustomUser" 

# サインアップフォームの指定
# こちらは"アプリ名.forms.モデルフォーム名"なので注意
ACCOUNT_FORMS = {
    "signup": "accounts.forms.CustomUserCreationForm",
}

# アダプターの指定
ACCOUNT_ADAPTER = 'accounts.adapter.AccountAdapter'
```
アダプターはサインアップ機能の実装に必要になります。

参照先が指定できたら、次はCustomUserモデルを作ります。

CustomUserモデルの作成
-
CustomUserモデルを作成する際には、既存のAbstractUserモデルかAbstractBaseUserモデルを継承します。

ただし後者はほとんど1からモデルを定義していく形になり、拡張性と引き換えに手軽さが犠牲になっています。

前者では自由度が足りないという場合には導入の余地があると思いますが、優先順位は低いのではないでしょうか。

今回もAbstractUserの方を使います。

```python:accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.core.validators import MinValueValidator, MaxValueValidator

class CustomUser(AbstractUser):
  # フィールドを追加しない場合はpassでOK
  # pass
  # フィールド追加がある場合はそれを記述
  age = models.IntegerField(
    verbose_name="年齢",
    null=Falser,
    validator=[
      MaxValueValidator(150, "１５０以下の数字で入力してください"),
      MinValueValidator(0, "0以上の数字で入力してください"),
    ]
  )
```

これでCustomUserが作れたので、次にformを作ります。

CustomUserFormの作成
-
今回は、サインアップとユーザー情報編集の2つのフォームを作ります。

djangoにはあらかじめUserCreationForm, UserChangeFormという二つのフォームが用意されているので、そちらを継承してCustomFormを作成していきます。

```python:accounts/forms.py
from django.contrib.auth import get_user_model

from django.contrib.auth.forms import UserCreationForm, UserChangeForm

class CustomUserCreationForm(UserCreationForm):
  
  def __init__(self, *args, **kwargs): # emailの登録を必須に変更
      super().__init__(*args, **kwargs)
      self.fields["email"].required = True
  
  class Meta:
    model = get_user_model()
    fields = ["username", "email", "age"] # passwordは設定しない
    labels = {
      "username": "ユーザー名",
      "email": "メールアドレス",
      "age": "年齢",
    }
    help_texts = {
      "username": "",
      "email": "",
      "age": "",
    }

class CustomUserChangeForm(UserChangeForm):
  def __init__(self, *args, **kwargs): # emailの登録を必須に変更
      super().__init__(*args, **kwargs)
      self.fields["email"].required = True

  class Meta:
    model = get_user_model()
    fields = ["username", "email", "age"] # passwordは設定しない
    labels = {
      "username": "ユーザー名",
      "email": "メールアドレス",
      "age": "年齢",
    }
    help_texts = {
      "username": "",
      "email": "",
      "age": "",
    }
```
以下、個別解説です。

CustomUserCreationFormで入力するフィールドの中にパスワードは入れないでください。

パスワードフォームに関しては別で管理されているらしく、フォームのフィールドとして改めて設定すると入力欄が二つになります。

```python:サインアップフォームのフィールド
fields = ["username", "email", "age"] # passwordは設定しない
```

また、CustomUserChangeFormについてはパスワードをフィールドに含めても以下の用に表示されるだけなので、含めるかどうかはお好みで良いかと思います。
[![Image from Gyazo](https://i.gyazo.com/6029579072fc0dd40f406ccd26eb52cb.png)](https://gyazo.com/6029579072fc0dd40f406ccd26eb52cb)

また、今回はemailの登録を必須に変更しました。
デフォルトのUserモデル、UserFormではemailなどのいくつかのフィールドは必須とはなっていないので、そこを必須化したい場合には以下の用に編集する必要があります。

```python:required属性の変更
  def __init__(self, *args, **kwargs): # emailの登録を必須に変更
      super().__init__(*args, **kwargs)
      self.fields["email"].required = True
```

:::note alert
上記のCustomFormについてはmodels.pyには記述せず、CustomUserとはファイルを分けるようにしてください。
同じファイルになっているとマイグレーションが失敗する恐れがあります。
詳しくは以下の参考記事をご覧ください。
:::

https://qiita.com/keishi04hrikzira/items/854e934e79e3dfd09ee2

adapterの作成
-
次に、adapterというものを作成します。

adaptorとは、ユーザー新規登録時に呼び出されるsave_userメソッドを定義しているファイルになります。

新しく追加したageというフィールドはsave_userメソッドで保存されるようになっていないので、ここを上書きして年齢を登録できるようにします。


settings.pyを編集した際にアダプターの参照先も変更しているので（以下）、

```python:settings.py
# アダプターの指定
ACCOUNT_ADAPTER = 'accounts.adapter.AccountAdapter'
```
accounts/adopter.pyにAccountAdapterクラスを作成すればユーザー登録の際にはそちらが参照されるようになります。

```python:accounts/adapter.py
from allauth.account.adapter import DefaultAccountAdapter

class AccountAdapter(DefaultAccountAdapter):
  def save_user(self, request, user, form, commit):
    user = super().save_user(request, user, form, commit=False)
    user.age = form.cleaned_data.get("age")
    user.save()
```
なお、上記で用いているcleaned_dataについては[こちら](https://qiita.com/keishi04hrikzira/items/07f09b65a1f2295bc1cd)

引数のcommitはsave_userメソッドを用いてインスタンスに属性値をセットした後にそのまま保存まで行うかどうかを決める値です。

今回はageフィールドに年齢を設定するまでは保存を行いたくない（保存が実行されるとバリデーションエラーになる）のでcommit=falseに設定しています。

`commit=false`についての解説は以下をご覧ください。

https://qiita.com/keishi04hrikzira/items/371cb5f15e9f97641cf3

サインアップについてはテンプレートとビューは予め用意されているので、このadapterの追加を以て実装完了となります。

ユーザー情報の変更についてはこの後ビューとテンプレートを実装します。

ルーティング、viewsの作成

```python:accounts/urls.py
urlpatterns = [
  path("edit/<int:pk>/", views.edit, name="accounts_edit"),
]
```

```python:accounts/views.py
def edit(request, pk):
  target_user = User.objects.filter(id=pk).first()
  if request.method == "GET":
    form = CustomUserChangeForm(instance=target_user)
    return render(request, "accounts/edit.html", { "target_user": target_user })
  elif request.method ==POST:
    form = CustomUserChangeForm(request.POST, instance=target_user)
    if form.is_valid:
      form.save()
      return render(request, "home.html")
    else:
      return render(request, "accounts/edit.html", { "target_user": target_user })
```

テンプレートの作成
-
ビューから渡されたフォームを表示するだけなので、以下のように書くだけです。

```django:accounts/edit.html
<form>
  {{ form }}
</form>
```

マイグレーションの実行
-
ここまで来たらマイグレーションを実行しましょう。

```shell
% python manage.py makemigrations accounts
% python manage.py migrate
```
なお、マイグレーションが失敗するという方は以下の記事をご参照ください。

https://qiita.com/keishi04hrikzira/items/854e934e79e3dfd09ee2

終わりに
-
これでCustomUserが実装できました。
ただ、フォームが汚いのが気になるので何とかしたいですね・・・