torinaブログ

DjangoとBootstrap4で作成したブログ
Python, Django, Kivy, Bootstrap, Apache等のメモです
ソースコード

Djangoで、ユーザ登録~URLクリックで仮登録、パスワードリマインダー等

Python Django Bootstrap4 Django Bitbucketにソースあり
約161日前 2016年9月19日0:28
トップページ


ログインページ


会員登録ページ


実際に登録してみると、仮登録になり


メールが届き、URLをクリック



すると、本登録となります。


パスワードを忘れた場合は、ログインページのパスわード忘れたボタンを押し、メールアドレスを入力


すると、またもやURLが届いて...



クリックすると、パスワード設定ページ



ログインし、右上からマイページへ移動すると


ユーザ情報が簡単に表示される。


ユーザ情報の変更ページや、パスワード変更ページもあります




Djangoで、ユーザ登録機能(汎用ビュー)
https://torina.top/main/286/

Djangoで、Userモデルを使ったユーザ情報変更ページとパスワード変更ページ
https://torina.top/main/280/

Djangoで仮登録後、メールから本登録させる
https://torina.top/main/273/

Django メールアドレスでログイン
https://torina.top/main/272/

等のまとめ的な内容になります。
結構量があるので、細かい解説はしません。

Django1.10
Python3.5
プロジェクト名は「django-easy-regist」
アプリケーション名は「easy_regist」
です。

django-easy-regist/django-easy-regist/settings.py
ログイン関連のURLや、Gmailで送信する設定をしています。
"""
Django settings for mysite project.

Generated by 'django-admin startproject' using Django 1.10.1.

For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'wshrs528nvsp=l)k*jc_7_8ie*(bvf$0jrtqegz23zq7sjdf-h'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'easy_regist',  # 足した
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'django-easy-regist.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        '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',
            ],
        },
    },
]

WSGI_APPLICATION = 'django-easy-regist.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/

STATIC_URL = '/static/'

# 以下も追加
LOGIN_URL = "easy_regist:login"  # ログインするページ。デフォルトにするなら"/admin/login/"等も
LOGIN_REDIRECT_URL = 'easy_regist:index'  # ログインページに直接飛んだとき、ログイン完了後のリダイレクト先


# Gメールで送信する例
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'toritoritorina@gmail.com'
EMAIL_HOST_PASSWORD = 'パスワードをいれる'
EMAIL_USE_TLS = True


settings.pyのLOGIN_URL等には、以下の書き方もできます。
LOGIN_URL = "easy_regist:login"  # ログインするページ。デフォルトにするなら"/admin/login/"等も
LOGIN_REDIRECT_URL = 'easy_regist:index'  # ログインページに直接飛んだとき、ログイン完了後のリダイレクト先


django-easy-regist/django-easy-regist/urls.py
いつもの、アプリケーションのurl.pyを読み込む設定と、adminサイトの利用
from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('easy_regist.urls', namespace='easy_regist')),
]



django-easy_regist/easy_regist/urls.py
長くなりました。
from django.conf.urls import url
from . import views


urlpatterns = [

    # トップページ
    url(r'^$', views.TopPageView.as_view(), name='index'),

    # マイページ機能
    url(r'^mypage/$', views.MyPageView.as_view(), name='mypage'),
    url(r'^user_update/(?P<pk>[0-9]+)/$',
        views.UserUpdateView.as_view(), name='user_update'),
    url(r'^change_password/$', views.change_password, name='change_password'),
    url(r'^change_password_done/$', views.change_password_done,
        name='change_password_done'),

    # 会員登録
    url(r'^create/$', views.CreateUserView.as_view(), name='create'),
    url(r'^create_done/$', views.CreateDoneView.as_view(), name='create_done'),
    url(r'^create_complete/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
        views.CreateCompleteView.as_view(), name='create_complete'),

    # ログイン、ログアウト
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),

    # パスワード忘れ
    url(r'^password_reset/$', views.password_reset, name='password_reset'),
    url(r'^password_reset_done/$', views.password_reset_done,
        name='password_reset_done'),
    url(r'^password_reset_confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
        views.password_reset_confirm, name='password_reset_confirm'),
    url(r'^password_reset_complete/$', views.password_reset_complete,
        name='password_reset_complete'),
]


django-easy_regist/easy_regist/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, PasswordChangeForm, PasswordResetForm, SetPasswordForm
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import validate_email


class RegisterForm(UserCreationForm):

    first_name = forms.CharField(label="姓", required=True)
    last_name = forms.CharField(label="名", required=True)

    class Meta:
        model = User
        fields = (
            "username", "password1", "password2",
            "first_name", "last_name",
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['class'] = 'form-control'
        self.fields['username'].widget.attrs['placeholder'] = 'メールアドレス'

        self.fields['first_name'].widget.attrs['class'] = 'form-control'
        self.fields['first_name'].widget.attrs['placeholder'] = '姓'

        self.fields['last_name'].widget.attrs['class'] = 'form-control'
        self.fields['last_name'].widget.attrs['placeholder'] = '名'

        self.fields['password1'].widget.attrs['class'] = 'form-control'
        self.fields['password1'].widget.attrs['placeholder'] = 'パスワード'

        self.fields['password2'].widget.attrs['class'] = 'form-control'
        self.fields['password2'].widget.attrs['placeholder'] = 'パスワード(確認)'

    def clean_username(self):
        username = self.cleaned_data["username"]
        try:
            validate_email(username)
        except ValidationError:
            raise ValidationError("正しいメールアドレスを指定してください。")

        try:
            self.user = User.objects.get(username=username)
        except User.DoesNotExist:
            return username
        else:
            raise ValidationError("既に存在するメールアドレスです。")


class UpdateForm(forms.ModelForm):

    first_name = forms.CharField(label="姓", required=True)
    last_name = forms.CharField(label="名", required=True)

    class Meta:
        model = User
        fields = (
            "username", "first_name", "last_name",
        )

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['class'] = 'form-control'
        self.fields['first_name'].widget.attrs['class'] = 'form-control'
        self.fields['last_name'].widget.attrs['class'] = 'form-control'

    def clean_username(self):
        username = self.cleaned_data["username"]
        try:
            validate_email(username)
        except ValidationError:
            raise ValidationError("正しいメールアドレスを指定してください。")

        try:
            self.user = User.objects.get(username=username)
        except User.DoesNotExist:
            return username
        else:
            if self.user.username == username:
                return username
            raise ValidationError("既に存在するメールアドレスです。")


class LoginForm(AuthenticationForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['class'] = 'form-control'
        self.fields['username'].widget.attrs['placeholder'] = 'メールアドレス'

        self.fields['password'].widget.attrs['class'] = 'form-control'
        self.fields['password'].widget.attrs['placeholder'] = 'パスワード'


class ForgetPasswordForm(PasswordResetForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['email'].widget.attrs['class'] = 'form-control'
        self.fields['email'].widget.attrs['placeholder'] = 'メールアドレス'


class ChangePasswordForm(PasswordChangeForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['new_password1'].widget.attrs['class'] = 'form-control'
        self.fields['new_password2'].widget.attrs['class'] = 'form-control'
        self.fields['old_password'].widget.attrs['class'] = 'form-control'


class PasswordConfirmForm(SetPasswordForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['new_password1'].widget.attrs['class'] = 'form-control'
        self.fields['new_password1'].widget.attrs['placeholder'] = '新パスワード'
        self.fields['new_password2'].widget.attrs['class'] = 'form-control'
        self.fields['new_password2'].widget.attrs['placeholder'] = '新パスワード(確認)'


ユーザ登録ようのFormです。
UserCreationFormを継承し、姓、名を必須項目として上書き
今回はメールアドレスがユーザID的なものになっており、usernameをメールアドレスとして扱います。そのため、userモデルのメール項目は使っていません。
__init__ではBootstrap用のclass指定とプレースホルダの入力、
clean_usernameは、usernameがメールアドレスとして問題ないかを確認するのと、そのアドレスがまだ使われていないかの確認をします。
class RegisterForm(UserCreationForm):

    first_name = forms.CharField(label="姓", required=True)
    last_name = forms.CharField(label="名", required=True)

    class Meta:
        model = User
        fields = (
            "username", "password1", "password2",
            "first_name", "last_name",
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['class'] = 'form-control'
        self.fields['username'].widget.attrs['placeholder'] = 'メールアドレス'

        self.fields['first_name'].widget.attrs['class'] = 'form-control'
        self.fields['first_name'].widget.attrs['placeholder'] = '姓'

        self.fields['last_name'].widget.attrs['class'] = 'form-control'
        self.fields['last_name'].widget.attrs['placeholder'] = '名'

        self.fields['password1'].widget.attrs['class'] = 'form-control'
        self.fields['password1'].widget.attrs['placeholder'] = 'パスワード'

        self.fields['password2'].widget.attrs['class'] = 'form-control'
        self.fields['password2'].widget.attrs['placeholder'] = 'パスワード(確認)'

    def clean_username(self):
        username = self.cleaned_data["username"]
        try:
            validate_email(username)
        except ValidationError:
            raise ValidationError("正しいメールアドレスを指定してください。")

        try:
            self.user = User.objects.get(username=username)
        except User.DoesNotExist:
            return username
        else:
            raise ValidationError("既に存在するメールアドレスです。")



ユーザ情報更新用Formです。
clean_usernameでメールアドレスが使われていないかを確認しつつ、現在使っているメールアドレスは許可する、という処理をしています。
これをしないと、メールアドレスを変更せず姓、名のみ更新した場合にエラーとなります。
そのため、__init__内でself.userとしてログイン中のユーザを一度格納し、clean_usernameでそれを取り出しています。
Formにrequest.userを渡しているのはview側です。後で紹介します。
class UpdateForm(forms.ModelForm):

    first_name = forms.CharField(label="姓", required=True)
    last_name = forms.CharField(label="名", required=True)

    class Meta:
        model = User
        fields = (
            "username", "first_name", "last_name",
        )

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['class'] = 'form-control'
        self.fields['first_name'].widget.attrs['class'] = 'form-control'
        self.fields['last_name'].widget.attrs['class'] = 'form-control'

    def clean_username(self):
        username = self.cleaned_data["username"]
        try:
            validate_email(username)
        except ValidationError:
            raise ValidationError("正しいメールアドレスを指定してください。")

        try:
            self.user = User.objects.get(username=username)
        except User.DoesNotExist:
            return username
        else:
            if self.user.username == username:
                return username
            raise ValidationError("既に存在するメールアドレスです。")


残りのフォームには、classやplaceholderを指定するために作成したフォームです。
全てdjango.contrib.auth.formsにあるもので、cssのclassを変更する必要があったので作成しただけです。
LoginForm
(django.contrib.auth.forms.AuthenticationForm)
→ログイン用のフォーム

ForgetPasswordForm
(django.contrib.auth.forms.PasswordResetForm)
→メールアドレスを入力し、そのメールにパスワード設定用ページのURLを送付するのに使う

PasswordConfirmForm
(django.contrib.auth.forms.SetPasswordForm)
→パスワード忘れ、設定用ページにきたユーザに新パスワードを入力させるフォーム。ForgetPasswordFormの後に使う。

ChangePasswordForm
(django.contrib.auth.forms.PasswordChangeForm)
→パスワード変更用フォーム。古いパスワードと、新しいパスワードを入力して使う。現在のパスワードがわかっている場合の変更はこれ。



django-easy_regist/easy_regist/views.py
ながい
from django.conf import settings
from django.contrib.auth import views as auth_views
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail
from django.core.urlresolvers import reverse_lazy
from django.http import Http404
from django.template.loader import get_template
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.views import generic

from .forms import (
    RegisterForm,
    LoginForm,
    UpdateForm,
    ChangePasswordForm,
    ForgetPasswordForm,
    PasswordConfirmForm
)


class TopPageView(generic.TemplateView):
    template_name = "easy_regist/index.html"


class MyPageView(LoginRequiredMixin, generic.TemplateView):
    template_name = "easy_regist/info.html"


class UserUpdateView(LoginRequiredMixin, generic.UpdateView):
    model = User
    form_class = UpdateForm
    success_url = reverse_lazy('easy_regist:mypage')
    template_name = "easy_regist/user_update.html"

    def form_valid(self, form):
        user = form.save(commit=False)
        user.email = user.username
        user.save()
        return super(UserUpdateView, self).form_valid(form)

    def get_form_kwargs(self):
        kwargs = super(UserUpdateView, self).get_form_kwargs()
        kwargs.update({'user': self.request.user})
        return kwargs


def change_password(request):
    context = {
        'post_change_redirect': reverse_lazy('easy_regist:change_password_done'),
        'template_name': 'easy_regist/change_password.html',
        'password_change_form': ChangePasswordForm,
    }
    return auth_views.password_change(request, **context)


def change_password_done(request):
    context = {
        'template_name': 'easy_regist/change_password_done.html',
    }
    return auth_views.password_change_done(request, **context)


class CreateUserView(generic.FormView):
    template_name = 'easy_regist/create.html'
    form_class = RegisterForm
    success_url = reverse_lazy('easy_regist:create_done')

    def form_valid(self, form):
        user = form.save(commit=False)
        user.is_active = False
        user.email = user.username
        user.save()

        current_site = get_current_site(self.request)
        domain = current_site.domain

        subject_template = get_template('easy_regist/mailtemplate/new/subject.txt')
        message_template = get_template('easy_regist/mailtemplate/new/message.txt')

        context = {
            'protocol': 'https' if self.request.is_secure() else 'http',
            'domain': domain,
            'uid': urlsafe_base64_encode(force_bytes(user.pk)),
            'token': default_token_generator.make_token(user),
            'user': user,
        }

        subject = subject_template.render(context)
        message = message_template.render(context)
        from_email = settings.EMAIL_HOST_USER
        to = [user.username]

        send_mail(subject, message, from_email, to)
        return super(CreateUserView, self).form_valid(form)


class CreateDoneView(generic.TemplateView):
    template_name = "easy_regist/create_done.html"


class CreateCompleteView(generic.TemplateView):
    template_name = 'easy_regist/create_complete.html'

    def get(self, request, **kwargs):
        token = kwargs.get("token")
        uidb64 = kwargs.get("uidb64")
        try:
            uid = force_text(urlsafe_base64_decode(uidb64))
            user = User.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None

        if user and not user.is_active and default_token_generator.check_token(user, token):
            user.is_active = True
            user.save()
            return super(CreateCompleteView, self).get(request, **kwargs)
        else:
            raise Http404


def password_reset(request):
    context = {
        'post_reset_redirect': reverse_lazy('easy_regist:password_reset_done'),
        'template_name': 'easy_regist/password_reset_form.html',
        'email_template_name': 'easy_regist/mailtemplate/password_reset/message.txt',
        'subject_template_name': 'easy_regist/mailtemplate/password_reset/subject.txt',
        'password_reset_form': ForgetPasswordForm,
    }
    return auth_views.password_reset(request, **context)


def password_reset_done(request):
    context = {
        'template_name': 'easy_regist/password_reset_done.html',
    }
    return auth_views.password_reset_done(request, **context)


def password_reset_confirm(request, uidb64, token):
    context = {
        'uidb64': uidb64,
        'token': token,
        'post_reset_redirect': reverse_lazy('easy_regist:password_reset_complete'),
        'template_name': 'easy_regist/password_reset_confirm.html',
        'set_password_form': PasswordConfirmForm,
    }
    return auth_views.password_reset_confirm(request, **context)


def password_reset_complete(request):
    context = {
        'template_name': 'easy_regist/password_reset_complete.html',
    }
    return auth_views.password_reset_complete(request, **context)


def login(request):
    context = {
        'template_name': 'easy_regist/login.html',
        'authentication_form': LoginForm
    }
    return auth_views.login(request, **context)


def logout(request):
    context = {
        'template_name': 'easy_regist/index.html',
    }
    return auth_views.logout(request, **context)



これはトップページに使っています。単純です。
class TopPageView(generic.TemplateView):
    template_name = "easy_regist/index.html"


これはマイページに。
LoginRequiredMixin で、ログインしないと見れなくなっています。
{{ user }}は何もしなくてもtemplateで書けるので、ListViewやDetailViewにする必要はなかったです。
class MyPageView(LoginRequiredMixin, generic.TemplateView):
    template_name = "easy_regist/info.html"



マイページ内のユーザ情報変更ページのビューです。UpdateViewを使用しています。
このアップデートビューは、CreateViewと使い方はほぼ同じです。
model、queryset、又はget_querysetメソッドを上書きしなければなりません。注意です。
class UserUpdateView(LoginRequiredMixin, generic.UpdateView):
    model = User
    form_class = UpdateForm
    success_url = reverse_lazy('easy_regist:mypage')
    template_name = "easy_regist/user_update.html"

    def form_valid(self, form):
        user = form.save(commit=False)
        user.email = user.username
        user.save()
        return super(UserUpdateView, self).form_valid(form)

    def get_form_kwargs(self):
        kwargs = super(UserUpdateView, self).get_form_kwargs()
        kwargs.update({'user': self.request.user})
        return kwargs



form_validで行っているのは、user.emailにusernameを入れているだけです。
今回はusernameをメールアドレスとして扱っているため、user.emailは不要ではありますが、一応user.emailにもメールアドレスを入れています。
get_form_kwargsですが、UpdateFormにrequest.userを渡す必要があるという話をしました。
get_form_kwargsを適切にオーバーライドすることで、Formに渡すパラメータを増やしたりできます。


マイページ内のパスワード変更ページです。
django.contrib.auth.viewsにpassword_changeという関数があり、それを呼び出しています。
その際に、使うテンプレートやフォーム等はこちらで指定したものを渡すことが可能です。
def change_password(request):
    context = {
        'post_change_redirect': reverse_lazy('easy_regist:change_password_done'),
        'template_name': 'easy_regist/change_password.html',
        'password_change_form': ChangePasswordForm,
    }
    return auth_views.password_change(request, **context)



パスワード変更した後に呼び出されるビューで、パスワードを変更しました!と表示するページを見せています。
これもauth.viewsにあったものを利用したのですが、change_passwordのpost_change_redirectをマイページトップにする、とかで充分でしたね。
def change_password_done(request):
    context = {
        'template_name': 'easy_regist/change_password_done.html',
    }
    return auth_views.password_change_done(request, **context)



会員登録ページのビューです。
class CreateUserView(generic.FormView):
    template_name = 'easy_regist/create.html'
    form_class = RegisterForm
    success_url = reverse_lazy('easy_regist:create_done')

    def form_valid(self, form):
        user = form.save(commit=False)
        user.is_active = False
        user.email = user.username
        user.save()

        current_site = get_current_site(self.request)
        domain = current_site.domain

        subject_template = get_template('easy_regist/mailtemplate/new/subject.txt')
        message_template = get_template('easy_regist/mailtemplate/new/message.txt')

        context = {
            'protocol': 'https' if self.request.is_secure() else 'http',
            'domain': domain,
            'uid': urlsafe_base64_encode(force_bytes(user.pk)),
            'token': default_token_generator.make_token(user),
            'user': user,
        }

        subject = subject_template.render(context)
        message = message_template.render(context)
        from_email = settings.EMAIL_HOST_USER
        to = [user.username]

        send_mail(subject, message, from_email, to)
        return super(CreateUserView, self).form_valid(form)


UpdateViewでやっていたように、念のためuser.emailにuser.usernameを入れて保存しています。
仮登録の段階なので、is_activeをFalseにします。
    def form_valid(self, form):
        user = form.save(commit=False)
        user.is_active = False
        user.email = user.username
        user.save()




残りの部分は、メールを送信する処理です。
URLを作成しなければならないため、プロトコルやドメインを取得しています。
        current_site = get_current_site(self.request)
        domain = current_site.domain


メールで送信するためにURLを作成する、という処理は案外多いです。
以前は以下のような書き方をしていました。見づらかったですね
base_url = "/".join(request.build_absolute_uri().split("/")[:3])
activation_url = "{0}/activation/{1}".format(base_url, activate_key)



メールについては、
https://torina.top/main/278/
などでもまとめました。
context内のprotocolはhttp、httpsが入り、domainにはドメインが入ります。userも、わかるかと思います。
        subject_template = get_template('easy_regist/mailtemplate/new/subject.txt')
        message_template = get_template('easy_regist/mailtemplate/new/message.txt')

        context = {
            'protocol': 'https' if self.request.is_secure() else 'http',
            'domain': domain,
            'uid': urlsafe_base64_encode(force_bytes(user.pk)),
            'token': default_token_generator.make_token(user),
            'user': user,
        }

        subject = subject_template.render(context)
        message = message_template.render(context)
        from_email = settings.EMAIL_HOST_USER
        to = [user.username]

        send_mail(subject, message, from_email, to)
        return super(CreateUserView, self).form_valid(form)



今回は仮登録後、メールが届きそのメールをクリックすると本登録になります。
ユーザに送付する本登録メールのURLを作成するのですが、以前に

Djangoで仮登録後、メールから本登録させる
https://torina.top/main/273/

で、uuid.uuid4().hexでURLを作成していましたが、あまりカッコいい感じはしませんでした。
今回は、adminのパスワードリセットに使われている方法を、そのまま流用しています。
            'uid': urlsafe_base64_encode(force_bytes(user.pk)),
            'token': default_token_generator.make_token(user),



メール本文にて、以下のようにしており...
{{ user.first_name }} {{ user.last_name }}さん、以下から本登録して
{{ protocol}}://{{ domain }}{% url 'easy_regist:create_complete' uidb64=uid token=token %}



ユーザはこのurlをクリック、本登録となります。
あい うえおさん、以下から本登録して
http://127.0.0.1:8000/create_complete/NTE/4ff-88de436db000ce5a441a/


これは仮登録しました、本登録はメールを見てね、というページを見せるだけのビュー。
class CreateDoneView(generic.TemplateView):
    template_name = "easy_regist/create_done.html"



メールに届いた本登録用URLをクリックすると、こちらです。
TemplateViewを使い、getメソッドを上書きし、そのURLが正しいかを確認しています。
ちゃんとメール本文のリンクで来たんだな、とわかればuser.is_activeをTrueにし、有効化します。
class CreateCompleteView(generic.TemplateView):
    template_name = 'easy_regist/create_complete.html'

    def get(self, request, **kwargs):
        token = kwargs.get("token")
        uidb64 = kwargs.get("uidb64")
        try:
            uid = force_text(urlsafe_base64_decode(uidb64))
            user = User.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None

        if user and not user.is_active and default_token_generator.check_token(user, token):
            user.is_active = True
            user.save()
            return super(CreateCompleteView, self).get(request, **kwargs)
        else:
            raise Http404


この辺を詳しく知りたい、ということならば、

https://github.com/django/django/blob/master/django/contrib/auth/views.py
のpassword_reset関数、password_reset_confirm関数

https://github.com/django/django/blob/master/django/contrib/auth/forms.py
のPasswordResetFormクラス

https://github.com/django/django/blob/master/django/contrib/auth/tokens.py
(tokenの作成と、チェック)

https://github.com/django/django/blob/master/django/contrib/admin/templates/registration/password_reset_email.html
(adminのパスワードリセットで送られるメール本文の内容)

あたりを見るとよいです。


CreateUserViewは、当初CreateViewを使う予定でした。しかし、FormViewにしました。
default_token_generator.make_token(user)では、内部で
            six.text_type(user.pk) + user.password +
            six.text_type(login_timestamp) + six.text_type(timestamp)


といった感じでハッシュ値を生成しています。user.passwordもかかわっていますね。
今回はCreateUserViewのform_validでUserモデルを作成し、そのユーザを使って
'token': default_token_generator.make_token(user),

とtokenを作成しています。

しかし、CreateViewの祖先にあるModelFormMixinのform_validでも
self.object = form.save()

とsave()をしています。

これの何が問題かというと、RegisterForm(の親のUserCreationForm)のsave()では
    def save(self, commit=True):
        user = super(UserCreationForm, self).save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user

という処理をしており、このset_passwordによってuser.passwordの値が変わってしまっていました。
これを回避する方法は色々とあると思いますが、今回はModelFormMixinのform_validを呼び出さないFormViewでも代用できたので、こちらを使用しました。

今思えば、CreateViewを使いつつ、以下のようにする方がよかったようにも思います。
return super(CreateUserView, self).form_valid(form)
↓
from django.http import HttpResponseRedirect
...
...
return HttpResponseRedirect(self.get_success_url())



パスワードのリセットページのビューです。
post_reset_refirectには入力後のviewを、
email_template_nameにはメールの本文となるものを、
subject_template_nameにはタイトルを、
そしてpassword_reset_form(デフォルトはPasswordResetForm)を指定し、django.contrib.auth.viewsのpassword_resetに渡すだけです。
これに限らず、urls.pyで指定することも可能ですが、今回はviewに書いてます。
def password_reset(request):
    context = {
        'post_reset_redirect': reverse_lazy('easy_regist:password_reset_done'),
        'template_name': 'easy_regist/password_reset_form.html',
        'email_template_name': 'easy_regist/mailtemplate/password_reset/message.txt',
        'subject_template_name': 'easy_regist/mailtemplate/password_reset/subject.txt',
        'password_reset_form': ForgetPasswordForm,
    }
    return auth_views.password_reset(request, **context)


今回は、django.contrib.auth.viewsの機能をかなり活用しました。
渡せるパラメータやデフォルト値などは、余裕があれば以下で確認しましょう。
https://github.com/django/django/blob/master/django/contrib/auth/views.py


パスワワードリセットページ入力後に、リダイレクトするviewです。
templateを指定するだけ
def password_reset_done(request):
    context = {
        'template_name': 'easy_regist/password_reset_done.html',
    }
    return auth_views.password_reset_done(request, **context)



メールのURLにアクセスし、パスワードを設定するviewです。
def password_reset_confirm(request, uidb64, token):
    context = {
        'uidb64': uidb64,
        'token': token,
        'post_reset_redirect': reverse_lazy('easy_regist:password_reset_complete'),
        'template_name': 'easy_regist/password_reset_confirm.html',
        'set_password_form': PasswordConfirmForm,
    }
    return auth_views.password_reset_confirm(request, **context)


パスワード設定後にリダイレクトされるview
def password_reset_complete(request):
    context = {
        'template_name': 'easy_regist/password_reset_complete.html',
    }
    return auth_views.password_reset_complete(request, **context)



ログインと、ログアウト。
ログインに画像クリックやreCAPTCHA等の認証を挟みたいならば、認証成功時だけreturn auth_views.login(request, **context)するとよいです。
Djangoで、reCAPTCHAとログイン処理
https://torina.top/main/279/
def login(request):
    context = {
        'template_name': 'easy_regist/login.html',
        'authentication_form': LoginForm
    }
    return auth_views.login(request, **context)


def logout(request):
    context = {
        'template_name': 'easy_regist/index.html',
    }
    return auth_views.logout(request, **context)


viewはこれで終わりです。
以降はtemplateやstaticの内容ですが、コードだけのせておきます。

django-easy-regist/easy_regist/templates/easy_regist/mailtemplate/new/subject.txt
会員登録時のメール題名
ご登録ありがとうございます。


django-easy-regist/easy_regist/templates/easy_regist/mailtemplate/new/message.txt
会員登録時のメール本文
{{ user.first_name }} {{ user.last_name }}さん、以下から本登録して
{{ protocol}}://{{ domain }}{% url 'easy_regist:create_complete' uidb64=uid token=token %}


django-easy-regist/easy_regist/templates/easy_regist/mailtemplate/password_reset/subject.txt
パスワードリセットのメール題名
パスワードの再登録


django-easy-regist/easy_regist/templates/easy_regist/mailtemplate/password_reset/message.txt
パスワードリセットのメール本文
{{ user.first_name }} {{ user.last_name }}さん、以下からパスワードを再登録して
{{ protocol}}://{{ domain }}{% url 'easy_regist:password_reset_confirm' uidb64=uid token=token %}



django-easy-regist/easy_regist/static/easy_regist/css/main.css
.center {
  padding: 3rem 1.5rem;
  text-align: center;
}

.errorlist li {
  list-style-type: none;
}
 
.errorlist {
  color: red;
  margin-left: 0;
  padding-left: 0;
}


django-easy-regist/easy_regist/static/easy_regist/js/main.js
なし


django-easy-regist/easy_regist/templates/easy_regist/base.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <!-- Required meta tags always come first -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/css/bootstrap.min.css" integrity="2hfp1SzUoho7/TsGGGDaFdsuuDL0LX2hnUp6VkX3CUQ2K4K+xjboZdsXyp4oUHZj" crossorigin="anonymous">

    <!-- Custom CSS -->
    <link rel="stylesheet" href="{% static 'easy_regist/css/main.css' %}">

  </head>
  <body>
    <nav class="navbar navbar-dark bg-primary">
      <div class="container">
        <div class="nav navbar-nav">
          <a class="nav-item nav-link" href="{% url 'easy_regist:index' %}">トップ</a>
          
          {% if user.is_authenticated %}
          <div class="pull-sm-right">
            <a class="nav-item nav-link" href="{% url 'easy_regist:mypage' %}">
              ようこそ、{{ user.username }}!マイページへ
            </a>
            <a class="nav-item nav-link" href="{% url 'easy_regist:logout' %}">ログアウト</a>
          </div>
          
          {% else %}
          <a class="nav-item nav-link pull-sm-right" href="{% url 'easy_regist:login' %}">
            ようこそ、ゲスト!ログインはこちら
          </a>
          
          {% endif %}
        </div><!-- /.nav -->
      </div><!-- /.container -->
    </nav>

    {% block content %} {% endblock %}

    <!-- jQuery first, then Tether, then Bootstrap JS. -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js" integrity="sha384-THPy051/pYDQGanwU6poAc/hOdQxjnOEXzbT+OuUAFqNqFjL+4IGLBgCJC3ZOShY" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.2.0/js/tether.min.js" integrity="sha384-Plbmg8JY28KFelvJVai01l8WyZzrYWG825m+cZ0eDDS1f7d/js6ikvy1+X+guPIB" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/js/bootstrap.min.js" integrity="VjEeINv9OSwtWFLAtmc4JCtEJXXBub00gtSnszmspDLCtC0I4z4nqz7rEFbIZLLU" crossorigin="anonymous"></script>

    <!-- Custom JS -->
    <script src="{% static 'easy_regist/js/main.js' %}"></script>

  </body>
</html>


django-easy-regist/easy_regist/templates/easy_regist/login.html
ログインページ
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ログインページ</h1>
  </div>

  <div class="card-group">
    <div class="card card-outline-primary">
      <div class="card-block">
        <form action="{% url 'easy_regist:login' %}" method="POST">
  		  <p>{{ form.non_field_errors }}</p>
	      {{ form.username }}
	      {{ form.password }}
	      {% csrf_token %}
	  	  <input type="hidden" name="next" value={{ next }} />
	      <button type="submit" class="btn btn-lg btn-block btn-outline-primary">
	        ログイン
	      </button>
	    </form>
      </div>
    </div>
    <div class="card card-outline-primary">
      <div class="card-block">
        <a class="btn btn-lg btn-block btn-outline-primary" href="{% url 'easy_regist:create' %}">
          会員登録
        </a>
        <a class="btn btn-lg btn-block btn-outline-primary" href="{% url 'easy_regist:password_reset' %}">
          パスワードを忘れた
        </a>
      </div>
    </div>

  </div><!-- /.card-group -->
  
</div><!-- /.container -->
{% endblock %}



django-easy-regist/easy_regist/templates/easy_regist/index.html
トップ画面
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>トップページ</h1>
    <p class="lead">Bootstrap4を使っています。</p>
  </div>
</div><!-- /.container -->
{% endblock %}


django-easy-regist/easy_regist/templates/easy_regist/info.html
マイページ
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ユーザ情報</h1>
    <p class="lead">ログイン中のユーザデータを表示します</p>
  </div>

  <table class="table">
    <thead class="thead-inverse">
      <tr>
        <th>名前・説明</th>
        <th>templateでの呼び出し方</th>
        <th>ログインユーザのデータ</th>
        <th>備考</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>ユーザ名</td>
        <td>{% verbatim %} {{ user.username }} {% endverbatim %} </td>
        <td>{{ user.username }}</td>
        <td>必須</td>
      </tr>
      <tr>
        <td>メール</td>
        <td>{% verbatim %} {{ user.email }} {% endverbatim %} </td>
        <td>{{ user.email }}</td>
        <td>デフォルトは空欄でもOK</td>
      </tr>
      <tr>
        <td>姓</td>
        <td>{% verbatim %} {{ user.first_name }} {% endverbatim %} </td>
        <td>{{ user.first_name }}</td>
        <td>デフォルトは空欄でもOK</td>
      </tr>
      <tr>
        <td>名</td>
        <td>{% verbatim %} {{ user.last_name }} {% endverbatim %} </td>
        <td>{{ user.last_name }}</td>
        <td>デフォルトは空欄でもOK</td>
      </tr>
      <tr>
        <td>adminサイトにアクセスできるか</td>
        <td>{% verbatim %} {{ user.is_staff }} {% endverbatim %} </td>
        <td>{{ user.is_staff }}</td>
        <td>デフォルトはFalse</td>
      </tr>
      <tr>
        <td>アカウントが有効か(ログインできるか)</td>
        <td>{% verbatim %} {{ user.is_active }} {% endverbatim %} </td>
        <td>{{ user.is_active }}</td>
        <td>デフォルトはTrue</td>
      </tr>
    </tbody>
  </table>
  <a href="{% url 'easy_regist:user_update' user.pk %}" class="btn btn-outline-primary">ユーザ情報の変更</a>
  <a href="{% url 'easy_regist:change_password' %}" class="btn btn-outline-primary">パスワードの変更</a>

</div><!-- /.container -->
{% endblock %}


django-easy-regist/easy_regist/templates/easy_regist/user_update.html
ユーザ変更ページ
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ユーザ情報</h1>
    <p class="lead">ログイン中のユーザデータを表示します</p>
  </div>

  <form action="" method="POST">
  <table class="table">
    <thead class="thead-inverse">
      <tr>
        <th>項目</th>
        <th>入力欄</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>メールアドレス</td>
        <td>
          {{ form.username }}
          {{ form.username.errors }}
        </td>
      </tr>
      <tr>
        <td>姓</td>
        <td>
          {{ form.first_name }}
          {{ form.first_name.errors }}
        </td>
      </tr>
      <tr>
        <td>名</td>
        <td>
          {{ form.last_name }}
          {{ form.last_name.errors }}
        </td>
      </tr>
    </tbody>
  </table>
  {% csrf_token %}
  <button type="submit" class="btn btn-outline-primary">変更</button>
  <a href="{% url 'easy_regist:mypage' %}" class="btn btn-outline-primary">戻る</a>
  </form>
  
</div><!-- /.container -->
{% endblock %}


django-easy-regist/easy_regist/templates/easy_regist/change_password.html
マイページ内パスワード変更ページ
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ユーザ情報</h1>
    <p class="lead">ログイン中のユーザデータを表示します</p>
  </div>

  <form action="" method="POST">
  <table class="table">
    <thead class="thead-inverse">
      <tr>
        <th>項目</th>
        <th>入力欄</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>古いパスワード</td>
        <td>
          {{ form.old_password }}
          {{ form.old_password.errors }}
        </td>
      </tr>
      <tr>
        <td>パスワード</td>
        <td>
          {{ form.new_password1 }}
          {{ form.new_password1.errors }}
        </td>
      </tr>
      <tr>
        <td>パスワード(確認)</td>
        <td>
          {{ form.new_password2 }}
          {{ form.new_password2.errors }}
        </td>
      </tr>
    </tbody>
  </table>
  {% csrf_token %}
  <button type="submit" class="btn btn-outline-primary">変更</button>
  <a href="{% url 'easy_regist:mypage' %}" class="btn btn-outline-primary">戻る</a>
  </form>
  

</div><!-- /.container -->
{% endblock %}



django-easy-regist/easy_regist/templates/easy_regist/change_password_done.html
マイページパスワード変更後ページ
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ユーザ情報</h1>
    <p class="lead">パスワードを変更しました</p>
  </div>

</div><!-- /.container -->
{% endblock %}


django-easy-regist/easy_regist/templates/easy_regist/create.html
会員登録
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ユーザ登録</h1>
  </div>

  <form action="" method="POST">
	<div class="row">
	  <div class="col-sm-10 offset-sm-1">
        <div class="card">
          <div class="card-block">

  			<div class="row">
  			  <div class="col-xs-12">
  			    <div class="form-group">
  			  	  {{ form.username }}
  			  	  {{ form.username.errors }}
  			  	</div>
  			  </div>
  			</div>
  			<div class="row">
  			  <div class="col-xs-12 col-sm-6">
  			    <div class="form-group">
  			  	  {{ form.password1 }}
  			  	  {{ form.password1.errors }}
  			  	</div>
  			  </div>
   			  <div class="col-xs-12 col-sm-6">
   			    <div class="form-group">
  			  	  {{ form.password2 }}
  			  	  {{ form.password2.errors }}
  			  	</div>
  			  </div>
  			</div>
    		<div class="row">
  			  <div class="col-xs-12 col-sm-6">
  			    <div class="form-group">
  			  	  {{ form.first_name }}
  			  	</div>
  			  </div>
   			  <div class="col-xs-12 col-sm-6">
   			    <div class="form-group">
  			  	  {{ form.last_name }}
  			  	</div>
  			  </div>
  			</div>
	        {% csrf_token %}
	        <button type="submit" class="btn btn-lg btn-block btn-outline-primary">会員登録</button>
          </div>
        </div>
      </div>
    </div>
  </form>

</div>
{% endblock %}


django-easy-regist/easy_regist/templates/easy_regist/create_done.html
会員登録後
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ユーザ登録</h1>
    <p class="lead">仮登録しました。本登録はメールをみてね</p>
  </div>
</div><!-- /.container -->
{% endblock %}


django-easy-regist/easy_regist/templates/easy_regist/create_complete.html
メールのURLをクリックし、本登録完了ページ
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ユーザ登録</h1>
    <p class="lead">会員登録がおわりました</p>
  </div>
</div><!-- /.container -->
{% endblock %}


django-easy-regist/easy_regist/templates/easy_regist/password_reset_form.html
パスワードリセットページ
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>パスワードリセットページ</h1>
    <p class="lead">メールアドレスを入力してください。</p>
  </div>
  <form action="" method="POST">
  <div class="row">
    <div class="col-sm-8 offset-sm-2">
        <div class="card">
          <div class="card-block">
            <div class="form-group">
              {{ form.email }}
            </div>
          {% csrf_token %}
          <button type="submit" class="btn btn-lg btn-block btn-outline-primary">送信</button>
          </div>
        </div>
      </div>
    </div>
  </form>
</div><!-- /.container -->
{% endblock %}



django-easy-regist/easy_regist/templates/easy_regist/password_reset_done.html
パスワードリセット完了後
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>パスワードリセットページ</h1>
    <p class="lead">メールアドレスにパスワード初期化ページのURLを送付しました。</p>
  </div>
</div><!-- /.container -->
{% endblock %}


django-easy-regist/easy_regist/templates/easy_regist/password_reset_confirm.html
メールのURLにアクセス、新パスワード設定ページ
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>パスワードリセットページ</h1>
    <p class="lead">新パスワードを入力してください</p>
  </div>

  <form action="" method="POST">
  <div class="row">
    <div class="col-sm-8 offset-sm-2">
        <div class="card">
          <div class="card-block">
            <div class="form-group">
              {{ form.new_password1 }}
              {{ form.new_password1.errors }}
            </div>
            <div class="form-group">
              {{ form.new_password2 }}
              {{ form.new_password2.errors }}
            </div>
          {% csrf_token %}
          <button type="submit" class="btn btn-lg btn-block btn-outline-primary">送信</button>
          </div>
        </div>
      </div>
    </div>
  </form>
</div><!-- /.container -->
{% endblock %}



django-easy-regist/easy_regist/templates/easy_regist/password_reset_complete.html
新パスワード設定後
{% extends "easy_regist/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>パスワードリセットページ</h1>
    <p class="lead">パスワードをリセットしました。</p>
  </div>
</div><!-- /.container -->
{% endblock %}


今後も使いそうなので、Bitbucketにソースを置きました。
こちらのソースはたまに改良していく予定です。
https://bitbucket.org/toritoritorina/django-easy-regist