👋

dj-rest-authでパスワードリセットメールをカスタマイズする(続き)

2023/04/12に公開

はじめに

この記事の続きです。パスワードリセット時のメールの文面をカスタマイズするためにSerialier をいじったりして、すったもんだしました。これで後はテンプレートはいじれば終わりだ・・・、と思ったらまたハマりました。

やろうとしたこと

site-package/django/contrib/admin/templates/registrationにあるpassword_reset_email.html
をコピーして、自分が設定したい変数を埋め込めんだテンプレートにした。

{% with uidb64 as uid_str %}
  {% with token as token_str %}
    {% url 'password_reset_confirm' uidb64=uid_str token=token_str as password_reset_confirm_url %}
  {% endwith %}
{% endwith %}

{% load i18n %}{% autoescape off %}
{% blocktranslate %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktranslate %}

{% translate "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{frontend_origin}}{{frontend_auth}}{{ uid }}/{{ token }}/
{% endblock %}
{% translate 'Your username, in case you’ve forgotten:' %} {{ user.get_username }}

{% translate "Thanks for using our site!" %}

{% blocktranslate %}The {{ site_name }} team{% endblocktranslate %}

{% endautoescape %}

実際にはこんな感じでfrontend_originやらrontend_authという変数を使いたかった。が、どーやってもこの変数が反映されない。context_processors.py を定義し、そこで context を設定してもなぜか値が入らない。テンプレートに変数を渡すので setitings.py からは持ってこれないし、なんでだー、とうんうん言ってました。

原因:メールのテンプレートは context_processors を通過しない

こちらの Q&Aによると、どうやらメールのテンプレートは context_processor を利用しないらしい。ゆえに、context_processors.py とかで設定しても無視されてしまうようである。

解決策:カスタムフォームを作成する

PasswordResetFormsend_mailメソッドをオーバーライドし、メール本文を作成する前に context を追加しておく。ここではメール本文に含めたい変数は settings.py から持ってきている。

from django.contrib.auth.forms import PasswordResetForm
from django.template import loader
from django.core.mail import EmailMultiAlternatives
from django.conf import settings

class CustomPasswordResetForm(PasswordResetForm):

    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])

        context["site_name"] = getattr(settings, "EMAIL_SITE_NAME", None)
        context["frontend_origin"] = getattr(settings, "FRONTEND_ELB_HOST", None)
        context["frontend_auth"] = getattr(settings, "FRONTEND_AUTH_URL", None)

        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()

また、前回作成した CustomPasswordResetSerializer を編集して、この Serializer が CustomPasswordResetForm を見に行くようにしておく。これで解決。

from .forms import CustomPasswordResetForm


class CustomPasswordResetSerializer(PasswordResetSerializer):
    @property
    def password_reset_form_class(self):
        use_custom_email_template = bool(
            self.get_email_options().get("html_email_template_name", "")
        )
        if "allauth" in settings.INSTALLED_APPS and not use_custom_email_template:
            return AllAuthPasswordResetForm
        else:
            return CustomPasswordResetForm

    def get_email_options(self, **kwargs):
        return {
            "html_email_template_name": "custom_password_reset_email.html",
            "from_email": os.environ.get("EMAIL_HOST_USER"),
        }

おまけ

認証用のリンクの URL を変えるだけなら AccountAdapter をいじってもできる。

from allauth.account.adapter import DefaultAccountAdapter
from django.conf import settings

class CustomAccountAdapter(DefaultAccountAdapter):

    def get_email_confirmation_url(self, request, emailconfirmation):

        """
            Changing the confirmation URL to fit the domain that we are working on
        """
        backend_origin = getattr(settings, "BACKEND_ELB_HOST", None)
        url = (
            f"{request.scheme}://{backend_origin}/accounts/confirm-email/"
            + emailconfirmation.key
        )
        return url

settings.pyACCOUNT_ADAPTER = "backend.views.CustomAccountAdapter"を追加しておくこと。

Discussion