次はユーザ登録後に検証用のメールを送信する機能を実装します.ただし,メールの検証を行ってアカウントを有効化する処理は次のページで行います.
まず,送信したメールのリンクから開かれるページの URL を定義します.
users/urls.py
from django.urls import path
from . import views
app_name = 'users'
urlpatterns = [
path('', views.users_index, name='index'),
path('<int:user_id>/', views.users_profile, name='profile'),
path('<int:user_id>/update/', views.users_update, name='update'),
path('login/', views.LoginView.as_view(), name='login'),
path('logout/', views.LogoutView.as_view(), name='logout'),
path('password/', views.PasswordChangeView.as_view(), name='password_change_form'),
path('password_change_done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
# ユーザ登録
path('create/', views.UserCreateView.as_view(), name='create'),
# メールリンクから呼び出される
path('create/<uidb64>/<token>/', views.creation_confirm, name='create_confirm'),
# パスワードリセットのメール送信画面
path('password_reset/', views.PasswordResetView.as_view(), name='password_reset'),
# メール送信後の画面
path('password_reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
# メールリンクから呼び出される
path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]
ユーザ登録時にメールを送信するように forms.py を修正します.
/users/forms.py
class UserCreationForm(forms.ModelForm):
"""
A form that creates a user, with no privileges, from the given username and
password.
"""
error_messages = {
"password_mismatch": _("The two password fields didn’t match."),
}
password1 = forms.CharField(
label=_("Password"),
strip=False,
widget=forms.PasswordInput(attrs={"autocomplete": "new-password", 'class': 'form-control'}),
help_text=password_validation.password_validators_help_text_html(),
)
password2 = forms.CharField(
label=_("Password confirmation"),
widget=forms.PasswordInput(attrs={"autocomplete": "new-password", 'class': 'form-control'}),
strip=False,
help_text=_("Enter the same password as before, for verification."),
)
class Meta:
model = UserModel
fields = ("username", 'first_name', 'last_name', 'email')
field_classes = {"username": UsernameField}
widgets = {
'first_name': forms.TextInput(attrs={
'class': 'form-control',
'required' : True
}),
'last_name': forms.TextInput(attrs={
'class': 'form-control',
'required' : True
}),
'email': forms.EmailInput(attrs={
"autocomplete": "email",
'class': 'form-control',
'required' : True
}),
}
labels = {
'first_name': 'First Name',
'last_name': 'Last Name',
'email': 'Email',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self._meta.model.USERNAME_FIELD in self.fields:
self.fields[self._meta.model.USERNAME_FIELD].widget.attrs[
"autofocus"
] = True
def clean(self):
data = super().clean()
first_name = data.get('first_name')
last_name = data.get('last_name')
if len(first_name) == 0:
msg = "First Name が入力されていません"
self.add_error('first_name', msg)
elif len(first_name) > 20:
msg = "First Name の最大文字数は20文字です"
self.add_error('first_name', msg)
if len(last_name) == 0:
msg = "Last Name が入力されていません"
self.add_error('last_name', msg)
elif len(last_name) > 20:
msg = "Last Name の最大文字数は20文字です"
self.add_error('last_name', msg)
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise ValidationError(
self.error_messages["password_mismatch"],
code="password_mismatch",
)
return password2
def _post_clean(self):
super()._post_clean()
# Validate the password after self.instance is updated with form data
# by super().
password = self.cleaned_data.get("password2")
if password:
try:
password_validation.validate_password(password, self.instance)
except ValidationError as error:
self.add_error("password2", error)
def send_mail(
self,
subject_template_name,
email_template_name,
context,
from_email,
to_email,
html_email_template_name=None,
):
"""
Send a django.core.mail.EmailMultiAlternatives to `to_email`.
"""
subject = loader.render_to_string(subject_template_name, context)
# Email subject *must not* contain newlines
subject = "".join(subject.splitlines())
body = loader.render_to_string(email_template_name, context)
email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
if html_email_template_name is not None:
html_email = loader.render_to_string(html_email_template_name, context)
email_message.attach_alternative(html_email, "text/html")
email_message.send()
def save(
self,
# domain_override=None,
subject_template_name="users/user_creation_subject.txt",
email_template_name="users/user_creation_email.html",
use_https=False,
token_generator=default_token_generator,
from_email=None,
request=None,
html_email_template_name=None,
extra_email_context=None,
):
user = super().save(commit=False)
user.is_active = False # まだログインできないようにする
# user.is_active = True # すぐにログインできるようにする
user.set_password(self.cleaned_data["password1"])
user.save()
"""
Generate a one-use only link for user creation and send it to the
user.
"""
current_site = get_current_site(request)
site_name = current_site.name
domain = current_site.domain
user_email = user.email
context = {
"email": user_email,
"domain": domain,
"site_name": site_name,
"uid": urlsafe_base64_encode(force_bytes(user.pk)),
"user": user,
"token": token_generator.make_token(user),
"protocol": "https" if use_https else "http",
**(extra_email_context or {}),
}
# print("email : ", context["email"])
# print("domain : ", context["domain"])
# print("site_name : ", context["site_name"])
# print("uid : ", context["uid"])
# print("user : ", context["user"])
# print("token : ", context["token"])
# print("protocol : ", context["protocol"])
# メールを送信する
self.send_mail(
subject_template_name,
email_template_name,
context,
from_email,
user_email,
html_email_template_name=html_email_template_name,
)
return user
また,views.py も編集します.
users/views.py
class UserCreateView(CreateView):
template_name = 'users/create_form.html'
form_class = UserCreationForm
from_email = settings.EMAIL_FROM
email_template_name = 'users/user_creation_email.html'
subject_template_name = 'users/user_creation_subject.txt'
html_email_template_name = None
extra_email_context = None
token_generator = default_token_generator
def form_valid(self, form):
opts = {
'use_https': self.request.is_secure(),
'token_generator': self.token_generator,
'from_email': self.from_email,
'email_template_name': self.email_template_name,
'subject_template_name': self.subject_template_name,
'request': self.request,
'html_email_template_name': self.html_email_template_name,
'extra_email_context': self.extra_email_context,
}
user = form.save(**opts)
messages.success(self.request, f'ユーザ登録しましたが,まだログインはできません {user.email}')
return HttpResponseRedirect("/")
def creation_confirm(request, uidb64, token):
messages.success(request, f'メールを確認しました uidb64 : {uidb64}, :token : {token}')
return HttpResponseRedirect("/")
メールの件名と本文のテンプレートを作成します.
users/templates/users/user_creation_subject.txt
{% load i18n %}{% autoescape off %}
【{{ site_name }}】ユーザ登録
{% endautoescape %}
users/templates/users/user_creation_email.html
{% autoescape off %}
このメールは {{ site_name }} で,ユーザ登録が要求されたため送信されました.
次のリンク先のページで登録を完了してください.
{{ protocol }}://{{ domain }}{% url 'users:create_confirm' uidb64=uid token=token %}
{% endautoescape %}
受信できるメールアドレスを使ってユーザ登録を行います.受信したメールのリンクを開きます.
メールのリンクから検証ページを開くことができました.メールのリンクに含まれる uidb64
と token
を取得できていることにも注意してください.実際の検証は次のページで行います.