3-10. 独自カスタマイズのユーザーを使用する

今回のテーマは「独自カスタマイズのユーザーを使用する」です。ここまではDjango標準のUserを使用してきました。しかし標準のUserを使用すると不都合が多い場合も多いです。ユーザーのカスタマイズ方法を見ていきましょう。

※本ページは「3-9. LogoutViewで作成したログアウト画面をカスタマイズする」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


usernameの説明文とバリデーションの齟齬の問題

既存ユーザーのusernameのバリデーションはUnicodeUserValidatorが使われいてます。usernameの説明文には「半角アルファベット、半角数字、@/./+/-/_ で150文字以下にしてください。」となっています。しかし、実際には漢字やひらがな、カタカナ等の全角文字でもユーザー登録出来てしまいます。これはDjango1.10からpython3のみでusernameでunicodeが許容される仕様に変更されたことによるものです。(参考:リリース1.10)よって既存のユーザーモデルのusernameの説明文は「全角、半角アルファベット、半角数字、@/./+/-/_ で150文字以下にしてください。」が正しいということになります。この問題を解決する方法は二つです。1つは説明文やエラーメッセージを変更する方針。もう一つは説明文に合せて半角英数と@/./+/-/_ のみを許容するバリデーションを使用するバリデータに切り替える方針です。今回はバリデータをASCIIUsernameValidatorに変更します。

ユーザーカスタマイズの2つの方法

ユーザーのカスタマイズには大きく分けて二つの方法があります。1つは既存のユーザーモデルの拡張です。例えばプロフィールモデルなどをユーザーのIDとリレーションさせた1対1関係のモデルを用意することで既存ユーザーにない情報を付与することができます。もう一方は独自のユーザーモデルを構築する方法です。この方法の中でもAbstructUserを継承したユーザーモデルを使用する方法と、AbstructBaseUserを継承したユーザーモデルを構築する方法があります。AbstructBaseUserを継承したモデルのほうがカスタマイズ性が高く融通がききます。今回はAbstructBaseUserを継承した独自ユーザーを作成していきます。

データベースの初期化

独自ユーザーモデルを使用する場合は最初のデータベースマイグレーションの前に行う必要があります。そうしないとユーザーに関連する多くのモデルがDjangoの既存ユーザーモデルとリレーションした状態でデータベースが構築されてしまうからです。ここまで、学習の都合で既存Userモデルを使用してきたので、一度データベースを破棄して新規に作成しましょう。

$ mysql -u root mysql
[mysql] drop database forum_data;
[mysql] create database forum_data;
[mysql] grant all on forum_data.* to 'forum_user'@'localhost';

独自カスタマイズユーザーの実装

今回は独自ユーザーモデルはaccountsアプリケーション内に作成していきます。
accounts/models.py


from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.validators import UnicodeUsernameValidator, ASCIIUsernameValidator
from django.core.mail import send_mail
from django.utils.translation import gettext_lazy as _
from django.utils import timezone

# Create your models here.

class UserManager(BaseUserManager):
    """
    Create and save user with email
    """
    use_in_migrations = True

    def _create_user(self, username, email, password, **extra_fields):
        """
        Create and save a user with the given username, email, and password.
        """
        if not username:
            raise ValueError('The given username must be set')

        if not email:
            raise ValueError('The given email must be set')

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

    def create_user(self, username, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(username, email, password, **extra_fields)

    def create_superuser(self, username, 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(username, email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    """
    Django標準のUserをベースにカスタマイズしたUserクラス
    """
    username_validator = UnicodeUsernameValidator()
    # python3で半角英数のみ許容する場合はASCIIUsernameValidatorを用いる
    # username_validator = ASCIIUsernameValidator()

    username = models.CharField(
        _('username'),
        max_length=50,
        unique=True,
        # help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
        help_text='この項目は必須です。全角文字、半角英数字、@/./+/-/_ で50文字以下にしてください。',
        validators=[username_validator],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )
    # first_name = models.CharField(_('first name'), max_length=30, blank=True)
    # last_name = models.CharField(_('last name'), max_length=150, blank=True)
    email = models.EmailField(
        _('email address'),
        help_text='この項目は必須です。メールアドレスは公開されません。',
        blank=False
    )
    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
        abstract = False

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    # first_nameとlast_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

    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)

基本的にはdjango.contrib.auth.models.UserManagerとdjango.contrib.auth.models.AbstructUserのコピーです。ただし今回はfirst_nameとlast_nameフィールドを削ったので、その部分に関しては変更を加えています。また、バリデータに関してはデフォルトのままUnicodeUserValidatorを使用していますが、もしpython3環境で半角英数を使用したい場合はASCIIUsernameValidatorに変更をしましょう。ただヘルプメッセージは実態に即したメッセージに変更しないと整合性が失われるので変更しています。

独自ユーザー用のフォームの作成

独自のユーザーモデルを作成したので各フォームもそれに従って変更します。多くのものはimportを変更するだけで問題ないですが、ユーザー作成、ユーザー情報変更用のフォームは追加が必要になります。

accounts/forms.py


from django.forms import ModelForm
# from django.contrib.auth.models import User
# from django.contrib.auth import get_user_model
from django.contrib.auth.forms import (
    AuthenticationForm, PasswordChangeForm,
    PasswordResetForm, SetPasswordForm,
    UserChangeForm, UserCreationForm
)

from .models import User

# UserModel = get_user_model()

class UserInfoChangeForm(ModelForm):
    class Meta:
        model = User
        fields = [
            'email',
            # 'last_name',
            # 'first_name',
        ]

    def __init__(self, email=None, first_name=None, last_name=None, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        # ユーザーの更新前情報をフォームに挿入
        if email:
            self.fields['email'].widget.attrs['value'] = email
        if first_name:
            self.fields['first_name'].widget.attrs['value'] = first_name
        if last_name:
            self.fields['last_name'].widget.attrs['value'] = last_name

    def update(self, user):
        user.email = self.cleaned_data['email']
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.save()

class EmailChangeForm(ModelForm):
    class Meta:
        model = User
        fields = ['email']

    def __init__(self, email=None, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)
        # ユーザーの更新前情報をフォームに挿入
        if email:
            self.fields['email'].widget.attrs['value'] = email

    def update(self, user):
        user.email = self.cleaned_data['email']
        user.save()


class CustomAuthenticationForm(AuthenticationForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class CustomPasswordChangeForm(PasswordChangeForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class CustomPasswordResetForm(PasswordResetForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class CustomSetPasswordForm(SetPasswordForm):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix', '')
        super().__init__(*args, **kwargs)

class AdminUserCreationForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('username', 'email')
    
    # def save(self, commit=True):
    #     user = User.objects.create_user(
    #         self.cleaned_data["name"],
    #         self.cleaned_data["email"],
    #         self.cleaned_data["password1"],
    #     )
    #     return user

class CustomUserChangeForm(UserChangeForm):
    class Meta:
        model = User
        fields = '__all__'
        
class CustomUserCreationForm(UserCreationForm):
    class Meta:
        model = User
        fields = ('username', 'email')

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('label_suffix','')
        super().__init__(*args, **kwargs)

少々複雑なので、不要な部分はコメントアウトしています。import部分で先程作成したUserモデルをimportしています。EmailChangeFormはこれまでのUserInfoChangeFormの代わりに使います。ここは大きな問題はないと思います。

ビューの変更

accounts/views.py(一部抜粋)


from django.shortcuts import render, redirect
from django.views.generic import TemplateView, FormView, UpdateView
# from django.contrib.auth.forms import UserCreationForm
# from django.contrib.auth.models import User
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import (
    LoginView, LogoutView, PasswordChangeView, PasswordChangeDoneView,
    PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView, PasswordResetCompleteView
)
from django.urls import reverse_lazy
from django.contrib.auth import login, authenticate

from .models import User
from .forms import (
    UserInfoChangeForm,
    CustomAuthenticationForm, CustomPasswordChangeForm,
    CustomPasswordResetForm, CustomSetPasswordForm,
    CustomUserChangeForm, CustomUserCreationForm, EmailChangeForm
)

# Create your views here.

class UserCreateView(FormView):
    # form_class = UserCreationForm
    form_class = CustomUserCreationForm
    template_name = 'registration/create.html'
    success_url = reverse_lazy('accounts:profile')
    def form_valid(self, form):
        print(self.request.POST['next'])
        if self.request.POST['next'] == 'back':
            return render(self.request, 'registration/create.html', {'form': form})
        elif self.request.POST['next'] == 'confirm':
            return render(self.request, 'registration/create_confirm.html', {'form': form})
        elif self.request.POST['next'] == 'regist':
            form.save()
            # 認証
            user = authenticate(
                username=form.cleaned_data['username'],
                password=form.cleaned_data['password1'],
            )
            # ログイン
            login(self.request, user)
            return super().form_valid(form)
        else:
            # 通常このルートは通らない
            return redirect(reverse_lazy('base:top'))

class UserProfileView(LoginRequiredMixin, TemplateView):
    template_name = 'registration/profile.html'
    def get_queryset(self):
        return User.objects.get(id=self.request.user.id)

class EmailChangeView(LoginRequiredMixin, FormView):
    template_name = 'registration/change.html'
    form_class = EmailChangeForm
    success_url = reverse_lazy('accounts:profile')
    
    def form_valid(self, form):
        #formのupdateメソッドにログインユーザーを渡して更新
        form.update(user=self.request.user)
        return super().form_valid(form)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        # 更新前のユーザー情報をkwargsとして渡す
        kwargs.update({
            'email' : self.request.user.email,
        })
        return kwargs

class UserChangeView(LoginRequiredMixin, FormView):
    '''
    Django組込みのUserを利用する場合のユーザー情報変更ビュー
    カスタムユーザーでは使用しない
    '''
    template_name = 'registration/change.html'
    form_class = UserInfoChangeForm
    success_url = reverse_lazy('accounts:profile')
    
    def form_valid(self, form):
        #formのupdateメソッドにログインユーザーを渡して更新
        form.update(user=self.request.user)
        return super().form_valid(form)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        # 更新前のユーザー情報をkwargsとして渡す
        kwargs.update({
            'email' : self.request.user.email,
            'first_name' : self.request.user.first_name,
            'last_name' : self.request.user.last_name,
        })
        return kwargs

class CustomLoginView(LoginView):
    form_class = CustomAuthenticationForm

class CustomLogoutView(LogoutView):
    template_name = 'registration/logged_out.html'
    next_page = '/'

class CustomPasswordChangeView(PasswordChangeView):
    form_class = CustomPasswordChangeForm
    template_name = 'registration/password_change_form.html'
    success_url = reverse_lazy('accounts:password_change_done')

class CustomPasswordChangeDoneView(PasswordChangeDoneView):
    template_name = 'registration/password_change_done.html'

class CustomPasswordResetView(PasswordResetView):
    email_template_name = 'registration/password_reset_email.html'
    form_class = CustomPasswordResetForm
    from_email = 'info@example.com'
    subject_template_name = 'registration/password_reset_subject.txt'
    success_url = reverse_lazy('accounts:password_reset_done')
    template_name = 'registration/password_reset_form.html'

class CustomPasswordResetDoneView(PasswordResetDoneView):
    template_name = 'registration/password_reset_done.html'

class CustomPasswordResetConfirmView(PasswordResetConfirmView):
    form_class = CustomSetPasswordForm
    post_reset_login = False
    post_reset_login_backend = None
    success_url = reverse_lazy('accounts:password_reset_complete')
    template_name = 'registration/password_reset_confirm.html'

class CustomPasswordResetCompleteView(PasswordResetCompleteView):
    template_name = 'registration/password_reset_complete.html'

ビューに関しては基本的にはUserモデルのインポートとフォームを変更するだけで問題ありません。ただしEmailChangeViewは新設しています。

テンプレートの変更

ユーザーからfirst_nameとlast_nameを削除したため、テンプレートにも多少修正が必要です。以下変更したテンプレートのみ表示します。

templates/accounts/profile.html(一部抜粋)



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">ユーザー情報</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>ユーザー情報</h3></div>
                <div class="ui divided bulleted list">
                    <div class="item">ログインID:{{user.username}}</div>
                    <div class="item">E-mail:{% if user.email %}{{user.email}}{% else %}未設定{% endif %}</div>
                    <!-- <div class="item">名字:{% if user.last_name %}{{user.last_name}}{% else %}未設定{% endif %}</div>
                    <div class="item">名前:{% if user.first_name %}{{user.first_name}}{% else %}未設定{% endif %}</div> -->
                </div>
            </div>
        </div>
        <a class="ui button" href="{% url 'accounts:change' %}">登録情報変更</a>
        <a class="ui button" href="{% url 'accounts:password_change' %}">パスワード変更</a>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

templates/accounts/create_confirm.html(一部抜粋)



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">ユーザー作成</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>ユーザー作成</h3></div>
                <table  class="ui celled table table table-hover" >
                    <tr><td>お名前</td><td>{{form.cleaned_data.username}}</td></tr>
                    <tr><td>メールアドレス</td><td>{{form.cleaned_data.email}}</td></tr>
                    <tr><td>パスワード</td><td>********</td></tr>
                </table>
                <form class="ui form" action="" method="POST">
                    {% csrf_token %}
                    {% for field in form %}
                    {{field.as_hidden}}
                    {% endfor %}
                    <button class="ui button" name="next" value="back" type="submit">修正</button>
                    <button class="ui orange button" name="next" value="regist" type="submit">登録</button>
                </form>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

独自ユーザーを管理画面で表示する設定

独自に作成したユーザーは登録しないと管理画面で表示・修正することは出来ません。accounts/admin.pyに追記します。

accounts/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 User
from .forms import AdminUserCreationForm, CustomUserChangeForm

class CustomUserAdmin(UserAdmin):
    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('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', 'email', 'password1', 'password2'), }),)
    form = CustomUserChangeForm
    add_form = AdminUserCreationForm
    list_display = ('username', 'email','is_staff')
    search_fields = ('username', 'email')
    ordering = ('username',)

admin.site.register(User, CustomUserAdmin)

ここも基本的にはdjango.contrib.auth.admin.UserAdminを継承したクラスを作成してパラメータを上書きしただけです。特にformとadd_formに作成したフォームを指定することを忘れないで下さい。add_formはUserモデルを作成するためのフォームです。間違えないようにしましょう。

URL設定

ビューに変更を加えているのでURL設定も変更しましょう。

accounts/urls.py


from django.urls import path, include
from django.contrib.auth import views as av
from . import views
from .forms import (
    CustomAuthenticationForm, CustomPasswordChangeForm
)

app_name = 'accounts'

urlpatterns = [
    # path('', include('django.contrib.auth.urls')),
    # copy from django.contrib.auth.urls.py
    # path('login/', av.LoginView.as_view(form_class=CustomAuthenticationForm
    #                                     ), name='login'),
    path('login/', views.CustomLoginView.as_view(), name='login'),
    path('logout/', views.CustomLogoutView.as_view(), name='logout'),
    # path('logout/', av.LogoutView.as_view(
    #     template_name='regstration/logged_out.html',
    #     next_page='/'
    # ), name='logout'),

    # path('password_change/', av.PasswordChangeView.as_view(
    #     form_class=CustomPasswordChangeForm,
    #     template_name='registration/password_change_form.html',
    #     success_url='accounts/password_change/done/'
    # ), name='password_change'),
    # path('password_change/done/', av.PasswordChangeDoneView.as_view(
    #     template_name = 'registration/password_change_done.html',
    # ), name='password_change_done'),

    path('password_change/', views.CustomPasswordChangeView.as_view(), name='password_change'),
    path('password_change/done/', views.CustomPasswordChangeDoneView.as_view(), name='password_change_done'),
    

    # path('password_reset/', av.PasswordResetView.as_view(), name='password_reset'),
    # path('password_reset/done/', av.PasswordResetDoneView.as_view(), name='password_reset_done'),
    # path('reset///', av.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    # path('reset/done/', av.PasswordResetCompleteView.as_view(), name='password_reset_complete'),

    path('password_reset/', views.CustomPasswordResetView.as_view(), name='password_reset'),
    path('password_reset/done/', views.CustomPasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset///', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', views.CustomPasswordResetCompleteView.as_view(), name='password_reset_complete'),


    path('create/', views.UserCreateView.as_view(), name="create"),
    path('profile/', views.UserProfileView.as_view(), name="profile"),
    path('change/', views.EmailChangeView.as_view(), name="change"),
]

基本的にはEmailChangeViewを設定するだけです。

独自ユーザーを使用する設定

独自ユーザーを使用するためにはAUTH_USER_MODELを指定する必要があります。この値はデフォルトでは’auth.User’となっているのですが、この値を{アプリケーション名}.{ユーザーモデル名}に変更します。
mysite/settings.py(一部抜粋)


+ AUTH_USER_MODEL = 'accounts.User'

カスタムユーザーモデルをThreadアプリのモデルに適用する

ここまで学習のためThreadアプリのモデルにはDjangoのユーザーを適用させてきました。カスタムユーザーを作成したのでThreadモデルにもカスタムユーザーを紐付ける修正をしましょう。
thread/models.py(一部抜粋)

from accounts.models import User
・・・省略・・・

class Topic(models.Model):
    ・・・省略・・・
    user = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    email = models.EmailField(
        verbose_name='メールアドレス',
        null=True,
        blank=True,
    )
    ・・・省略・・・

class Comment(models.Model):
    ・・・省略・・・
    user = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    email = models.EmailField(
        verbose_name='メールアドレス',
        null=True,
        blank=True,
    )
    ・・・省略・・・

データベースマイグレーション

では、新規作成したデータベースにマイグレーションを行いましょう。以下コマンドを入力してマイグレーションを行います。


$ ./manage.py makemigrations
$ ./manage.py migrate

動作確認

では動作確認をしていきましょう。

ユーザー登録画面

ユーザープロフィール画面

ユーザー情報変更画面

最後に

今回はほとんど、Django既存ユーザーのコピーを追加した感じでしたが、このユーザーをベースにより自分好みのモデルにカスタマイズしていくことが出来ます。次回はログイン方式を変更していきましょう。

Sponsored Link


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です