🎉

Django 4.0をプライベートサイトに入れてみた

2021/12/08に公開

はじめに

これは Django Advent Calendar 2021 10日目の記事です(空いていたので飛び入りで)。

2021/12/07にDjango 4.0がリリースされました🎉

https://www.djangoproject.com/weblog/2021/dec/07/django-40-released/
https://docs.djangoproject.com/en/4.0/releases/4.0/

早速プライベートで使っているサイトに入れてみました。以前記事にしたものです。

https://zenn.dev/ikemo/articles/test-coverage-100-percent

このプライベートサイトは次の環境で動いています。なお、 Django 4.0 を動かすためには Python 3.8 以上が必要です。

  • デプロイ先: Heroku
  • Python: 3.9
  • Django: 3.2.9

やったこと

Django 4.0を動かすためにやったことを記載していきます。

pytest の RemovedInDjango40Warning のフィルタを削除

pytestで RemovedInDjango40Warning をフィルタする設定が残っていたので削除しました。

[pytest]
filterwarnings = ignore::django.utils.deprecation.RemovedInDjango40Warning

ライブラリも最新にしていたため、特に問題ありませんでした。

DeleteView が FormMixin を使うようになった

リリースノートには次のように書かれています。
例えば、削除確認画面にチェックボックスを追加するために使えそうです。

DeleteView now uses FormMixin, allowing you to provide a Form subclass, with a checkbox for example, to confirm deletion. In addition, this allows DeleteView to function with django.contrib.messages.views.SuccessMessageMixin.
In accordance with FormMixin, object deletion for POST requests is handled in form_valid(). Custom delete logic in delete() handlers should be moved to form_valid(), or a shared helper method, as needed.

Custom delete logic in delete() handlers should be moved to form_valid() と書かれているように、 delete() に書かれているロジックを form_valid() に移す必要があります。プライベートサイトの場合、次のように書き換えました。 delete(self, request, *args, **kwargs)form_valid(self, form) にするだけです。

def delete(self, request, *args, **kwargs):
    def collect_model_names(protected_objects):
        names = set()
        for protected_object in protected_objects:  # type: Model
            names.add(protected_object._meta.verbose_name)

        return names

    try:
        response = super().delete(request, *args, **kwargs)
        messages.success(self.request, f"{self.get_object_name()}を削除しました。")
        return response
    except ProtectedError as e:
        protected_models = "、".join(collect_model_names(e.protected_objects))
        message = f"削除できませんでした。『{protected_models}』にこの{self.get_object_name()}に依存するレコードがあります。"
        messages.error(self.request, message)
        return self.render_to_response(self.get_context_data(object=self.object))
def form_valid(self, form):
    def collect_model_names(protected_objects):
        names = set()
        for protected_object in protected_objects:  # type: Model
            names.add(protected_object._meta.verbose_name)

        return names

    try:
        response = super().form_valid(form)
        messages.success(self.request, f"{self.get_object_name()}を削除しました。")
        return response
    except ProtectedError as e:
        protected_models = "、".join(collect_model_names(e.protected_objects))
        message = f"削除できませんでした。『{protected_models}』にこの{self.get_object_name()}に依存するレコードがあります。"
        messages.error(self.request, message)
        return self.render_to_response(self.get_context_data(object=self.object))

Formset の ManagementForm がおかしいときのメッセージが翻訳されている

コードではなく翻訳の改善かもしれませんが、 ManagementForm がおかしいときのメッセージが翻訳されていました。以前は次のように検証をしていたのが通らなくなったので、

errors = res.html.select(".formset-non-form-errors .error")
assert len(errors) == 1
assert "ManagementForm data is missing" in errors[0].get_text()

次のように書き換えました。

errors = res.html.select(".formset-non-form-errors .error")
assert len(errors) == 1
assert "ManagementForm のデータが不足しているか改竄されています。" in errors[0].get_text()

USER_L10N のデフォルトが True に

実行すると次のような警告が出ました。

RemovedInDjango50Warning: The USE_L10N setting is deprecated. Starting with Django 5.0, localized formatting of data will always be enabled. For example Django will display numbers and dates using the format of the current locale.

リリースノートではこちらです。

The default value of the USE_L10N setting is changed to True. See the Localization section above for more details.

これは単に USE_L10N = True を削除しました。

CheckboxSelectMultiple が RadioSelect を継承するように

CheckboxSelectMultiple を使っていた画面でテストが落ち、表示が乱れる問題が発生しました。

よくよく調べたところ、 RadioSelect のテンプレートをカスタマイズしていたところが、間違って CheckboxSelectMultiple にも適用されていたのが原因でした。次のコードが原因です。

if isinstance(widget, RadioSelect):
    widget.template_name = "widgets/radio.html"

Django 3.2 までは CheckboxSelectMultipleRadioSelect はともに ChoiceWidget を継承していたのですが、Django 4.0では CheckboxSelectMultipleRadioSelect を継承し、 RadioSelectChoiceWidget を継承するようになっていました。

リリースノートはこちらで、

RadioSelect and CheckboxSelectMultiple widgets are now rendered in <div> tags so they are announced more concisely by screen readers. If you need the previous behavior, override the widget template with the appropriate template from Django 3.2.

チケットはこちらのようです。

https://code.djangoproject.com/ticket/32338

あまりいいとは言えませんが、 CheckboxSelectMultiple については書き換えないように修正しました。

if isinstance(widget, RadioSelect) and not isinstance(widget, CheckboxSelectMultiple):
    widget.template_name = "widgets/radio.html"

おわりに

プライベートサイトとは言え、1時間でサクッとアップデートできたのはよかったです。
そのためには、テストを書いて、ライブラリやPythonをアップデートしておくことが大事だと改めて思いました。

Discussion