Djangoでオリジナルのカスタムユーザーを作ってみましょう。カスタムユーザーを作る方法は大きく分けて2つあるのですが、今回は自由度の高い(ただし難易度も高い)AbstractBaseUserを継承する方法について解説します。
目次
- Djangoのユーザーモデルの構造
- カスタムユーザーを利用するアプリの作成
- カスタムユーザーモデルを定義
- カスタムユーザーモデルのカスタマイズ
- 認証に使用するモデルを変更
- マイグレーション
- スーパーユーザーの追加
- カスタムユーザーを管理画面に登録
- ルーティング
- ビュー
- フォーム
- テンプレート
- 動作確認
- まとめ
Djangoのユーザーモデルの構造
Djangoには標準で次のような構造のUserモデルが用意されています。
認証情報を持つAbstractBaseUserモデルとグループや権限情報を持つPermissionsMixinモデルをそれぞれ継承し、そこに一般的なユーザー情報を加えたAbstractUserモデルをさらに継承しているのがDjango標準のUserモデルです。
Userモデルと同様に、AbstractUserモデルを継承することでもカスタムユーザーモデルを作成できるのは次の記事で紹介しました。
Djangoでカスタムユーザーを作ってみよう【AbstractUser】
この方法は手っ取り早いですが、アプリによってはAbstractUserで定義されているフィールドが必要ない場合もあります。
今回はさらにその元となっているAbstractBaseUserモデルとPermissionsMixinモデルを継承することで、これらのユーザー情報をすべて自分で設定できる、より自由度の高いカスタムユーザーモデルを作成してみましょう。
このように、AbstractUserモデルと同様、AbstractBaseUserモデルとPermissionsMixinモデルを継承してカスタムユーザーモデルを作成します。
カスタムユーザーを利用するプロジェクトとアプリの作成
カスタムユーザーの作成と利用を試してみるためのプロジェクトと簡単なアプリを用意しましょう。
Djangoプロジェクトの作成
この記事では学習のために新たなプロジェクトとアプリケーションを用意します。
カスタムユーザーモデルを作成して既存のモデルの代わりに使用するため、既存のプロジェクトでは標準のUserモデルを外部キーとして使用している場合、それらをすべて変更する必要があるためです。
WindowsPowerShellの場合、次のコマンドでプロジェクトディレクトリを作成、仮想環境を作成して有効化し、Djangoプロジェクトを作成します。
mkdir customuser-project cd customuser-project py -3 -m venv venv venv\Scripts\Activate.ps1 python -m pip install Django django-admin startproject config .
アプリケーションの作成と登録
カスタムユーザーを利用するアプリを作成しましょう。
python manage.py startapp customuser_app
作成したアプリをプロジェクトに登録しておきましょう。
settings.pyのINSTALLED_APPSに次のように記述します。
INSTALLED_APPS = [ 'customuser_app', # 追加 . . . ]
カスタムユーザーを定義
まずはDjangoのフレームワーク内で定義されているAbstractUserモデルをコピペして、必要な部分をカスタマイズしましょう。
AbstractUserは django.contrib.auth.models で定義されています。
ここから必要なファイルのインポートとAbstractUserクラスをコピーし、今作成したcustomuser_appアプリのmodels.pyにペースト。クラス名をCustomUserに変更します。
from django.apps import apps from django.contrib import auth from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.contrib.auth.hashers import make_password from django.core.mail import send_mail from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import PermissionsMixin from django.contrib.auth.validators import UnicodeUsernameValidator # これだけimport先の指定を修正する必要あり class CustomUser(AbstractBaseUser, PermissionsMixin): # クラス名をCustomUserに変更 """ An abstract base class implementing a fully featured User model with admin-compliant permissions. Username and password are required. Other fields are optional. """ 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."), }, ) first_name = models.CharField(_("first name"), max_length=150, blank=True) last_name = models.CharField(_("last name"), max_length=150, blank=True) email = models.EmailField(_("email address"), blank=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() EMAIL_FIELD = "email" USERNAME_FIELD = "username" REQUIRED_FIELDS = ["email"] class Meta: verbose_name = _("user") verbose_name_plural = _("users") abstract = True def clean(self): super().clean() self.email = self.__class__.objects.normalize_email(self.email) 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 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)
これを元に不要なフィールドやメソッドを削除したり、オリジナルのフィールドを定義していきます。
カスタムユーザーモデルのカスタマイズ
不要なフィールドを削除
まずは不要なフィールドを削除しましょう。今回作成するアプリでは次のフィールドは不要なので削除することにします。
- first_name
- last_name
# 削除 first_name = models.CharField(_("first name"), max_length=150, blank=True) # 削除 last_name = models.CharField(_("last name"), max_length=150, blank=True)
機能しなくなったメソッドを削除
フィールドを削除する場合、他の場所でそのフィールドが使われていないか注意するひつようがあります。
例えば上記のfirst_nameとlast_nameのフィールドは、今定義したCustomUserモデルの中でも次のメソッドで使われています。
- get_full_name
- get_short_name
フィールドを削除すると、当然これらのメソッドも機能しなくなり、実行されるとエラーになってしまいます。これらの機能しなくなったメソッドも削除しましょう。
''' 削除 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など色々な場所で使われているフィールドを削除する場合、修正量も多くなります。
追加したいフィールドやメソッドを定義
次に、追加したいフィールドやメソッドを定義します。今回のアプリでは次のフィールドとメソッドを追加することにします。
from datetime import date # 追加 class CustomUser(AbstractBaseUser, PermissionsMixin): ... bio = models.TextField(blank=True, null=True) # 自己紹介 location = models.CharField(max_length=50, blank=True, null=True) # 場所 birth_date = models.DateField(blank=True, null=True) # 生年月日 ... # birth_dateから年齢を計算して返すメソッド def get_age(self): if self.birth_date: today = date.today() return today.year - self.birth_date.year - ((today.month, today.day) < (self.birth_date.month, self.birth_date.day)) return None
設定の変更
下記のクラス変数を変更することで、カスタムユーザーモデルの設定を変更することができます。
- EMAIL_FIELD:メールアドレスとして扱うフィールドの指定
- USERNAME_FIELD:ユーザーを一意に識別するフィールドの指定
- REQUIRED_FIELDS:createsuperuser コマンド実行時に入力受付を行うフィールドの指定
USERNAME_FIELDはユーザーを一意に識別するためのフィールド、つまりユーザー間で重複してはいけない値です。そのためここに指定するフィールドはuniqueオプションをTrueにしておく必要があります。
また、REQUIRED_FIELDS に指定するリストにはUSERNAME_FIELD に指定するフィールドを含ませてはいけません。
では実際に設定を変更してみましょう。
今回はユーザーの識別をメールアドレスで行うようにします。
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."), }, ) email = models.EmailField(_("email address"), unique=True) ... EMAIL_FIELD = "email" USERNAME_FIELD = "email" # emailに変更 REQUIRED_FIELDS = ["username", "bio", "location", "birth_date"] # USERNAME_FIELD 以外の入力必須の項目をリストで指定
ここまででCustomUserモデルの中身は次のようになっています。
class CustomUser(AbstractBaseUser, PermissionsMixin): username_validator = UnicodeUsernameValidator() username = models.CharField( _("username"), max_length=150, help_text=_( "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." ), validators=[username_validator], error_messages={ "unique": _("A user with that username already exists."), }, ) email = models.EmailField(_("email address"), unique=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) bio = models.TextField(blank=True, null=True) location = models.CharField(max_length=50, blank=True, null=True) birth_date = models.DateField(blank=True, null=True) objects = UserManager() EMAIL_FIELD = "email" USERNAME_FIELD = "email" REQUIRED_FIELDS = ["username", "bio", "location", "birth_date"] # 追加 class Meta: verbose_name = _("user") verbose_name_plural = _("users") # abstract = True 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_age(self): if self.birth_date: today = date.today() return today.year - self.birth_date.year - ((today.month, today.day) < (self.birth_date.month, self.birth_date.day)) return None
ユーザーの識別をemailにしたので、ユーザーマネージャーも変更しておきましょう。UserManagerクラスではcreate_superuserメソッドやcreate_userメソッドが定義されています。
Djangoのフレームワーク内で定義されているUserManagerモデルをコピーしてmodels.pyに貼り付け、必要な部分を変更します。
class UserManager(BaseUserManager): use_in_migrations = True def _create_user(self, email, username, password=None, **extra_fields): """ Create and save a user with the given email, and password. """ if not email: # emailが必須となったためusernameから変更 raise ValueError("The given email must be set") email = self.normalize_email(email) # Lookup the real model class from the global app registry so this # manager method can be used in migrations. This is fine because # managers are by definition working on the real model. GlobalUserModel = apps.get_model( self.model._meta.app_label, self.model._meta.object_name ) user = self.model(username=username, email=email, **extra_fields) user.password = make_password(password) user.save(using=self._db) return user def create_user(self, email, username, password=None, **extra_fields): # emailの入力を必須に extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_superuser", False) return self._create_user(email, username, password, **extra_fields) def create_superuser(self, email, username, password=None, **extra_fields): # emailの入力を必須に 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, username, password, **extra_fields) def with_perm( self, perm, is_active=True, include_superusers=True, backend=None, obj=None ): if backend is None: backends = auth._get_backends(return_tuples=True) if len(backends) == 1: backend, _ = backends[0] else: raise ValueError( "You have multiple authentication backends configured and " "therefore must provide the `backend` argument." ) elif not isinstance(backend, str): raise TypeError( "backend must be a dotted import path string (got %r)." % backend ) else: backend = auth.load_backend(backend) if hasattr(backend, "with_perm"): return backend.with_perm( perm, is_active=is_active, include_superusers=include_superusers, obj=obj, ) return self.none()
createuserコマンドやcreatesuperコマンドでユーザーを作成する際、email引数を必須かつ最初に受け取るように。
usernameではなくemailが入力されなかった場合にエラーメッセージを返すように変更。
また、必須でなくなったusernameのノーマライズ処理を削除しました。
認証に使用するモデルを変更
続いて認証に利用するユーザー管理モデルを今作成した CustomUser に変更します。
settings.pyに次のように追記します。
AUTH_USER_MODEL = 'customuser_app.CustomUser'
マイグレーション
customuser-project ディレクトリで次のコマンドを実行します。
python manage.py makemigrations python manage.py migrate
ここでエラーが発生する場合、削除したフィールドやメソッドが使われていたり、標準のUserモデルを外部キーとして使用していたり、AUTH_USER_MODELの記述が抜けていないかチェックしてみて下さい。
スーパーユーザーの追加
管理用のスーパーユーザーを追加します。
customuser-project ディレクトリで次のコマンドを実行します。
python manage.py createsuperuser
普段とは異なり、メールアドレスの入力が最初にくるはずです。
カスタムユーザーを管理画面に登録
作成したカスタムユーザーを管理画面で管理できるようにしましょう。
admin.py を次のように修正します。
from django.contrib import admin from customuser_app.models import CustomUser admin.site.register(CustomUser)
Djangoの開発用テストサーバーを起動して管理画面を確認してみましょう。
次のコマンドを実行します。
python manage.py runserver
Webブラウザで次のURLにアクセスして管理画面を開き、今作成したスーパーユーザーでログインしてみましょう。
http://127.0.0.1:8000/
管理画面に CUSTOMUSER_APP が追加されています。クリックしてみましょう。
管理画面でユーザーの一覧を確認・追加・変更できるようになっています。ユーザー名をクリックしてみましょう。
ユーザーの詳細情報を確認・変更できます。ただ、標準のUserモデルを使った時と違って管理画面からパスワードが変更可能になってしまっています。
このままだとセキュリティ的によくないので、標準のUserモデルを使った時と同じように管理画面でパスワードがハッシュ値として表示され、直接編集できないようにしてみましょう。
admin.py を次のように修正します。
from django.contrib import admin from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin from django.contrib.auth.forms import UserChangeForm from customuser_app.models import CustomUser class CustomUserChangeForm(UserChangeForm): class Meta(UserChangeForm.Meta): model = CustomUser fields = ('email', 'password', 'bio', 'location', 'birth_date', 'is_active', 'is_staff') class CustomUserAdmin(DefaultUserAdmin): form = CustomUserChangeForm add_form = DefaultUserAdmin.add_form model = CustomUser list_display = ('email', 'is_staff', 'is_active', 'bio', 'location', 'birth_date') fieldsets = ( (None, {'fields': ('email', 'password')}), ('Personal info', {'fields': ('bio', 'location', 'birth_date')}), ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}), ('Important dates', {'fields': ('last_login', 'date_joined')}), ) admin.site.register(CustomUser, CustomUserAdmin)
UserChangeFormを継承したCustomUserChangeFormという新しいフォームクラスを定義し、そのmodel属性をCustomUserモデルに変更。
ここで、継承元のUserChangeFormでは、今回削除したfirst_nameやlast_nameといったフィールドを利用しているため、fieldsを指定して存在するフィールドのみを扱うように変更します。
次に、DefaultUserAdminを継承したCustomUserAdminという新しい管理クラスを定義しています。
こちらも継承元ではユーザーリストの表示項目(list_display)や編集画面でのフィールドセット(fieldsets)にfirst_nameやlast_nameが使われていて、そのままではエラーになってしまうため、フィールドを今回作成したカスタムユーザーに合うよう再定義しています。
最後にadmin.site.registerでCustomUserモデルとCustomUserAdmin管理クラスを関連付けることにより、管理画面からCustomUserモデルのオブジェクトを操作する際にはCustomUserAdminの設定が使用されるようにしています。
では、これらの変更を行ったうえでもう一度管理画面からユーザーの詳細データを見てみましょう。
これで標準のUserモデルを使った時と同じように、管理画面でパスワードがハッシュ値として表示され、直接編集できないようになりました。
また、グループやユーザーパーミッションなどの表示も標準のUserモデルを使った時と同じようになりました。
ルーティング
では実際にこのカスタムユーザーをフォームやビューから利用してみましょう。
次の3画面を実装します。
- サインアップページ(カスタムユーザーを追加するページ)
- ログインページ
- ログインユーザーのプロフィールを表示するページ
まずはルーティングを記述します。
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('customuser_app/', include('customuser_app.urls')) # 追加 ]
次にアプリ内のルーティングです。customuser_appディレクトリにもurls.pyファイルを作成し、次のように記述します。
from . import views from django.urls import path urlpatterns = [ path('signup/', views.signup_view, name='signup'), path('login/', views.login_view, name='login'), path('profile/', views.profile_view, name='profile'), ]
サインアップページ、ログインページ、プロフィールページの3つを作ります。
ビュー
ビューを作成します。
from django.shortcuts import render, redirect from .forms import SignupForm, LoginForm from django.contrib.auth import login from django.contrib.auth.decorators import login_required from datetime import datetime def signup_view(request): if request.method == 'POST': form = SignupForm(request.POST) if form.is_valid(): user = form.save() login(request, user) return redirect(to='/customuser_app/profile/') else: form = SignupForm() param = { 'form': form } return render(request, 'customuser_app/signup.html', param) def login_view(request): if request.method == 'POST': next = request.POST.get('next') form = LoginForm(request, data=request.POST) if form.is_valid(): user = form.get_user() if user: login(request, user) return redirect(to='/customuser_app/profile/') else: form = LoginForm() param = { 'form': form, } return render(request, 'customuser_app/login.html', param) @login_required def profile_view(request): user = request.user params = { 'age': user.get_age(), } return render(request, 'customuser_app/profile.html', params)
settings.py の AUTH_USER_MODEL で今回作成したカスタムユーザーモデルを指定しているのでrequest のデータ属性 user はカスタムユーザーのインスタンスとなります。
あとは最後のprofile_viewでカスタムユーザーモデルで定義した get_age() メソッドを使用しています。
フォーム
ビューで使用しているSignupFormとLoginFormを作成しましょう。
forms.pyを作成し次のように記述します。
from django import forms from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from customuser_app.models import CustomUser class SignupForm(UserCreationForm): class Meta: model = CustomUser fields = [CustomUser.USERNAME_FIELD] + CustomUser.REQUIRED_FIELDS + ['password1', 'password2'] class LoginForm(AuthenticationForm): pass
SignupFormのmodelに今回作成したCustomUserを指定、fieldsにはカスタムユーザーの作成時に必要な情報を指定しておきましょう。
特に入力必須のフィールドを追加した場合はこのfieldsに指定しておかなければユーザーが入力必須のフィールドを入力することができません。
入力必須のフィールドをCustomUserモデルのREQUIRED_FIELDSに記述しておけば、CustomUser.REQUIRED_FIELDSでまとめて指定することができます。
LoginFormは継承元のAuthenticationFormをそのまま使用します。
テンプレート
あとはページを表示するテンプレートファイルを作成します。
まずはsettings.pyを開きテンプレートファイルの場所を指定します。
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], # テンプレート用のフォルダを追記 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
これでDjangoがプロジェクトディレクトリ(customuser-project)直下のtemplatesディレクトリからテンプレートファイルを探してくれるようになりました。
ではcustomuser-projectディレクトリにtemplatesディレクトリを作成し、その中にcustomuser_appディレクトリを作成、その中に次の3つのテンプレートファイルを作成します。
{% load static %} <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>ユーザー登録</title> </head> <body> <h1>ユーザー登録</h1> <form action="{% url 'signup' %}" method="post"> {% csrf_token %} {{ form.as_p }} <p><input type="submit" value="登録"></p> </form> </body> </html>
{% load static %} <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>ログイン</title> </head> <body> <h1>ログイン</h1> <p>{{message}}</p> <form action="{% url 'login'%}" method="post"> {% csrf_token %} {{ form.as_p }} <p><input type="hidden" name="next" value="{{next}}"></p> <p><input type="submit" value="ログイン"></p> </form> <p><a href="{% url 'signup'%}">ユーザー登録</a></p> </body> </html>
{% load static %} <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>{{ user }}のプロフィール</title> </head> <body> <h1>{{ user }}のプロフィール</h1> <p>Bio: {{ user.bio }}</p> <p>Location: {{ user.location }}</p> <p>Birth date: {{ user.birth_date }}</p> <p>Age: {{ age }}</p> </body> </html>
動作確認
では、PowerShellでcustomuser-projectディレクトリに移動し、Djangoの開発用テストサーバーを起動しましょう。
python manage.py runserver
Webブラウザで http://127.0.0.1:8000/customuser_app/signup/ にアクセスします。
このようにカスタムユーザーモデルで追加したフィールドを入力できるユーザー登録フォームが表示されます。
実際に項目を入力して「登録」ボタンを押してみましょう。
登録が完了し、ユーザーのプロフィールページに自動遷移します。
さきほど入力したbio, location, birth_date のほか、birth_date から年齢を計算して表示してくれています。
ログインページも確認しておきましょう。http://127.0.0.1:8000/customuser_app/login/ にアクセスします。
ログイン中かどうかによって処理を分けていないので、ログイン中でもログインページが表示されます。
試しにログインしてみましょう。
ログインが完了し、ユーザーのプロフィールページに自動遷移します。
まとめ
このように、AbstractBaseUserモデルとPermissionsMixinモデルを継承することで、より自由度の高いカスタムユーザーモデルを作成することができます。
ただし、デフォルトで用意されているフィールドやメソッドは、存在する前提としてDjangoの他のクラスで使用されていることがあるため、影響範囲のコードを確認・修正する必要が出てきます。
そのためこちらの方法はAbstractUserを継承する場合と比べて自由度が高い反面、難易度も高いです。
ある程度Djangoを使い慣れていたり、Webアプリケーションフレームワークの経験がある方向けの方法で、Djangoを学びたての初心者には少し厳しいかもしれません。
いつか本格的なDjangoアプリケーションを作ろうと思った時のために、こちらの方法も頭の片隅に置いておきましょう。
勉強中の方はまずはAbstractUserを継承する方法でカスタムユーザーを作ってみることをおすすめします。