Djangoでカスタムユーザーについて甘く見て痛い目を見たという話



# はじめに

Djangoでユーザー登録・認証・ログイン機能周りを整備したいと思い、進めていたら詰まったので記録を残しておきます。

# 結論

**Djangoでアプリ開発をする際はprojectを立ち上げる前に要件定義と設計は済ませておくこと**

**設計等で織り込み済みでない限り、基本的には1project、1アプリを守るほうがDjangoの開発になれるまではベター。**

**勉強で開発をしているときもめんどくさがらずにprojectを再度立ち上げること、使い回さない。**

# 何が起きたか

Djangoチュートリアルも終わったので、[こちらの記事](https://qiita.com/okoppe8/items/54eb105c9c94c0960f14#%E6%89%8B%E9%A0%86%EF%BC%96%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E4%BD%9C%E6%88%90) と [こちらの記事](https://narito.ninja/blog/detail/38/) を元に、
Djangoでユーザー登録・認証・ログイン機能周りを整備したTODOアプリの雛形のようなものを作ろうと色々とやっていたのですが、面倒くさがりでチュートリアルのprojectを流用していたところ`User`モデル及び`SuperUser`の仕様周りで詰まってしまいました。

# Djangoでの会員登録機能

Djangoで会員登録機能を使う場合は、基本的にDjangoで用意されている`User`モデルを使います。
と、いうよりフレームワークを導入する以上はおそらくどのフレームワークであってもよほどのことがない限りは、フレームワーク側で用意されている認証の仕組みを使うことになり、それには間違いなく組み込みのモデルやメソッドがあるものです。

で、ここからが問題なのですがDjangoでは1つのProjectフォルダで複数のアプリケーションを管理することができ、逆に言うと複数のprojectフォルダに同じアプリケーションを導入することもできるようになっています。
しかし、実は`User`モデルはprojectにつき1つしか使えません。
これは厳密に言うとDjangoは導入にあたってUserモデルをもとにカスタムユーザモデルを作ることを強く推奨していて、さらに初回のマイグレーションまでにそれが作成されてなければなりません。
その際にUserモデルを認証機能て扱うために`settings.py`で設定する`AUTH_USER_MODEL`の部分を設定するのですが、これをあとから変更するのはprojectを立ち上げ直した方が早いレベルで手間がかかります。
[参考](https://docs.djangoproject.com/ja/3.0/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project)

つまり、1つの`project`で複数のアプリを管理する場合は`project`を立ち上げた時点で既にどのアプリケーションに認証周りの機能をもたせるかということを決めておかないといけないということです。

これを守らない……つまりチュートリアルでUserクラスを定義してるのに、別のアプリケーションを作ってそこで認証機能をつけたくなった私やノリで`project`を立ち上げてそのまま勢いで`User`クラスを作ると大変なことになるということですね。

しかし、やってしまったものはしょうがないので今回は後学のためという意味も込めてどうにかして既存の`project`のままどうにかならないかやってみました。

# やったこと

- 既存の`User`モデルをカスタマイズする
- `User`モデルを拡張するモデルを新たに作成するアプリケーションフォルダで作成し`OneToOneField`を用いてリレーションする。
- カスタムしたUserモデルとUserモデルを拡張したモデルを管理画面に反映するためにUserモデルがある側の`admin.py`を編集する。

また、これに際してTODOアプリ側のモデルを機能で使うモデルと上記の`User`拡張モデル(ついでにメール認証フラグのためのレコードをオーバーライドしています)は別々のファイルにしてあります。

そして、`models.py`周りをいじるということは当然`makemigrations`をやり直すことになるので一応慎重にやっていきましょう。

#### 既存のUserモデルのカスタマイズ

<img width="198" alt="2020-05-03_00h05_45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428779/46758e90-9500-a706-d5c2-4f0192045bcf.png">

ディレクトリ図を書くのが億劫なので画像で失礼します。
src以下が開発環境でprojectフォルダ以下の`app`、`polls`、`todo`が各アプリケーションでUserモデルを定義したのは`app`、今回製作したいアプリケーションが`todo`となります。

まずは`User`モデル本体をカスタムしていきます。
[参考:DjangoでUserモデルのカスタマイズ](https://narito.ninja/blog/detail/39/)

<details><summary>app/models.py</summary><div>

```python

from django.db import models
from django.core.mail import send_mail
from django.contrib.auth.models import PermissionsMixin, UserManager
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.core.validators  import EmailValidator

class CustomUserManager(UserManager):

	use_in_migrations = True

	def _create_user(self,email,password, **extra_fields):
		if not email:
			raise ValueError('The given email must be set')

		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(emai, password, **extra_fields)

class User(AbstractBaseUser, PermissionsMixin):

	username = models.CharField(max_length=30, unique=True)
	email = models.EmailField(_('email address'), unique=True, validators=[EmailValidator('Invalid email address.')])
	first_name = models.CharField(_('first name'), max_length=30, blank=True)
	last_name = models.CharField(_('last name'), max_length=150, blank=True)


	is_staff = models.BooleanField(
		_('staff status'),
		default=False,
		help_text=_(
			'Designates whether this 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 = CustomUserManager()

	EMAIL_FIELD = 'email'
	USERNAME_FIELD = 'email'
	REQUIRED_FIELD = []

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

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


class SampleDB(models.Model):
	class Meta:
		db_table = 'sample_table' # tablename
		verbose_name_plural = 'sample_table' # Admintablename
	sample1 = models.IntegerField('sample1', null=True, blank=True) # 数値を格納
	sample2 = models.CharField('sample2', max_length=255, null=True, blank=True)

```
</div></details>



```python

class CustomUserManager(UserManager):

	use_in_migrations = True

	def _create_user(self,email,password, **extra_fields):
		if not email:
			raise ValueError('The given email must be set')

		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(emai, password, **extra_fields)
```

この部分はユーザー作成及びスーパーユーザーを作る際のメソッドをオーバーライドしています。
つまり、ここはDjangoの管理画面へのログインなどにも関わる部分になるので`django.contrib.auth.models `の`UserManager`の部分をコピペして必要なところを加筆修正する形をとりました。
ソースは [こちら](https://github.com/django/django/blob/master/django/contrib/auth/models.py)。



```python

class User(AbstractBaseUser, PermissionsMixin):

	# Userモデルの基本項目。
	username = models.CharField(max_length=30, unique=True)
	email = models.EmailField(_('email address'), unique=True, validators=[EmailValidator('Invalid email address.')])
	first_name = models.CharField(_('first name'), max_length=30, blank=True)
	last_name = models.CharField(_('last name'), max_length=150, blank=True)

	# adminサイトへのアクセス権をユーザーが持っているか判断するメソッド
	is_staff = models.BooleanField(
		_('staff status'),
		default=False,
		help_text=_(
			'Designates whether this 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 = CustomUserManager()

	# 平たくいうと上からメールドレスフィールド、ユーザー名として使うフィールド、スーパーユーザーを作る際に必ず入力するべきフィールドを指定している。
	EMAIL_FIELD = 'email'
	USERNAME_FIELD = 'email'
	REQUIRED_FIELD = []

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

	# メールの送信に関するメソッド
	def email_user(self, subject, message, from_email=None, **kwargs):
		send_mail(subject, message, from_email, [self.email], **kwargs)

```


こちらは同じく`django.contrib.auth.models `より`AbstractBaseUser`を継承しています。
`PermissionsMixin`はもとのクラスで継承があるのでこちらでも継承しています。
役割としては権限周りに関するメソッドの集まりみたいです。
`models.BooleanField`はモデルフィールドにおける`BooleanField`で`default`デフォルトの値を設定してTrue、Falseを返します。
参考元ではユーザー名の代わりにメールアドレスでログインするような処理をしたいので`username`フィールドを消して、`email`フィールドにその役割を担わせています。
その設定が`USERNAME_FIELD = 'email'`となります。
今回はこのTODOアプリは汎用的であるということと、これをもとにリファクタリングして機能を追加したものを作っていきたいので`username`を残してあります。

ちなみに

```python

def get_full_name(self):
        """Return the first_name plus the last_name, with a space in
        between."""
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

def get_short_name(self):
    """Return the short name for the user."""
    return self.first_name

# 他アプリケーションが、username属性にアクセスした場合に備えて定義しておく、アクセスがあったらメールアドレスを返す
@property
def username(self):
    return self.email



```

といった項目も必要に応じて追加します。
今回は`first_name`及び`last_name`フィールドは使わないので実装していませんが、仮に姓名を登録させるようなフォームを作る場合はあると便利だと思います。


#### Userモデルを拡張するモデルを新たに作成するアプリケーションフォルダで作成し`OneToOneField`を用いる

```todo/models/account.py

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

class Activate(models.Model):
	user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
	key = models.CharField(max_length=255, blank=True, unique=True)
	expiration_date = models.DateTimeField(blank=True, null=True)

	def __str__(self):
		return self.key


	class Meta:
		verbose_name = 'メール認証フラグ'
		verbose_name_plural = 'メール認証フラグ'

```

今度はTODOアプリケーション側に`User`モデルを拡張するようなモデルを作ります。
今回は`User`モデル側の情報の他にメール認証のためのフィールドを定義しておきます。
`OneToOneField`はあるモデルと1対1関係でリレーションを定義するための設定です。
ユーザーに対して認証フラグは1つなので1対1関係となるわけです。
多対1などは`ForeignKey`で定義することになるようです。

あとは分割したモデルをパッケージするために`models.py`を削除し、`models`フォルダを作成しその中に`__init__.py`を作成し、分割したモデルも同じフォルダに入れておきます。
`__init__.py`は以下のようにパッケージするモデルをインポートするように定義します。

```todo/models/__init__.py

from .todo import Todo
from .account import Activate

```
これでモデルの分割は完了です。


#### カスタムしたUserモデルとUserモデルを拡張したモデルを管理画面に反映するためにUserモデルがある側の`admin.py`に手を入れる。

あとは`admin.py`に手を入れてれば一段落となります。


```app/admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.utils.translation import ugettext_lazy as _
from .models import User
# todoアプリケーションのaccount.pyからActivateオブジェクトをインポート
from todo.models.account import Activate
from.models import SampleDB

class MyUserChangeForm(UserChangeForm):
    class Meta:
        model = User
        fields = '__all__'


class MyUserCreationForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('email',)

# リレーションしたメール認証フラグをUserモデルの管理画面でも扱えるようにする。
class ActivateInline(admin.StackedInline):
    model = Activate
    max_num = 1
    can_delete = False


class MyUserAdmin(UserAdmin):
    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
        (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
                                       'groups', 'user_permissions')}),
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'password1', 'password2'),
        }),
    )
    form = MyUserChangeForm
    add_form = MyUserCreationForm
    list_display = ('username','email', 'first_name', 'last_name', 'is_staff')
    list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups')
    search_fields = ('username','email', 'first_name', 'last_name')
    ordering = ('email',)
    # ActivateInlineクラスを指定しておく
    inlines = [ActivateInline]


admin.site.register(User, MyUserAdmin)
admin.site.register(SampleDB)


```

今度は`django.contrib.auth.admin.py`から借用し、これをオーバーライドして管理画面でカスタムユーザーモデルを表示できるようにします。
肝は`from todo.models.account import Activate`の部分。


```todo/admin.py

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.admin import UserAdmin as AuthUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.utils.translation import ugettext_lazy as _
from .models import Todo, Activate
from app.models import User

# Register your models here.

class TodoAdmin(admin.ModelAdmin):

	fields = ['todo_title', 'memo', 'datetime', 'tags']

	list_display = ('todo_title', 'datetime', 'created_at', 'tag_list')
	list_display_links = ('todo_title', 'datetime','tag_list')

	def get_queryset(self, request):
		return super().get_queryset(request).prefetch_related('tags')

	def tag_list(self, obj):
		return u", ".join(o.name for o in obj.tags.all())

	pass

admin.site.register(Todo,TodoAdmin)
admin.site.register(Activate)


```

このようにtodoアプリケーション側でも`Activate`モデルは管理できますが、リレーション関係でありかつ`User`モデルで管理した方が都合がよいので`from todo.models.account import Activate`でモデルをインポートして、
Userクラス側に`Inline`クラスを作っておくということですね。
ひとまずあとは作業を進めていく上でもし`USERNAME_FIELD = 'email'`他でエラーが出たらその都度対処をしていきます。

# 参考
[DjangoでUserモデルのカスタマイズ](https://narito.ninja/blog/detail/39/)
[Django、Userモデルのカスタマイズ(OneToOne)](https://narito.ninja/blog/detail/47/)
[Djangoのカスタムユーザーモデルでサインアップできるようにする](https://hodalog.com/how-to-create-user-sign-up-view/)
[Django AbstractBaseUserでカスタムユーザー作成](https://noumenon-th.net/programming/2019/12/13/abstractbaseuser/)
[公式ドキュメントより認証方法のカスタマイズ](https://docs.djangoproject.com/ja/3.0/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project)
[公式ドキュメントよりdjango.contrib.auth](https://docs.djangoproject.com/ja/3.0/ref/contrib/auth/#django.contrib.auth.models.User)