自動化無しに生活無し

WEBとかAIとかLinux関係をひたすら書く備忘録系ブログ

DjangoでUUIDを主キーとし、first_nameとlast_nameを1つにまとめたカスタムユーザーモデルを作る【AbstractBaseUserとallauth】

thumbnail

Djangoでユーザーを作ったとき、デフォルトでは数値型オートインクリメントの主キーになる。

身内だけで使う小さなウェブアプリであれば大した問題にはならないと思うが、基本主キーが数値型かつオートインクリメントであれば、簡単に予測されてしまう。セキュリティリスクは最小限に留めるためにも、なるべく主キーはUUID型にしたい。

そこで、本記事ではユーザーの主キーにUUIDを使用したカスタムユーザーモデルの作り方を解説する。

なお、本記事ではDjangoのユーザーモデルのうち、first_nameとlast_nameを1つにまとめてhandle_nameとしている。もともとのDjangoのカスタムユーザーモデルを流用して作りたい場合は、

上記ふたつを参考にすると良いだろう。

AbstractBaseUserを継承したユーザーモデルを作る

前回ではAbstractUserを継承したカスタムユーザーモデルを作ったが、今回はユーザーモデルを一から作ることができるAbstractBaseUserを継承したカスタムユーザーモデルを作る。

他にも、first_namelast_namehandle_nameに統一化。handle_nameemailの入力必須化を実現させる。まずは、カスタムユーザーモデル専用のアプリを作る。

python3 manage.py startapp users

続いて、settings.pyを編集する。下記のように編集。

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

    'users.apps.UsersConfig',
]
AUTH_USER_MODEL = 'users.CustomUser'
ACCOUNT_FORMS   = { "signup":"users.forms.SignupForm"}

django-allauthも実装したい場合は、こうする。

SITE_ID = 1 

#django-allauthログイン時とログアウト時のリダイレクトURL
LOGIN_REDIRECT_URL = '/' 
ACCOUNT_LOGOUT_REDIRECT_URL = '/' 

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

    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',

    'users.apps.UsersConfig',

]
AUTH_USER_MODEL = 'users.CustomUser'
ACCOUNT_FORMS   = { "signup":"users.forms.SignupForm"}

これでsettings.pyの設定は完了。カスタムユーザーモデルを実装させる場合は、このように使用するモデルとフォームをsettings.pyに指定させるのだ。INSTALLED_APPSに追加するのは、作ったモデルをマイグレーションさせるため。

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 


#ここ( https://github.com/django/django/blob/master/django/contrib/auth/models.py#L321 )から流用
class CustomUser(AbstractBaseUser, PermissionsMixin):

    username_validator  = UnicodeUsernameValidator()

    id          = models.UUIDField( default=uuid.uuid4, primary_key=True, editable=False )
    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とlast_nameをひとまとめにした。
    handle_name = models.CharField(verbose_name="Handle_name", max_length=150)

    email       = models.EmailField(_('email address'))

    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','handle_name']

    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_mail(subject, message, from_email, [self.email], **kwargs)

    def get_full_name(self):
        return self.handle_name

    def get_short_name(self):
        return self.handle_name

定義されているフィールドを大まかにまとめるとこうなる。

フィールド名 フィールドのルール
id UUIDフィールド、デフォルトuuid4、主キー、編集不可
username 文字列フィールド、最長150文字、重複禁止
handle_name 文字列フィールド、最長150文字
email メールフィールド
is_staff ブーリアンフィールド、デフォルトFalse
is_active ブーリアンフィールド、デフォルトTrue
date_joined 日時フィールド、デフォルトtimezone.now

CustomUserクラスにはPermissionsMixinクラスが継承されるので下記フィールドも追加される。

フィールド名 フィールドのルール
is_superuser ブーリアンフィールド、デフォルトFalse
groups 多対多フィールド、ブランク可
user_permissions 多対多フィールド、ブランク可

handle_nameemailblank=Trueを削除し、入力必須とした。

REQUIRED_FIELDS = ['email','handle_name']python3 manage.py createsuperuser等のコマンドでユーザーを作る時、インタラクティブシェルにemail及びhandle_nameに入力させるためである。実際にシェルからユーザーを作ると、下記画像のようになる。

ハンドルネームの入力が要求された。

REQUIRED_FIELDSの指定をしていないとDBでは入力必須にもかかわらず、入力欄が無いので何度やってもユーザー生成にエラーが出てしまう。モデルの入力必須をするのであれば、REQUIRED_FIELDSに追加することも忘れずに。

続いて、users/admin.pyの編集に入る。管理画面で編集表示させる項目を指定する。

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _

from .models import CustomUser

class CustomUserAdmin(UserAdmin):

    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('handle_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': ('username', 'password1', 'password2',"handle_name","email"),
        }),
    )


    list_display = ('username', 'email', 'handle_name', 'is_staff')
    search_fields = ('username', 'handle_name', 'email')

admin.site.register(CustomUser, CustomUserAdmin)

大部分は前回のカスタムユーザーモデルの生成と同様。ただ、今回は入力必須のemailhandle_nameが加えられるので、add_fieldsetsを定義している。これは管理ページからユーザーを作る時、入力必須であるemailhandle_nameをフォームに表示させるためにある。また、デフォルトのfirst_namelast_nameを削除しているので、デフォルトでそれらを参照しているlist_displaysearch_fieldsを書き換えている。

管理画面からユーザー生成を確認するとこうなる。

ハンドルネームとメールアドレス入力必須

ただ、これだと一般ユーザーがブラウザからアカウントを作る時のメールアドレスとハンドルネームの入力を必須にすることはできない。settings.pyで指定しておいた、ACCOUNT_FORMS = { "signup":"users.forms.SignupForm"}がまだ作られていないので、エラーも出る。そこで、users/forms.pyを編集する。

from django.contrib.auth.forms import UserCreationForm
from .models import CustomUser

class SignupForm(UserCreationForm):
    class Meta(UserCreationForm.Meta):
        model   = CustomUser
        fields  = ("username","email","handle_name")

これでOK。パスワードは継承元のUserCreationFormに含まれているので、fieldsに追加する必要はない。これで、django-allauthを使用したサインアップ画面でハンドルネームとメールアドレスの指定が必須となる。

ハンドルネームとメールアドレスの入力フォームが作られた

このようにサインアップ画面でハンドルネームとメールアドレスの入力必須になった。

マイグレーション

カスタムユーザーモデルを実装するときは、マイグレーションファイルを作る順序にも注意が必要。まずカスタムユーザーモデルのアプリからマイグレーションファイルを作る。その後に他のアプリを指定してマイグレーションファイルを作る。その上でマイグレーション実行。

python3 manage.py makemigrations users
python3 manage.py makemigrations [他のアプリ]
python3 manage.py migrate

こうすればエラーは起きない。

結論

AbstractBaseUserを継承したユーザーモデルの生成方法を解説した。本件はDjangoの公式のコードを引っ張ってきて一部を編集する必要があるので、Djangoを初めたばかりの人にはやや難しいだろう。

しかし、カスタムユーザーモデルを実装させることで、ユーザーに紐付いたデータ(所属や生年月日、会員ステータスなど)を新たに追加することができる。本格的にシステムを開発したり運営したりするには避けて通ることはできないだろう。最初にマイグレーションをしていなければDBを全部消して、一からマイグレーションファイルを作っていかないといけないので、難しくても最優先でやりたいところだ。

スポンサーリンク

シェアボタン

Twitter LINEで送る Facebook はてなブログ