Djangoでユーザー作成処理(仮登録後、URLクリックで本登録)

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年12月2日20:55に更新(約11日前)
2018年10月21日9:10に作成(約53日前)

旧ブログ移行記事です。

概要

Djangoで、会員登録機能を自作するシリーズの1つです。

前回、ログインページを作りました。 今回はユーザー登録機能と、仮登録後にメールが届き、それにアクセスさせると本登録する、といった機能を実装します。

見た目

ログインページに、会員登録用のリンクが増えました。

メールアドレスと、パスワードを入力します。

この状態ではまだ仮登録です。

このようなメールが届きます。本登録用のURLがついていますね。

そのURLクリックで、本登録です。

ソースコードと解説

settings.py

今回、メールで本登録用のURLを送付します。 とりあえずは、メール内容をコンソールに表示することにします。

# メールをコンソールに表示する
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Gmail等を使ったり、メール送信の詳しい方法は以下の記事にまとめています。
Djangoで、メールを送信

urls.py

アプリケーション側のurls.pyに、いくつか追加します。前回から増えた部分だけ書いています。

from django.urls import path
from . import views

app_name = 'register'

urlpatterns = [
...
...
    # この3つが増えた
    path('user_create/', views.UserCreate.as_view(), name='user_create'),
    path('user_create/done', views.UserCreateDone.as_view(), name='user_create_done'),
    path('user_create/complete/<token>/', views.UserCreateComplete.as_view(), name='user_create_complete'),
]

views.py

こちらも、前回から増えた分を書いています。

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import (
    LoginView, LogoutView
)
from django.contrib.sites.shortcuts import get_current_site
from django.core.signing import BadSignature, SignatureExpired, loads, dumps
from django.http import Http404, HttpResponseBadRequest
from django.shortcuts import redirect
from django.template.loader import get_template
from django.views import generic
from .forms import (
    LoginForm, UserCreateForm
)


User = get_user_model()
...
...
...
class UserCreate(generic.CreateView):
    """ユーザー仮登録"""
    template_name = 'register/user_create.html'
    form_class = UserCreateForm

    def form_valid(self, form):
        """仮登録と本登録用メールの発行."""
        # 仮登録と本登録の切り替えは、is_active属性を使うと簡単です。
        # 退会処理も、is_activeをFalseにするだけにしておくと捗ります。
        user = form.save(commit=False)
        user.is_active = False
        user.save()

        # アクティベーションURLの送付
        current_site = get_current_site(self.request)
        domain = current_site.domain
        context = {
            'protocol': request.scheme,
            'domain': domain,
            'token': dumps(user.pk),
            'user': user,
        }

        subject_template = get_template('register/mail_template/create/subject.txt')
        subject = subject_template.render(context)

        message_template = get_template('register/mail_template/create/message.txt')
        message = message_template.render(context)

        user.email_user(subject, message)
        return redirect('register:user_create_done')


class UserCreateDone(generic.TemplateView):
    """ユーザー仮登録したよ"""
    template_name = 'register/user_create_done.html'


class UserCreateComplete(generic.TemplateView):
    """メール内URLアクセス後のユーザー本登録"""
    template_name = 'register/user_create_complete.html'
    timeout_seconds = getattr(settings, 'ACTIVATION_TIMEOUT_SECONDS', 60*60*24)  # デフォルトでは1日以内

    def get(self, request, **kwargs):
        """tokenが正しければ本登録."""
        token = kwargs.get('token')
        try:
            user_pk = loads(token, max_age=self.timeout_seconds)

        # 期限切れ
        except SignatureExpired:
            return HttpResponseBadRequest()

        # tokenが間違っている
        except BadSignature:
            return HttpResponseBadRequest()

        # tokenは問題なし
        else:
            try:
                user = User.objects.get(pk=user_pk)
            except User.DoesNotExist:
                return HttpResponseBadRequest()
            else:
                if not user.is_active:
                    # 問題なければ本登録とする
                    user.is_active = True
                    user.save()
                    return super().get(request, **kwargs)

        return HttpResponseBadRequest()

get_user_model関数は、そのプロジェクトで使用しているUserモデルを取得します。つまりデフォルトのUserか、カスタムしたUserが帰ります。このように書くと汎用的な処理を書くことができ、逆に言うとこれを使わずにUserモデルをimportしていると別種類のユーザーが使えなくなります。

from django.contrib.auth import get_user_model
...
...
User = get_user_model()
# from django.contrib.auth.models import User これはあまりうまくない

ユーザーの仮登録用のビューです。

class UserCreate(generic.CreateView):
    """ユーザー仮登録"""
    template_name = 'register/user_create.html'
    form_class = UserCreateForm

    def form_valid(self, form):
        """仮登録と本登録用メールの発行."""
        # 仮登録と本登録の切り替えは、is_active属性を使うと簡単です。
        # 退会処理も、is_activeをFalseにするだけにしておくと捗ります。
        user = form.save(commit=False)
        user.is_active = False
        user.save()

        # アクティベーションURLの送付
        current_site = get_current_site(self.request)
        domain = current_site.domain
        context = {
            'protocol': request.scheme,
            'domain': domain,
            'token': dumps(user.pk),
            'user': user,
        }

        subject_template = get_template('register/mail_template/create/subject.txt')
        subject = subject_template.render(context)

        message_template = get_template('register/mail_template/create/message.txt')
        message = message_template.render(context)

        user.email_user(subject, message)
        return redirect('register:user_create_done')

まず、is_active=Falseにすることで仮登録にすることにしました(今回、デフォルトがTrueの設定でした)。

    def form_valid(self, form):
        """仮登録と本登録用メールの発行."""
        # 仮登録と本登録の切り替えは、is_active属性を使うと簡単です。
        # 退会処理も、is_activeをFalseにするだけにしておくと捗ります。
        user = form.save(commit=False)
        user.is_active = False
        user.save()

django.core.signing.dumpを使うことで、tokenを生成しています。これはsettings.pyのSECRET_KEYの値等から生成される文字列で、第三者が推測しずらい文字列です。

            'token': dumps(user.pk),

UserCreateDoneビューは特に言うことはないです。仮登録したよと表示するだけのものです。

こちらは、本登録用のURLでアクセスしたきた人を検証するビューです。

class UserCreateComplete(generic.TemplateView):
    """メール内URLアクセス後のユーザー本登録"""
    template_name = 'register/user_create_complete.html'
    timeout_seconds = getattr(settings, 'ACTIVATION_TIMEOUT_SECONDS', 60*60*24)  # デフォルトでは1日以内

    def get(self, request, **kwargs):
        """tokenが正しければ本登録."""
        token = kwargs.get('token')
        try:
            user_pk = loads(token, max_age=self.timeout_seconds)

        # 期限切れ
        except SignatureExpired:
            return HttpResponseBadRequest()

        # tokenが間違っている
        except BadSignature:
            return HttpResponseBadRequest()

        # tokenは問題なし
        else:
            try:
                user = User.objects.get(pk=user_pk)
            except User.DoesNotExist:
                return HttpResponseBadRequest()
            else:
                if not user.is_active:
                    # 問題なければ本登録とする
                    user.is_active = True
                    user.save()
                    return super().get(request, **kwargs)

        return HttpResponseBadRequest()

django.core.signing.dumps(user.pk)として作成したトークンは、django.core.signing.loads(token)としてuserのpkに復号化できます。 max_ageで有効期限の設定が可能です。

    def get(self, request, **kwargs):
        """tokenが正しければ本登録."""
        token = kwargs.get('token')
        try:
            user_pk = loads(token, max_age=self.timeout_seconds)

アクティベーションURLの期限は、settings.pyにてACTIVATION_TIMEOUT_SECONDS = 60*60*24等と書いてもいいですし、timeout_seconds属性を書き換えてても動作します。 606024は、1分×60×24 の一日です。

class UserCreateComplete(generic.TemplateView):
    """メール内URLアクセス後のユーザー本登録"""
    template_name = 'register/user_create_complete.html'
    timeout_seconds = getattr(settings, 'ACTIVATION_TIMEOUT_SECONDS', 60*60*24)  # デフォルトでは1日以内

SignatureExpired例外は期限切れ、デフォルトならば1日過ぎてからアクティベーションリンクを踏んだということです。 BadSignatureは、そもそもトークンを適当に入力していたりする場合です。

        # 期限切れ
        except SignatureExpired:
            return HttpResponseBadRequest()

        # tokenが何かおかしいとき
        except BadSignature:
            return HttpResponseBadRequest()

問題なければis_active=Trueで本登録とします。

        # tokenは問題なし
        else:
            try:
                user = User.objects.get(pk=user_pk)
            except User.DoesNotExist:
                return HttpResponseBadRequest()
            else:
                if not user.is_active:
                    # 問題なければ本登録とする
                    user.is_active = True
                    user.save()
                    return super().get(request, **kwargs)

        return HttpResponseBadRequest()

forms.py

増えたのはユーザー登録用フォームのUserCreateFormです。Djangoで用意されたUserCreationフォームを継承すればすぐですが、UserCreationフォームではpassword1とpassword2が単純なフィールドとして定義されていることに注意です。つまり、Meta内でwidget=...といった上書きはできないことを意味します。なので、forms.pyにてこれらのcssのclassをいじりたい場合は__init__メソッドやクラスのフィールドとして定義しなおす必要があります。

from django import forms
from django.contrib.auth.forms import (
    AuthenticationForm, UserCreationForm
)
from django.contrib.auth import get_user_model

User = get_user_model()


class LoginForm(AuthenticationForm):
    """ログインフォーム"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
            field.widget.attrs['placeholder'] = field.label  # placeholderにフィールドのラベルを入れる


class UserCreateForm(UserCreationForm):
    """ユーザー登録用フォーム"""

    class Meta:
        model = User
        if User.USERNAME_FIELD == 'email':
            fields = ('email',)
        else:
            fields = ('username', 'email')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'

fieldsが少し特殊ですが、やりたいことはユーザー名として使っているフィールドを指定し、emailフィールドも使う、という指定です。メールアドレスは本登録の際に使うためです。 USERNAME_FIELDがusernameだった場合はそれとemailが、USERNAME_FIELDがemailならば、emailのみが使われます。

    class Meta:
        model = User
        if User.USERNAME_FIELD == 'email':
            fields = ('email',)
        else:
            fields = ('username', 'email')

通常のUserモデルを使う場合も、このフォームは利用できます。その際は、migrationsディレクトリを消し、models.pyのカスタムユーザーとadmiin.pyの記述を消しましょう。

mail_template/create/subject.txt

仮登録メールで使う題名です。正確なパスは、register/templates/register/mail_template/create/subject.txt です。mail_templateのように専用のディレクトリを作っておくとわかりやすいです。

ほにゃらら - 会員登録

mail_template/create/message.txt

こっちはメールの本文部分です。今回のカスタムユーザーはusername属性にアクセスすると、emailが返されます。通常のUserモデルでも使えるテンプレートだと思います。

{{ user.username }} 様 会員登録手続きを行っていただき、ありがとうございます。

下記URLよりサイトにアクセスの上、引き続き会員登録をお願いいたします。
まだ会員登録手続きは完了しておりませんので、ご注意ください。

本登録用URL
{{ protocol}}://{{ domain }}{% url 'register:user_create_complete' token %}

ほにゃらら

login.html

前回から少しかわっています。右側に会員登録リンクが増えていましたね。

{% extends "register/base.html" %}
{% block content %}
<div class="row">
    <!-- 左側、ログインエリア -->
    <div class="card col-md-6">
        <div class="card-body">
            <form action="{% url 'register:login' %}" method="POST">
                {{ form.non_field_errors }}
                {% for field in form %}
                    {{ field }}
                    {{ field.errors }}
                    <hr>
                {% endfor %}
                <button type="submit" class="btn btn-success btn-lg btn-block" >ログイン</button>
                <input type="hidden" name="next" value="{{ next }}" />
                {% csrf_token %}
            </form>
        </div>
    </div><!-- 左側、ログインエリアおわり -->

    <!-- 右側、会員登録エリア -->
    <div class="card col-md-6">
        <div class="card-body">
            <a href="{% url 'register:user_create' %}" class="btn btn-success btn-lg btn-block" >会員登録</a>
        </div>
    </div><!-- 右側、会員登録エリアおわり -->
</div>
{% endblock %}

user_create.html

よくある、汎用的なフォームの書き方です。

{% extends "register/base.html" %}
{% block content %}
<form action="" method="POST">
    {{ form.non_field_errors }}
    {% for field in form %}
    <div class="form-group">
        <label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
        {{ field }}
        {{ field.errors }}
    </div>
    {% endfor %}
    {% csrf_token %}
    <button type="submit" class="btn btn-primary btn-lg">送信</button>
</form>
{% endblock %}

user_create_done.html

仮登録したよーページです。

{% extends "register/base.html" %}
{% block content %}
<p>
    会員登録確認メールを送信しました。まだ、会員登録は完了していません。<br>
    確認メールを送信しましたので、確認メールに記載されているリンクから本登録を行ってください。
</p>
{% endblock %}

user_create_complte.html

本登録おわったよーページです。

{% extends "register/base.html" %}
{% block content %}
<p>
    本登録が完了しました。<br>
    登録されたメールアドレスとパスワードでログインしてください。<br>
    <a class="btn btn-primary btn-lg" href="{% url 'register:login' %}">ログイン</a>
</p>
{% endblock %}

本登録時にログインさせてしまう場合

is_activeをTrueにした後にでも、下記のように書きます。もし独自の認証バックエンドをsettings.pyで指定していれば、それを指定します。

from django.contrib.auth import login
...
...
            user.backend = 'django.contrib.auth.backends.ModelBackend'
            login(request, user)

仮登録はなしで、すぐに本登録させたい

  1. UserCreateのform_validとUserCreateCompleteのgetを削除し
  2. UserCreateのsuccess_urlでUserCreateCompleteにリダイレクトさせる

で大丈夫だと思います。

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする