AbstractBaseUser を継承したカスタムユーザーで E-mail ログインを実現する

# 動作環境とこれまでの経緯

- ホスト環境
    - macOS Catalina 10.15.7
    - VirtualBox 6.1.14
- ゲスト環境
    - Ubuntu Server 20.04.1 LTS
    - Docker 19.03.13
    - Docker Compose 1.27.3
- パッケージ
    - Django 3.1.4
    - uwsgi 2.0.18
    - mysqlclient 1.4.6
- これまでの経緯
    - [ローカルに Ubuntu Server を用意した](https://qiita.com/hajime-f/items/5d3373e98985776b4240)
    - [そこに Docker Compose をインストールした](https://qiita.com/hajime-f/items/2bd1cad93d3ae4a62be8)
    - [Django＋MySQL＋nginx の開発環境を Docker Compose で構築した](https://qiita.com/hajime-f/items/c40571647135df327aa7)
    - [Django を AWS Fargate で動作させようとした → 失敗した](https://qiita.com/hajime-f/items/4511f2aa036380249a3f)

# まだ本番環境いらないんじゃないか説
前回、[本番環境を作ろうとしたところ、アホみたいな理由で失敗しましたが](https://qiita.com/hajime-f/items/4511f2aa036380249a3f)、**コードを１行も書いてない現時点で本番環境などいらないんじゃないか**、という本質的なことに気づいたので、[前々回に Docker Compose で構築した開発環境](https://qiita.com/hajime-f/items/c40571647135df327aa7)でコーディングを進めたいと思います。

# 今回やりたいこと
[前々回](https://qiita.com/hajime-f/items/c40571647135df327aa7)で、最初のマイグレーションをして、```$ docker-compose up``` で Django のスタート画面を見たところから始めます。

今回やりたいことは、
- カスタムユーザのクラスを作る
- メールアドレスでログインできるようにする
の２つ。

[公式サイト](https://docs.djangoproject.com/ja/3.0/topics/auth/customizing/#substituting-a-custom-user-model)には AbstractUser を継承する例が記載されていますが、User の内容をグリグリいじりたいので、今回は AbstractBaseUser を継承して作っていきます。

# 手順
1.「users」という名前のアプリを作成する
2. ```users/models.py``` に AbstractBaseUser を継承した User クラスと、BaseUserManager を継承した UserManager クラスを作る
3. ```users/admin.py``` に UserAdmin を継承した AdminUserAdmin クラスを作る
4.「AUTH_USER_MODEL = 'users.User'」を定義する
5. マイグレーションして管理画面からカスタムユーザを登録できることを確認する

## 1. users アプリを作成する
次のコマンドを叩けば、「users」アプリが作成されます。

```bash
$ docker-compose -f docker-compose.dev.yml run python ./manage.py startapp users
```

今後、アプリをいろいろと開発していくのに、毎回長ったらしいコマンドを打つのが嫌だったので、```Makefile``` に追加しました。```Makefile``` の全内容については、[前回の記事](https://qiita.com/hajime-f/items/4511f2aa036380249a3f#makefile-%E3%81%AE%E4%BD%9C%E6%88%90)を読んでね。

```bash:Makefile（抜粋）
app:
	docker-compose -f docker-compose.dev.yml run python ./manage.py startapp $(APP_NAME)
```

これで ```$ make app APP_NAME=app``` でアプリケーションを新規作成できます。

忘れずに ```settings.py``` に「users」アプリをインストールしておきましょう。

```python:settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    #My applications
    'users',    # 追加
]
```

## 2. User クラスと UserManager クラスを作る
公式の [AbstructUser](https://github.com/django/django/blob/master/django/contrib/auth/models.py#L321) とか [AbstructBaseUser](https://github.com/django/django/blob/98ad327864aed8df245fd19ea9d2743279e11643/django/contrib/auth/base_user.py#L48) とか [UserManager](https://github.com/django/django/blob/master/django/contrib/auth/models.py#L129) とか[「完全な例」](https://docs.djangoproject.com/ja/3.0/topics/auth/customizing/#custom-users-admin-full-example)とかを参考にしながら試行錯誤した結果、最終的にこうなりました。

```python:users/models.py
import uuid
from django.db import models
from django.utils import timezone
from django.core.mail import send_mail
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.validators import UnicodeUsernameValidator

class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **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(email, password, **extra_fields)
    

class User(AbstractBaseUser, PermissionsMixin):

    uuid = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    username_validator = UnicodeUsernameValidator()
    
    username = models.CharField(
        _('username'),
        max_length=150,
        blank=True,
        null=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."),
        },
    )
    
    last_name = models.CharField(_('姓'), max_length=150)
    first_name = models.CharField(_('名'), max_length=150)
    last_name_kana = models.CharField(_('姓（かな）'), max_length=150)
    first_name_kana = models.CharField(_('名（かな）'), max_length=150)
    old_last_name = models.CharField(_('旧姓'), max_length=150, blank=True, null=True)
    old_last_name_kana = models.CharField(_('旧姓（かな）'), max_length=150, blank=True, null=True)
    email = models.EmailField(_('メールアドレス'), unique=True)

    sex = models.CharField(_('性別'), max_length=4, choices=(('男性','男性'), ('女性','女性')))
    birthday = models.DateField(_('生年月日'), blank=True, null=True)

    country = models.CharField(_('国'), default='日本', max_length=15, editable=False)
    postal_code = models.CharField(_('郵便番号（ハイフンなし）'), max_length=7, blank=True, null=True)
    prefecture = models.CharField(_('都道府県'), max_length=5, blank=True, null=True)
    address = models.CharField(_('市区町村番地'), max_length=50, blank=True, null=True)
    building = models.CharField(_('建物名'), max_length=30, blank=True, null=True)
    tel = models.CharField(_('電話番号（ハイフンなし）'), max_length=11, blank=True, null=True)
    
    url = models.URLField(_('URL'), max_length=300, blank=True, null=True)
    photo = models.ImageField(_('写真'), blank=True, null=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()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        db_table = 'User'
        verbose_name = _('user')
        verbose_name_plural = _('ユーザー')
    
    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self):
        full_name = '%s %s' % (self.last_name, self.first_name)
        return full_name.strip()

    def get_full_name_kana(self):
        full_name_kana = '%s %s' % (self.last_name_kana, self.first_name_kana)
        return full_name_kana.strip()
    
    def get_short_name(self):
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        send_mail(subject, message, from_email, [self.email], **kwargs)
```

本来なら、[このあたりの記事に書いてあるように](https://blog.narito.ninja/detail/86/)ちゃんとした多言語対応が必要なのですが、そんな小難しいことは必要になったときに考えればいいという割り切りのもと、各フィールドには日本語をそのまま書いてます。**こまけぇこたぁいいんだよ！！**

さて、ポイントは２つあります。

1. uuid を primary key に設定している
これは、Django をあとから REST 化しようと考えているからです。REST 化すると URL で操作対象のユーザを特定することになるので、id を primary key にするとセキュリティ的によくないんですね。なので、RESTful な設計にするときは、uuid を primary key にすることがベストプラクティスです。

2. UserManager から username を削除している
これは、メールアドレスでログインできるようにしたいからです。User モデルの方でも「USERNAME_FIELD」には email を設定し、「REQUIRED_FIELDS」は空に設定してあります。

## 3. AdminUserAdmin クラスを作る
これもいろいろと試行錯誤した結果、次のとおりになりました。

```python:users/admin.py
from .models import User
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _

@admin.register(User)
class AdminUserAdmin(UserAdmin):

    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('last_name', 'first_name', 'last_name_kana', 'first_name_kana', 'old_last_name', 'old_last_name_kana', 'email', 'sex', 'birthday', 'postal_code', 'prefecture', 'address', 'building', 'tel', 'url', 'photo')}),
        (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
                                       'groups', 'user_permissions')}),
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
    )
    list_display = ('get_full_name', 'get_full_name_kana', 'email', 'sex', 'birthday', 'postal_code', 'prefecture', 'address', 'building', 'tel', 'is_staff',)
    search_fields = ('username', 'email',)
    ordering = ('date_joined',)
    filter_horizontal = ('groups', 'user_permissions',)
```

## 4. 「AUTH_USER_MODEL = 'users.User'」を定義する
認証の対象となるユーザモデルは、「users.User」であることを ```settings.py``` に定義します。

```python:settings.py
AUTH_USER_MODEL = 'users.User'
```

## 5. マイグレーションして管理画面からカスタムユーザを登録できることを確認する
次のコマンドを叩けば、マイグレーションできます。

```bash
$ docker-compose -f docker-compose.dev.yml run python ./manage.py migrate
```

これもコマンドが長くて面倒なので、```Makefile``` を作っておきます。

```bash:Makefile（抜粋）
migrate:
	docker-compose -f docker-compose.dev.yml run python ./manage.py migrate
```

これで ```$ make migrate``` でマイグレーションできます。簡単だね。

マイグレーションに成功したら、管理画面には、メールアドレスでログインするようになります。
![スクリーンショット 2020-12-29 22.13.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667942/61041354-1ff5-950f-2a5a-01d48d07b32e.png)

また、カスタムしたとおりにユーザを登録することができます。
![スクリーンショット 2020-12-29 22.10.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667942/d4121fb5-95d6-889d-b628-76b48da1660a.png)

ちなみに、私の会社アドレスは上記のとおりなので、**[ウチの会社](https://www.heartorg.co.jp/)でエンジニアやりたいって人は気軽に連絡ください**。

# 今回苦しんだバグ
```bash
Migration admin.0001_initial is applied before its dependency account.0001_initial on database
```

**「なんじゃこりゃぁあ！！！」**と、[私の中の松田優作](https://dic.nicovideo.jp/a/%E3%81%AA%E3%82%93%E3%81%98%E3%82%83%E3%81%93%E3%82%8A%E3%82%83%E3%81%81%E3%81%82%21%21%21#h2-1)が叫びに叫んだバグ。

光の速さでググりまくったところ、同じバグに直面した先人たちが多数。どうやら、カスタムユーザを作ったら必ずマイグレーションに失敗するらしい。Django さんサイドの問題やないかこれ。

ここは先人たちの教えどおりに対処しよう。

```python:settings.py
INSTALLED_APPS = [
    # 'django.contrib.admin', # コメントアウト
    ...
]
```

```python:urls.py
urlpatterns = [
    # path('admin/', admin.site.urls), # コメントアウト
    ...
]
```

この２ヶ所をコメントアウトしてマイグレーションした後、元に戻して再度マイグレーションするとうまくいきました。

……と、こういう書き方をすると、まるであっという間にバグを潰したかのように見えますが、実際は User を作る過程と合わせてかなり試行錯誤しました。だって、あちこちいじったら、いろんなエラー出るんだもん。。

というわけで、例によってコマンドを簡略化するための ```Makefile``` を作りました。

```bash:Makefile（抜粋）
all_clear:
	docker-compose -f docker-compose.dev.yml down
	docker volume rm app.db.volume
	find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
	find . -path "*/migrations/*.pyc" -delete
```

開発環境といえども、できれば使いたくない滅びの呪文です。この呪文を唱えると、コンテナで動く DB の中身が全部ブッ飛ので、最初のマイグレーションとスーパーユーザーの作成まで戻ってしまいます。

こんなメガンテを唱えずにすむように、静かに祈りましょう。

# 参考

- [カスタムの User モデルを置き換える](https://docs.djangoproject.com/ja/3.0/topics/auth/customizing/#substituting-a-custom-user-model)
- [現場で使える Django の教科書《実践編》](https://www.amazon.co.jp/gp/product/B07L3DRGBT/)
- [Django ユーザー カスタマイズ方法](https://qiita.com/okoppe8/items/10ae61808dc3056f9c8e)
- [#django | AbstractUser でカスタムユーザーを作ってマイグレーションするまでの手順のデモ](https://qiita.com/YumaInaura/items/3df369cdb4c97695ba48)
- [DjangoでUserモデルのカスタマイズ](https://blog.narito.ninja/detail/39)
- [Django は必ず CustomUserモデルを使え！という話](https://hackmd.io/@AKLoPcIvSIqmV3io_RigWQ/Bkzxn9u0B)
- [Djangoでメールアドレスとパスワードでログインしてみる](https://qiita.com/cortyuming/items/2167a29a90c94bb4b1bb)
- [Django、多言語対応](https://blog.narito.ninja/detail/86/)
- [Djangoでカスタムユーザーのマイグレーションに失敗するときの対処法](https://kimuson.dev/blog/django/django_migrate_custom_user/)