はじめに
Djangoでユーザー登録・認証・ログイン機能周りを整備したいと思い、進めていたら詰まったので記録を残しておきます。
結論
Djangoでアプリ開発をする際はprojectを立ち上げる前に要件定義と設計は済ませておくこと
設計等で織り込み済みでない限り、基本的には1project、1アプリを守るほうがDjangoの開発になれるまではベター。
勉強で開発をしているときもめんどくさがらずにprojectを再度立ち上げること、使い回さない。
何が起きたか
Djangoチュートリアルも終わったので、こちらの記事 と こちらの記事 を元に、
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を立ち上げ直した方が早いレベルで手間がかかります。
参考
つまり、1つのproject
で複数のアプリを管理する場合はproject
を立ち上げた時点で既にどのアプリケーションに認証周りの機能をもたせるかということを決めておかないといけないということです。
これを守らない……つまりチュートリアルでUserクラスを定義してるのに、別のアプリケーションを作ってそこで認証機能をつけたくなった私やノリでproject
を立ち上げてそのまま勢いでUser
クラスを作ると大変なことになるということですね。
しかし、やってしまったものはしょうがないので今回は後学のためという意味も込めてどうにかして既存のproject
のままどうにかならないかやってみました。
やったこと
- 既存の
User
モデルをカスタマイズする -
User
モデルを拡張するモデルを新たに作成するアプリケーションフォルダで作成しOneToOneField
を用いてリレーションする。 - カスタムしたUserモデルとUserモデルを拡張したモデルを管理画面に反映するためにUserモデルがある側の
admin.py
を編集する。
また、これに際してTODOアプリ側のモデルを機能で使うモデルと上記のUser
拡張モデル(ついでにメール認証フラグのためのレコードをオーバーライドしています)は別々のファイルにしてあります。
そして、models.py
周りをいじるということは当然makemigrations
をやり直すことになるので一応慎重にやっていきましょう。
既存のUserモデルのカスタマイズ
ディレクトリ図を書くのが億劫なので画像で失礼します。
src以下が開発環境でprojectフォルダ以下のapp
、polls
、todo
が各アプリケーションでUserモデルを定義したのはapp
、今回製作したいアプリケーションがtodo
となります。
まずはUser
モデル本体をカスタムしていきます。
参考:DjangoでUserモデルのカスタマイズ
app/models.py
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)
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
の部分をコピペして必要なところを加筆修正する形をとりました。
ソースは こちら。
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
を残してあります。
ちなみに
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
を用いる
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
は以下のようにパッケージするモデルをインポートするように定義します。
from .todo import Todo
from .account import Activate
これでモデルの分割は完了です。
カスタムしたUserモデルとUserモデルを拡張したモデルを管理画面に反映するためにUserモデルがある側のadmin.py
に手を入れる。
あとは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
の部分。
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モデルのカスタマイズ
Django、Userモデルのカスタマイズ(OneToOne)
Djangoのカスタムユーザーモデルでサインアップできるようにする
Django AbstractBaseUserでカスタムユーザー作成
公式ドキュメントより認証方法のカスタマイズ
公式ドキュメントよりdjango.contrib.auth