はじめに
Djangoを利用しているアプリがあり外部に公開していることもあって、定期的にライブラリをバージョンアップしてリグレッションテストを行っています。
Dockerコンテナとして運用しているのですが、Djangoに限らずORMの機構は初期化時や節目で手動での作業を必要とすることがあるため、コンテナ環境との相性は必ずしも良くない部分があるなと感じています。
さてdjango_registratrion@pypIのバージョンがいきなり、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には次のような記述をしています。
path('accounts/', include('django_registration.backends.activation.urls')),
path('accounts/', include('django.contrib.auth.urls')),
エラーメッセージ
単純にdjango_registrationのバージョンを上げると、DeploymentモードのWebブラウザには次のようなエラーメッセージが表示されました。
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行目前後は次のようになっています。
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
で指定しているユーザーは次のようになっています。
class AuthUser(AbstractBaseUser, PermissionsMixin):
...
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['family_name','first_name', 'gender', 'phone']
...
Django 4.2.16の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 --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 --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行の修正と、テンプレートファイルの追加といった簡単な作業なのですが、理解するまで少し時間を取られました。
以上