Django ユーザー カスタマイズ方法

## この記事について

Django の ユーザーのカスタマイズ方法についてまとめました。
公式サイトではこのあたりの内容です。
https://docs.djangoproject.com/ja/2.0/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project

こちらのエントリで指摘している通り、Djangoでは基本的にカスタマイズしたUserを使うべきと思います。
参考：[Djangoでは常にカスタムUserを使用すべき](https://qiita.com/NAKKA-K/items/7627b6a22f364941b989)

バージョン
Django 2.0.1


## カスタマイズ方法

基本的に以下の作業手順ですすめます。

１．`users`という名前でアプリケーションを作成
２．users.models にAbstractBaseUserを継承したUserクラスを作成
３．users.admin に UserAdmin を継承したクラスを作成。
４．設定ファイルに「AUTH_USER_MODEL = 'users.User'」を定義
５．django.contrib.admin、django.contrib.authへの影響範囲を調べ、必要であれば他のクラスも継承して独自に修正。

アプリケーションを独立させると、他プロジェクトでの再利用性と、アプリケーション単位でダンプファイル操作が可能になるメリットがあります。

「users」はcookiecutter-djangoでも使われている名前です。こちらで統一するのがよいと思います。
参考：[cookiecutter-djangoを使ってみた](https://qiita.com/ping2shi2/items/e3366bf9600bddeede07)


### AbstractUser か AbstractBaseUser か

ユーザーモデルのカスタマイズ方法にはAbstractUserを継承する方法とAbstractBaseUserを継承する方法があります。

AbstractUserは抽象クラスAbstractBaseUserの実装です。
AbstractUserもとにカスタマイズすると柔軟性が低いがコーディング量は少なく、AbstractBaseUserを使うと柔軟性が高いがコーディング量は多くなるという関係です。

カスタマイズの内容が属性の追加変更のみのときはAbstractUser、属性の削除またはAbstractUserのカスタマイズで収まらないときはAbstractBaseUserを使うことが推奨されています。

しかし、私見ですが、どのような状況でもAbstractBaseUserを利用したほうがいいと思います。その理由は以下の３点です。

・AbstractBaseUserの方が柔軟性がある
・コードが分散しないので変更時に修正場所がわかりやすい
・デフォルトの属性（first_name、last_name、email）を使わない場合すぐ消せる

「AbstractUser vs AbstractBaseUser」というキーワードで検索すると色々考察がでてきます。自分に適した方法を採ってください。

https://stackoverflow.com/questions/21514354/difference-between-abstractuser-and-abstractbaseuser-in-django

## サンプルコード

練習として、需要ありそうな要件でカスタマイズをしてみます。

・idをUUIDに変更
・first_nameとlast_nameは削除し、full_nameを追加
・所属（department）を追加。兼任できるようにNxN関係とする。



ログインIDをメールアドレスにする変更はこちらのページで紹介されています。
[naritoブログ Djangoで、Userモデルのカスタマイズ（継承）](https://torina.top/detail/350/)


```py:project/setting.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users',
]

# Application definition
AUTH_USER_MODEL = 'users.User'

```

```py:users/models.py

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.core.mail import send_mail
import uuid as uuid_lib

# Create your models here.


class Department(models.Model):
    """所属 兼任可"""

    name = models.CharField(_('所属'), max_length=150, blank=True)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = _('所属')
        verbose_name_plural = _('所属')


class User(AbstractBaseUser, PermissionsMixin):
    """ユーザー AbstractUserをコピペし編集"""

    uuid = models.UUIDField(default=uuid_lib.uuid4,
                            primary_key=True, editable=False)
    username_validator = UnicodeUsernameValidator()

    username = models.CharField(
        _('username'),
        max_length=150,
        unique=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."),
        },
    )
    full_name = models.CharField(_('氏名'), max_length=150, blank=True)
    email = models.EmailField(_('email address'), blank=True)
    departments = models.ManyToManyField(
        Department,
        verbose_name=_('所属'),
        blank=True,
        help_text=_('Specific Departments for this user.'),
        related_name="user_set",
        related_query_name="user",
    )

    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()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email', ]

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)

    # 既存メソッドの変更
    def get_full_name(self):
        return self.full_name

    def get_short_name(self):
        return self.full_name

```

```py:users/admin.py

from .models import User, Department
from django.contrib.auth.admin import UserAdmin
from django.contrib import admin
from django.utils.translation import gettext, gettext_lazy as _

# Register your models here.
@admin.register(Department)
class AdminDepartment(admin.ModelAdmin):
    pass

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

    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('full_name', 'email','departments')}),
        (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
                                       'groups', 'user_permissions')}),
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
    )
    list_display = ('username', 'email', 'full_name', 'is_staff')
    search_fields = ('username', 'full_name', 'email')
    filter_horizontal = ('groups', 'user_permissions','departments')


```

この通りカスタマイズできました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/a1c2d171-41ae-2f0c-01a2-8cf973855c99.png)


## 参考

admin.ModelAdmin(UserAdminのスーパークラス)のカスタマイズについてはこちらのエントリが参考になります。

[Django 管理画面逆引きメモ](https://qiita.com/zenwerk/items/044c149d93db097cdaf8)