django_registrationを3.4から5.1.0にアップグレードしたらエラーになった

# はじめに

Djangoを利用しているアプリがあり外部に公開していることもあって、定期的にライブラリをバージョンアップしてリグレッションテストを行っています。

Dockerコンテナとして運用しているのですが、Djangoに限らずORMの機構は初期化時や節目で手動での作業を必要とすることがあるため、コンテナ環境との相性は必ずしも良くない部分があるなと感じています。

さて[django_registratrion@pypI](https://pypi.org/project/django-registration/)のバージョンがいきなり、3.4から5.1.0に数字が大きく変化したので何か変更があったのかなと思ったのですが、適用してみると``type object 'AuthUser' has no attribute 'EMAIL_FIELD'``というエラーが表示されるようになりました。

自前の``AUTH_USER_MODEL``が原因で引き起したエラーではあったのですが、顛末をまとめておきます。

そして2段階有効化フロー(two-step activcation workflow)がGETで完結していたものがさらにPOSTで確定する方法へ変更になったため、formタグを含むファイルを1つ追加する必要もありました。

# 環境

* Ubuntu 24.04.1 (amd64版)
* Python 3.12.3
* django 4.2.16
* django_registration 5.1.0

メールによる確認プロセスを利用しているので、myapp/urls.pyには次のような記述をしています。

```python:
    path('accounts/', include('django_registration.backends.activation.urls')),
    path('accounts/', include('django.contrib.auth.urls')),
```

# エラーメッセージ

単純にdjango_registrationのバージョンを上げると、DeploymentモードのWebブラウザには次のようなエラーメッセージが表示されました。

```text:
AttributeError at /webapp/accounts/register/

type object 'AuthUser' has no attribute 'EMAIL_FIELD'

Request Method: 	GET
Request URL: 	http://127.0.0.1:8000/webapp/accounts/register/
Django Version: 	4.2.16
Exception Type: 	AttributeError
Exception Value: 	

type object 'AuthUser' has no attribute 'EMAIL_FIELD'
Exception Location: 	.../venv/myapp/lib/python3.12/site-packages/django_registration/forms.py, line 259, in __init__
Raised during: 	django_registration.backends.activation.views.RegistrationView
```

エラーを出している``django_registration/forms.py``の259行目前後は次のようになっています。

```python:259行前後
from django.contrib.auth import get_user_model
...
class RegistrationForm(SetPasswordMixin, BaseRegistrationForm):
    ...
    class Meta(BaseRegistrationForm.Meta):
        fields = (
            UserModel.REQUIRED_FIELDS
            if UserModel.USERNAME_FIELD in UserModel.REQUIRED_FIELDS
            else [UserModel.USERNAME_FIELD] + UserModel.REQUIRED_FIELDS
        )
        field_classes = {UserModel.USERNAME_FIELD: auth_forms.UsernameField}

    def __init__(self, *args, **kwargs):
        ...
        self.fields[UserModel.EMAIL_FIELD].validators = [  ## ← 259行目
            validators.HTML5EmailValidator(),
            validators.validate_confusables_email,
        ]
        self.fields[UserModel.EMAIL_FIELD].required = True
        ...
```

``myapp/settings.py``の中で指定している``AUTH_USER_MODEL``で指定しているユーザーは次のようになっています。

```python:自前のUserModelの定義
class AuthUser(AbstractBaseUser, PermissionsMixin):
    ...
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['family_name','first_name', 'gender', 'phone']
    ...
```

Django 4.2.16の``django/contrib/auth/models.py``を調べると次のようになっています。

```python:django/contrib/auth/models.pyからの抜粋
class AbstractUser(AbstractBaseUser, PermissionsMixin):
    ...
    EMAIL_FIELD = "email"
    USERNAME_FIELD = "username"
    REQUIRED_FIELDS = ["email"]
    ...
```

自前のクラスとシステムのデフォルト設定との間の差分がエラーを引き起したことが分かりました。

この差分を適用して、この部分については解決しました。

# activation_form.htmlの追加

これまでは2段階認証の場合にはメールアドレスに記載されたURLをクリックするだけで完了しましたが、5.1.0からPOSTで情報を送信するためのページを追加する必要があります。

```diff:変更箇所
diff --git a/templates/django_registration/activation_email_body.txt b/templates/django_registration/activation_email_body.txt
index cc2eeca..4ae162c 100644
--- a/templates/django_registration/activation_email_body.txt
+++ b/templates/django_registration/activation_email_body.txt
@@ -1,6 +1,6 @@
 {% load i18n %}
 {% trans "Activate account at" %} {{ settings.DEFAULT_SMTP_FROM_HOST }}:
 
-https://{{ settings.DEFAULT_SMTP_FROM_HOST }}{% url 'django_registration_activate' activation_key %}
+https://{{ settings.DEFAULT_SMTP_FROM_HOST }}{% url 'django_registration_activate' %}?activation_key={{ activation_key }}
 
 {% blocktrans %}Link is valid for {{ expiration_days }} days.{% endblocktrans %}
diff --git a/templates/django_registration/activation_form.html b/templates/django_registration/activation_form.html
new file mode 100644
index 0000000..be877a4
--- /dev/null
+++ b/templates/django_registration/activation_form.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+<h1>Activation Forms</h1>
+<p>
+  <form action="." method="post">
+    {% csrf_token %}
+    <input type="hidden" name="activation_key" value="{{ form.activation_key.value }}">
+    <input class="my-3 btn btn-primary form-group" type="submit" value="Activate" />
+  </form>
+</p>
+{% endblock %}
```

urls.pyなどは変更しなくても``django_registration/activation_form.html``がデフォルトのテンプレート名なので、ファイルを配置するだけで自動的に表示されます。

Quick Start Guideの中では新しく追加されたことが、それほど強調もされずにさらっと書かれていました。

ドキュメントでは``{{ form }}``を表示すれば良いように読めたのですが、textフィールドに表示させてしまうのは見た目がよくないので、hiddenの中で値だけを利用しています。

# さいごに

自前のAuthUserクラスをAbstractUserから派生させなかったのが敗因ではあるのですが、それはしょうがないので、自前のクラスに``EMAIL_FIELD = 'email'``を追加して解決しました。

差分は次のようになっています。

```diff:最終的に解決したコードの差分
diff --git a/myapp/authenticate/models.py b/myapp/authenticate/models.py
index 2af7ffa..e291e69 100644
--- a/myapp/authenticate/models.py
+++ b/myapp/authenticate/models.py
@@ -61,6 +61,7 @@ class AuthUser(AbstractBaseUser, PermissionsMixin):
     is_staff = models.BooleanField(default=False)
     is_admin = models.BooleanField(default=False)
                        
+    EMAIL_FIELD = 'email'
     USERNAME_FIELD = 'email'
     REQUIRED_FIELDS = ['family_name','first_name', 'gender', 'phone']
```

解決してみれば1行の修正と、テンプレートファイルの追加といった簡単な作業なのですが、理解するまで少し時間を取られました。

以上