Djangoのカスタムユーザーモデルでサインアップできるようにする

Django既存のUserモデルを拡張したカスタムユーザーモデルに加え、一意のユーザーにひもづくProfileモデルを用意し、下図のようなフォームでサインアップができるようにしました。

Djangoのカスタムユーザーモデルでサインアップするためのフォーム

モデルの拡張やビューの設定など、確実に忘れるのでメモします。

モデルの作成

カスタムユーザーモデル

models.pyファイルにて、AbstractBaseUserを継承する形でカスタムユーザーモデルを用意しました。ユニークなメールアドレスを登録できるようにしています。

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager

class UserManager(BaseUserManager):
    def create_user(self, username, email, password=None):
        if not username:
            raise ValueError('Users must have an username')
        elif not email:
            raise ValueError('Users must have an email address')

        user = self.model(
            username = username,
            email = self.normalize_email(email),
        )
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, username, email, password):
        user = self.create_user(
            username,
            email,
            password=password,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user

class User(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(max_length=30, unique=True)
    email = models.EmailField(unique=True)
    first_name = models.CharField(max_length=30, blank=True)
    last_name = models.CharField(max_length=30, blank=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    date_joined = models.DateTimeField(auto_now_add=True)

    objects = UserManager()

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

class UserManager(BaseUserManager):

AbstractBaseUserを拡張する場合、 create_usercreate_superuser メソッドを定義しているUserManagerというクラスも修正する必要があります。 create_user はその名の通り、ユーザーの新規作成時に呼び出されるメソッドです。 create_superuser は管理者用のユーザーを作成するときに使われるメソッドですね。

objects = UserManager()

Userクラスの中にある objects という変数は、views.pyなどでUserモデルの情報を参照するときに使います。例えばこんな感じ↓

user = User.objects.get(username="Sakamoto")

↑はSakamotoという名前のユーザーをUserモデルから探し、その情報をuser変数につっこんでいます。

Profileモデル

上記カスタムUserモデルに加えて、Profileというモデルを用意しました。

ユーザーの新規登録と同期して、登録されたユーザーにひもづくProfileレコードが挿入されます。

from django.db.models.signals import post_save
from django.dispatch import receiver

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    gender = models.CharField(max_length=20, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    location = models.CharField(max_length=30, blank=True)
    favorite_words = models.CharField(max_length=50, blank=True)

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

signals、post_save、receiver

同期にはsignalsというモデルのpost_saveというモジュールと、receiverというモジュールを使います。

Userモデルのpost_saveシグナルをreceiverで受け取り、 create_user_profilesave_user_profile メソッドが起動します。

フォームの作成

forms.pyを作ります。Djangoがデフォルトで用意している認証用のフォームもあるのですが、もう少しいい感じにしたかったので拡張しました。

SignUpForm

from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
User = get_user_model()
from .models import Profile
from datetime import datetime

class SignUpForm(UserCreationForm):
    password = forms.CharField(widget=forms.PasswordInput)
    password1 = forms.CharField(required=False)
    password2 = password1

    class Meta:
        model = User
        fields = ('username', 'email', 'password')

class ProfileForm(forms.ModelForm):
    CHOICES = (
        ('female', '女性',),
        ('male', '男性',),
        ('not_applicable', '秘密',)
    )
    gender = forms.ChoiceField(widget=forms.RadioSelect, choices=CHOICES, required=False)

    def make_select_object(from_x, to_y, dates, increment=True):
        if increment:
            for i in range(from_x, to_y):
                dates.append([i, i])
        else:
            for i in range(from_x, to_y, -1):
                dates.append([i, i])
        return dates

    def make_select_field(select_object):
        dates_field = forms.ChoiceField(
            widget=forms.Select,
            choices=select_object,
            required=False
        )
        return dates_field

    years = [["",""]]
    current_year = datetime.now().year
    years = make_select_object(current_year, current_year-80, years, increment=False)
    birth_year = make_select_field(years)

    months = [["",""]]
    months = make_select_object(1, 13, months)
    birth_month = make_select_field(months)

    days = [["",""]]
    days = make_select_object(1, 32, days)
    birth_day = make_select_field(days)

    class Meta:
        model = Profile
        fields = ('gender', 'birth_year', 'birth_month', 'birth_day')

class SignUpForm(UserCreationForm)

from django.contrib.auth.forms import UserCreationForm ここでインポートしたユーザー作成用のフォームを SignUpForm クラスへ継承しています。

password

UserCreationFormではpassword1とpassword2(確認用)の二つのフォームフィールドがありますが、二つもいらないと思ったので無効にしています。

class ProfileForm(forms.ModelForm)

Profileモデルのためのフォームをこのクラスで設定していて、性別と生年月日が取得できます。selectオブジェクトに色々制限があったりして、少し苦労しました。

ビューの作成

views.pyにて、サインアップ用のメソッドを定義します。ここではsign_upという名前にしました(そのまんまですね!)。urls.pyの設定も忘れずにね!

sign_upビュー

from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login as auth_login, get_user_model
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from .forms import SignUpForm, ProfileForm
from .models import Profile
from datetime import date

User = get_user_model()

def home(request):
    return render(request, 'your_app/home.html')

def sign_up(request):
    if request.method == 'POST':
        signup_form = SignUpForm(request.POST)
        profile_form = ProfileForm(request.POST)
        if signup_form.is_valid() and profile_form.is_valid():
            username = signup_form.cleaned_data.get('username')
            email = signup_form.cleaned_data.get('email')
            password = signup_form.cleaned_data.get('password')
            gender = profile_form.cleaned_data.get('gender')
            birth_year = profile_form.cleaned_data.get('birth_year')
            birth_month = profile_form.cleaned_data.get('birth_month')
            birth_day = profile_form.cleaned_data.get('birth_day')

            user = User.objects.create_user(username, email, password)
            user.profile.gender = gender
            if birth_day and birth_month and birth_year:
                birth_date = date(int(birth_year), int(birth_month), int(birth_day)).isoformat()
                user.profile.birth_date = birth_date
            user.save()

            user = authenticate(request, username=username, password=password)
            auth_login(request, user, backend='django.contrib.auth.backends.ModelBackend')
            messages.add_message(request, messages.SUCCESS, 'ユーザー登録が完了しました!')
            return redirect('home')
    else:
        signup_form = SignUpForm()
        profile_form = ProfileForm()

    login_form = LoginForm()
    context = {
        'signup_form': signup_form,
        'profile_form': profile_form,
    }
    return render(request, 'registration/sign_up.html', context)

authenticate, login, get_user_modelモジュール

from django.contrib.auth import authenticate, login as auth_login, get_user_model でauthenticateとlogin、get_user_modelモジュールを呼び出しています。今回はカスタムユーザーを使うため、認証にget_user_modelを使う必要があります。

User = get_user_model() のところでget_user_model()をUserに代入していて、 user = User.objects.create_user(username, email, password) とかで使っています。

is_valid()

送信されたフォームの内容のバリデーションをいい感じにやってくれるメソッドです。

cleaned_data

is_valid()メソッドで検査された値はcleaned_dataに格納されます。フォームの値を取り出したいと時はrequest.POSTからは取得しないで、cleaned_dataから取るようにします。

User.objects.create_user

models.pyとviews.pyで定義したように、 User = get_user_model()objects = UserManager() となっていますので、最終的に UserManager クラスの create_user メソッドが呼ばれ、ユーザーが登録されます。また、先述した通り、このユーザーにひもづく形でProfileモデルにレコードが挿入されます。

auth_login(request, user, backend=‘django.contrib.auth.backends.ModelBackend’)

OAuth2認証など複数の認証方法をsettings.pyに追加している場合、backendに django.contrib.auth.backends.ModelBackend を指定する必要があります。

テンプレートの作成

registration/sign_up.html

{%!l(MISSING)oad widget_tweaks %!}(MISSING)
..........
<h3>30秒でアカウント登録(無料です)</h3>
<form action="{%!u(MISSING)rl 'sign_up' %!}(MISSING)" method="post">
  {%!c(MISSING)srf_token %!}(MISSING)

  <div class="form-group row">
    <label for="{{ form.username.id_for_label }}" class="col-sm-3 col-form-label">ユーザー名</label>
    <div class="col-sm-9">
      <div class="input-group">
        <div class="input-group-prepend">
          <div class="input-group-text"><i class="fas fa-user"></i></div>
        </div>
        {{ signup_form.username|add_class:'form-control' }}
        {%!f(MISSING)or error in signup_form.username.errors %!}(MISSING)
        <span class="text-warning">{{ error }}</span>
        {%!e(MISSING)ndfor %!}(MISSING)
      </div>
    </div>
  </div>
..........
  <div class="form-group row">
    <div class="col-sm">
      <button type="submit" class="btn btn-info btn-block">登録</button>
    </div>
  </div>
</form>

widget_tweaks

フォームのデザインを自由にいじれるように、widget_tweaksライブラリを使っています。widget_tweaksについては↓こちらの記事をどうぞ。

https://hodalog.com/how-to-use-bootstrap-4-forms-with-django/

signup_form.username|add_class:‘form-control’

views.pyのcontextに設定したsignup_formから、usernameのフォームに「form-control」というcssクラスをつけて出力しています。このcssクラスはBootstrapで使われるcssクラスです。

省略していますが、forms.pyに設定したそのほかのフォームフィールドについても同じような書き方で簡単に出力できます。

まとめ

Djangoの認証周りの設定はかなり自由にできるようになりました。やったね!