Qiita Django Advent Calendar 2021 2日目は、2021年12月リリース予定のDjango 4.0で追加された新機能について解説します。
【注意】
なお、2021年12月2日時点では、Django 4.0はまだ正式にリリースされてません。この記事はrelease candidate 1版を元に執筆しました。
祝・Django 4.0リリース!
公式サイトでのリリース情報は以下を参照してください。
Django 4.0 release notes - UNDER DEVELOPMENT | Django documentation | Django
4.0のサポート期限は2023年8月までです。
一方で、一つ前のバージョン3.2 LTS(long-term support)のサポート期限は2024年4月です。
現在3.2 LTSを使っている場合は、4.0にバージョンアップすることでサポート期限が短くなることに注意してください。
各バージョンがLTSなのか否か、サポート期限がいつまでなのかについては以下公式ドキュメント「Supported Versions」を参照してください。
Download Django | Django
zoneinfo default timezone implementation# タイムゾーンの実装にデフォルトでzoneinfo を使うようになりました。
zoneinfoはPython 3.9から追加された標準モジュールです。
Django 3.2まではサードパーティライブラリのpytz がデフォルトでしたが、4.0からは非推奨になり、5.0で廃止になる予定です。
Django 3.2ではデフォルトではないものの、zoneinfoを使うこともできます。Django 4.0にバージョンアップする前にzoneinfoを導入することは可能です。
以下の条件すべてに当てはまるアプリケーションは、pytzからzoneinfoに移行することで挙動が変わる可能性があります。タイムゾーンの計算に正確さが求められる場合は、十分検証してから移行しましょう。
TIME_ZONE にUTC以外のタイムゾーンを指定しているpytzのnormalize()
かlocalize()
を使っている pytzを当面使い続ける必要がある場合は、USE_DEPRECATED_PYTZ にTrue
を設定すると、デフォルトでpytzを使うようにできます。
また、zoneinfoはpytzとはインターフェイスが異なるため、移行にあたってコードの書き換え箇所が少し多めになってしまいます。
pytz_deprecation_shim はzoneinfoに近いインターフェイスでpytzを使えるようにするライブラリです。
今すぐzoneinfoに移行できない場合でも、pytzを使っている箇所をpytz_deprecation_shimを使うように書き換えておくと、後々zoneinfoに移行する際に変更量を減らすことができます。
Functional unique constraints# UniqueConstraint() の引数に式やデータベース関数を使えるようになりました。
リリースノートに載っているサンプルコード を参考に、実際にアプリケーションを作ってみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"""my_models/models.py"""
from django.db import models
from django.db.models import UniqueConstraint
from django.db.models.functions import Lower
class MyModel (models. Model):
first_name = models. CharField(max_length= 255 )
last_name = models. CharField(max_length= 255 )
class Meta :
constraints = [
UniqueConstraint(
Lower('first_name' ),
Lower('last_name' ). desc(),
name= 'first_last_name_unique' ,
),
]
実際にどんなSQLが発行されるのか見てみましょう(データベースはPostgreSQL 14.1を使用)。
CREATE UNIQUE INDEX
でLOWER
関数が使われていることがわかります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ./manage.py makemigrations
Migrations for 'my_models' :
my_models/migrations/0001_initial.py
- Create model MyModel
- Create constraint first_last_name_unique on model mymodel
$ ./manage.py sqlmigrate my_models 0001
BEGIN;
--
-- Create model MyModel
--
CREATE TABLE "my_models_mymodel" ( "id" bigserial NOT NULL PRIMARY KEY, "first_name" varchar( 255) NOT NULL, "last_name" varchar( 255) NOT NULL) ;
--
-- Create constraint first_last_name_unique on model mymodel
--
CREATE UNIQUE INDEX "first_last_name_unique" ON "my_models_mymodel" (( LOWER( "first_name" )) , ( LOWER( "last_name" )) DESC) ;
COMMIT;
shell
コマンド上でデータを登録してみます。大文字・小文字を区別せずにユニーク制約が効いています。
1
2
3
4
5
6
7
8
>>> from my_models.models import MyModel
>>> MyModel. objects. create(first_name= "Taro" , last_name= "Yamada" )
< MyModel: MyModel object (1 )>
>>> MyModel. objects. create(first_name= "taro" , last_name= "yamada" ) # 先頭を小文字にして再登録
Traceback (most recent call last):
( 省略)
django. db. utils. IntegrityError: duplicate key value violates unique constraint "first_last_name_unique"
DETAIL: Key (lower(first_name::text), lower(last_name::text))= (taro, yamada) already exists.
scrypt password hasher# 新しいscrypt password hasher(データベースにパスワードを保存する際に使われるアルゴリズム)としてdjango.contrib.auth.hashers.ScryptPasswordHasher
が追加されました。
これは、RFC 2898 で定義されている鍵導出関数PBKDF2よりセキュアであるとして推奨されています。
ただし、OpenSSL 1.1以上が必要なため、デフォルトでは有効になっていません。
また、メモリ使用量も他のscrypt password hasherより増大します。
導入する際は、OpenSSL 1.1以上をインストールし、十分なメモリを確保した上、PASSWORD_HASHERS の先頭にdjango.contrib.auth.hashers.PBKDF2PasswordHasherdjango.contrib.auth.hashers.ScryptPasswordHasher
を追加(つまり、優先順位を一番上にする)してください。
既存のデータベースに保存されたハッシュ値を更新する作業は必要はありません(というかできません)。上記の変更のみでOKです。
Djangoには、ログイン成功時に以前のログイン時より優先順位が高いscrypt password hasherが存在すれば、それを使ってパスワードを再度ハッシュ化し、データベース上の値を更新する仕組みがあります。
Redis cache backend# 新しいRedisキャッシュバックエンドdjango.core.cache.backends.redis.RedisCache
が追加されました。Django 4.0未満では、サードパーティのdjango-redis を使うのが定番でした。
django.core.cache.backends.redis.RedisCache
の具体的な使い方については以下公式ドキュメントを参照してください。
Django’s cache framework | Django documentation | Django
Form クラス、Formsets 、ErrorList クラスにtemplate_name
属性を指定することで、テンプレートファイルを使ってフォームのレンダリング内容をカスタマイズできるようになりました。
例として、お問い合わせフォームを模したアプリケーションを作ってみます。
contact/views.py
1
2
3
4
5
6
7
8
9
10
11
"""contact/views.py"""
from django.urls import reverse_lazy
from django.views.generic.edit import FormView
from .forms import ContactForm
class IndexView (FormView):
template_name = "contact/index.html"
form_class = ContactForm
success_url = reverse_lazy("contact:index" )
contact/urls.py
1
2
3
4
5
6
7
8
9
10
"""contact/urls.py"""
from django.urls import path
from . import views
app_name = "contact"
urlpatterns = [
path("" , views. IndexView. as_view(), name= "index" ),
]
contact/forms.py
1
2
3
4
5
6
7
8
9
10
11
12
"""contact/forms.py"""
from django import forms
class ContactForm (forms. Form):
name = forms. CharField(max_length= 50 , label= "お名前" )
email = forms. EmailField(
max_length= 100 ,
label= "メールアドレス" ,
help_text= "お問い合わせの返信のために使います" ,
)
content = forms. CharField(widget= forms. Textarea(), max_length= 1000 , label= "内容" )
contact/templates/contact/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang = "ja" >
<head >
<!-- Required meta tags -->
<meta charset = "utf-8" >
<meta name = "viewport" content = "width=device-width, initial-scale=1" >
<title >お問い合わせ</title >
<link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css" >
</head >
<body >
<section class = "section" >
<div class = "container" >
<form action = "{% url 'contact:index' %}" method = "POST" >
{% csrf_token %}
{{ form }}
<div class = "field is-grouped" >
<div class = "control" >
<button class = "button is-link" >送信</button >
</div >
</div >
</form >
</div >
</section >
</body >
</html >
このとき、http://127.0.0.1:8000/contact/
は以下のような画面になります。
{{ form }}
はtableタグを使う前提のHTMLが出力されますが、contact/templates/contact/index.html
はtableタグを使っていないので表示が変な感じです。
デフォルトでのフォームの表示内容
これをBulmaのForm controls に合わせたフォームにしてみましょう。
まず、contact/forms.py
にtemplate_name
フィールドを追加します。
1
2
3
4
5
6
7
8
9
10
11
12
13
"""contact/forms.py"""
from django import forms
class ContactForm (forms. Form):
name = forms. CharField(max_length= 50 , label= "お名前" )
email = forms. EmailField(
max_length= 100 ,
label= "メールアドレス" ,
help_text= "お問い合わせの返信のために使います" ,
)
content = forms. CharField(widget= forms. Textarea(), max_length= 1000 , label= "内容" )
template_name = "contact/forms/contact.html" # これを追加
次に、Djangoのソースコードからforms/templates/django/forms/table.html
をコピーして、contact/templates/contact/forms/contact.html
を作ります。
contact/templates/contact/forms/contact.html
は以下の内容に編集します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{% if errors %}
<div class = "notification is-danger" >
{{ errors }}
{% if not fields %}
<ul >
{% for field in hidden_fields %}<li >{{ field }}</li >{% endfor %}
</ul >
{% endif %}
</div >
{% endif %}
{% for field, errors in fields %}
<div class = "field" >
<label class = "label{% with classes=field.css_classes %}{% if classes %} {{ classes }}" {% endif %}{% endwith %}" >{% if field.label %}{{ field.label_tag }}{% endif %}</label >
<div class = "control" >
{{ errors }}
{{ field }}
</div >
{% if field.help_text %}
<p class = "help" >{{ field.help_text }}</p >
{% endif %}
{% if forloop.last %}
{% for field in hidden_fields %}{{ field }}{% endfor %}
{% endif %}
</div >
{% endfor %}
{% if not fields and not errors %}
{% for field in hidden_fields %}{{ field }}{% endfor %}
{% endif %}
http://127.0.0.1:8000/contact/
を再度表示すると、以下のような画面になります。
カスタマイズ後のフォームの表示内容