次はメールの本文に有効期限を表示してみましょう.
  まず,forms.py を編集します.具体的には class PasswordResetForm(forms.Form): と class UserCreationForm(forms.ModelForm):
users\forms.py
import unicodedata
from django import forms
from django.contrib.auth import authenticate, get_user_model, password_validation
from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher
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.exceptions import ValidationError
from django.core.mail import EmailMultiAlternatives
from django.template import loader
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.text import capfirst
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from django.conf import settings
UserModel = get_user_model()
class PasswordResetForm(forms.Form):
    email = forms.EmailField(
        label=_("Email"),
        max_length=254,
        widget=forms.EmailInput(attrs={"autocomplete": "email", 'class': 'form-control'}),
    )
    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 get_users(self, email):
        """Given an email, return matching user(s) who should receive a reset.
        This allows subclasses to more easily customize the default policies
        that prevent inactive users and users with unusable passwords from
        resetting their password.
        """
        email_field_name = UserModel.get_email_field_name()
        active_users = UserModel._default_manager.filter(
            **{
                "%s__iexact" % email_field_name: email,
                "is_active": True,
            }
        )
        return (
            u
            for u in active_users
            if u.has_usable_password()
            and _unicode_ci_compare(email, getattr(u, email_field_name))
        )
    def save(
        self,
        domain_override=None,
        # subject_template_name="registration/password_reset_subject.txt",
        # email_template_name="registration/password_reset_email.html",
        subject_template_name="users/password_reset_subject.txt",
        email_template_name="users/password_reset_email.html",
        use_https=False,
        token_generator=default_token_generator,
        from_email=None,
        request=None,
        html_email_template_name=None,
        extra_email_context=None,
    ):
        """
        Generate a one-use only link for resetting password and send it to the
        user.
        """
        email = self.cleaned_data["email"]
        if not domain_override:
            current_site = get_current_site(request)
            site_name = current_site.name
            domain = current_site.domain
        else:
            site_name = domain = domain_override
        email_field_name = UserModel.get_email_field_name()
        for user in self.get_users(email):
            user_email = getattr(user, email_field_name)
            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 {}),
                "password_reset_timeout": settings.PASSWORD_RESET_TIMEOUT // 60,
            }
            self.send_mail(
                subject_template_name,
                email_template_name,
                context,
                from_email,
                user_email,
                html_email_template_name=html_email_template_name,
            )
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 {}),
            "password_reset_timeout": settings.PASSWORD_RESET_TIMEOUT // 60,
        }
        # 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
def creation_resend(request):
    if request.method == 'POST':
        form = UserCreationResendForm(request.POST)
        if form.is_valid():
            user_email = form.cleaned_data.get("email")
            current_site = get_current_site(request)
            site_name = current_site.name
            domain = current_site.domain
            user = UserModel._default_manager.get(email=user_email)
            token_generator = default_token_generator
            use_https = False
            extra_email_context = None
            subject_template_name = 'users/user_creation_subject.txt'
            email_template_name = 'users/user_creation_email.html'
            from_email = settings.EMAIL_FROM
            html_email_template_name = None
            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 {}),
                "password_reset_timeout": settings.PASSWORD_RESET_TIMEOUT // 60,
            }
            form.send_mail(
                subject_template_name,
                email_template_name,
                context,
                from_email,
                user_email,
                html_email_template_name=html_email_template_name,
            )
            messages.success(request, f'メール送信しました {user_email}')
            messages.success(request, f'username {user.username}')
            return redirect(reverse('index'))
        else:
            # エラーメッセージをつけて返す
            context = {}
            context['form'] = form
            messages.success(request, 'エラーです.メールアドレスを確認してください')
            return render(request, 'users/create_resend_form.html', context)
    else:
        context = {}
        context['form'] = UserCreationResendForm(
                            initial={}
                        )
        return render(request, 'users/create_resend_form.html', context)
パスワードリセットページに有効期限切れのメッセージを追加します.
users/templates/users/password_reset_confirm.html
{% extends "base.html" %}
{% block title %}
パスワードリセット
{% endblock %}
{% block content %}
<h1 class="my-5">コメントアプリケーション</h1>
{% if validlink %}
<div class="card">
  <div class="card-header">パスワードリセット</div>
  <div class="card-body">
      <form method="post">
          {% csrf_token %}
          {{ form.new_password1.errors }}
          {{ form.new_password2.errors }}
          {{ form.as_p }}
          <button type="submit" class="btn btn-primary btn-block">
            パスワードをリセットする
          </button>
      </form>
  </div>
  <div class="card-footer">
      <p>
      </p>
  </div>
</div>
{% else %}
<p>
  不正なパスワードリセットリンクです.このリンクは有効期限が切れているかすでに利用されている可能性があります.もう一度パスワードリセットをリクエストしてください.
</p>
{% endif %}
<ul>
    <li>
        <a href="{% url 'index' %}">トップへ</a>
    </li>
</ul>
{% endblock content %}
パスワードリセットのメールテンプレートを編集します.
users/templates/users/password_reset_email.html
{% autoescape off %}
パスワードリセットが {{ site_name }} で要求されました.
次のリンク先で新しいパスワードを設定してください.
{{ protocol }}://{{ domain }}{% url 'users:password_reset_confirm' uidb64=uid token=token %}
なお,上のリンクの有効期間は送信時から {{ password_reset_timeout }} 分間です.
{% endautoescape %}
ユーザ登録後のメールテンプレートを編集します.
users/templates/users/user_creation_email.html
{% autoescape off %}
このメールは {{ site_name }} で,ユーザ登録が要求されたため送信されました.
次のリンク先のページで登録を完了してください.
{{ protocol }}://{{ domain }}{% url 'users:create_confirm' uidb64=uid token=token %}
なお,上のリンクの有効期間は送信時から {{ password_reset_timeout }} 分間です.
{% endautoescape %}
パスワードリセットメールに有効期限が表示されました.
ユーザ登録メールにも有効期限が表示されました.