Djangoのsignalを利用してみたいが、使い方が分からない。
実装例も交えて使い方を教えて欲しい。
こんなお悩みを解決します。
アプリケーションを作成していると「イベントが発火した際に通知を受けて、何らか処理をする」という対応を取りたくなることがあります。
Djangoでは、上記の仕組みがsignalと呼ばれる機構で提供されており、今回はsignalの使い方を題材に実装例を交えて紹介したいと思います。
アルゴリズムやDjangoの実装例を解説するので、興味がある方はぜひ最後までご覧ください。
ベースとなる題材
signalの例を取り上げる上で、良い題材が思いつかなかったため、以前紹介した「メンバ限定チャットルーム」をカスタマイズする方針で進めます。まだ、内容を詳しく知らない方は、以下の記事を参照してください。
【解説】メンバ限定チャットルームの作成【Django Channels】
続きを見る
Djangoのsignalとは?
デザインパターンの一種である「オブザーバーパターン」をDjangoで実現したものとなります。
もう少しかみ砕いて説明すると、何らかのイベントが発火した際に登録された処理を実行する機能のことをsignalと言います。
例えば、以下のようなシステムにおいて「新規ユーザを作成した際に新規ユーザがホストとなるルームを作りたい」という場合を考えます。
この場合、対象とするイベントと登録する処理の内容は次のようになりますよね。
対象とするイベント | 登録する処理 |
---|---|
新規ユーザ情報をDBに書き込んだ(DB書き込み終了) | 新規ユーザがホストとなるルームを作成する |
上記のようにDjangoのプロジェクト内で定義されたアプリケーション間(今回の場合、accountsとchatが対象)で連携を取る場合、signalを利用すると所望する機能を簡単に実現できます。
signalを多用すると思わぬ不具合が入り込むため、利用は最小限にした方が良いです。
今回実現したいこと
本題に入ります。
今回は、以前作成したメンバ限定のチャットルームにおいて、ルームに割り当てられたユーザそれぞれにconfigを割り当てることを考えます。
また、このconfigの仕様は以下のようになります。
以下は、上記の仕様を図示したものとなります。
configの内容
今回は、以下の2つをconfigを用いて管理するものとします。
configで管理する内容(変数名) | 内容 | 備考 |
---|---|---|
order | configと紐づくユーザのルーム内の順位を表す。ただし、重複は許容しない。 | 最小値1、最大値なしとする。 |
offset | オフセット。登録内容を確認するためのもので、特段意味はない。 | 最小値0、最大値10とする。 |
Djangoによる実装例
前回同様に、今回の実装結果はGitHubに格納しています。以降の内容を読み進めるにあたり、参考にしてください。
また、コード上の差分は以下にまとめています。
chat/models.pyの修正
先ほど定義した仕様を満たすモデルをconfigとして定義するため、テーブルのカラムは以下のようにします。
カラム名 | 内容 | 備考 |
---|---|---|
room | configと紐づくルーム | ルームのインスタンスからは「configs」で参照できるようにする。例:room.configs この場合、 room.configs.all() により、ルームに紐づくconfigがすべて取得できる。 |
owner | configと紐づくユーザ | ユーザのインスタンスからは「configs」で参照できるようにする。例:user.configs この場合、 user.configs.all() により、ユーザに紐づくconfigがすべて取得できる。 |
order | configと紐づくユーザのルーム内の順位 | - |
offset | オフセット | - |
これに基づいてモデルを定義すると以下のようになります。
from django.core.validators import MinValueValidator, MaxValueValidator
# 中略
class Config(models.Model):
room = models.ForeignKey(Room, on_delete=models.CASCADE, related_name='configs')
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='configs')
order = models.IntegerField(gettext_lazy('Order'), validators=[MinValueValidator(1)], default=1)
offset = models.IntegerField(gettext_lazy('Offset'), validators=[MinValueValidator(0), MaxValueValidator(10)], default=0)
def __str__(self):
return self.__unicode__()
def __unicode__(self):
return str(self.owner)
chat/signals.pyの作成
本題のsignalを使った実装部分となります。
signalの実装に関してですが、Djangoの公式サイトによると「signalsのサブモジュールとして定義する方が良い」とあるため、これに倣ってchat/signals.py
に定義します。
In practice, signal handlers are usually defined in a
https://docs.djangoproject.com/en/4.2/topics/signals/#connecting-receiver-functionssignals
submodule of the application they relate to.
signalの定義方法
内容に入る前に、signalの定義方法について解説します。
signalは以下の形式に従って定義します。(他の書き方もありますが、ここでは以下の方法で統一します。)
from django.dispatch import receiver
@receiver(監視するイベント, sender=監視対象のモデル)
def handler(sender, **kwargs):
# イベント発火時に実行する処理
ここで、監視するイベントはDjango側で定義されており、以下のイベントが利用できます。
- 簡単のため、モデル操作に関するイベントのみを取り上げます。
- 開発者側で独自のsignalを定義することも可能ですが、今回紹介する範囲外となるため割愛します。
監視するイベントの名称 | パス | 概要 |
---|---|---|
pre_init | django.db.models.signals.pre_init | モデルをインスタンス化する度に、__init__() の実行開始直前にイベントが発火する。 |
post_init | django.db.models.signals.post_init | モデルをインスタンス化する度に、__init__() が実行終了直後にイベントが発火する。 |
pre_save | django.db.models.signals.pre_save | モデルを保存する際、save() の実行開始直前にイベントが発火する。 |
post_save | django.db.models.signals.post_save | モデルを保存する際、save() の実行終了直後にイベントが発火する。 |
pre_delete | django.db.models.signals.pre_delete | モデルを削除する際、delete() の実行開始直前にイベントが発火する。 |
post_delete | django.db.models.signals.post_delete | モデルを削除する際、delete() の実行終了直後にイベントが発火する。 |
m2m_changed | django.db.models.signals.m2m_changed | ManyToManyFieldが変更された際にイベントが発火する。 |
上記のイベントの詳細(取り得る引数など)や上記以外のイベントはコチラを参照してください。
今回登録するsignalの整理
今回はルームに割り当てられたユーザごとにconfigを割り当てたいため、room
の状態に応じて場合分けして考えます。
room の状態 | room.participants の変更有無 | configに対する処理 |
room を新規作成した | -(Don't care) | configを新規作成する |
room を更新した | 変更あり | ユーザの構成に合わせてconfigを新規作成/削除する |
変更なし | 何もしない | |
room を削除した | -(Don't care) | 何もしない(on_delete で定義される処理にあわせる) |
以降では、赤字で強調した部分について実装例を含めて紹介します。
configを新規作成するケース
このケースを図示すると以下のようになります。
上記のイベントは、ルーム作成後(=roomインスタンスを保存した後)にイベントが発火すればよいため、デコレータ部分(@receiver
部分)は以下のようになります。
from django.db.models.signals import post_save
from django.dispatch import receiver
from . import models
@receiver(post_save, sender=models.Room)
def create_new_room_handler(sender, instance, created, **kwargs):
# イベント発火時に呼ばれる関数名は自由に定義できる(ここでは、create_new_room_handlerとした)
# ここにイベントが発火した際の処理を書く
ここで、引数の詳細に関してはコチラを参照してください。
次に、configを新規作成するのは、roomが作成された場合のみとなるため、イベントが発火した際の処理は以下のようになります。ここで、簡単のためorder
はconfigを生成した順としています。
def create_new_room_handler(sender, instance, created, **kwargs):
# 前提:instanceはmodels.Roomのインスタンスを表す
# roomが新規作成された場合のみ実行
if created:
# participantsに割り当てられたユーザすべてに対して処理を実施
for idx, owner in enumerate(instance.participants.all(), 1):
# room、owner、orderを指定してconfigを新規作成
_ = models.Config.objects.create(room=instance, owner=owner, order=idx)
ユーザの構成に合わせてconfigを新規作成/削除するケース
このケースを図示すると以下のようになります。
上記のイベントは、participantsが変更された時(=ManyToManyFieldが変更された時)にイベントが発火すればよいため、デコレータ部分(@receiver
部分)は以下のようになります。
ここで、ManyToManyFieldのイベントを監視する際は中間テーブル(through)を指定することに注意してください。
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from . import models
@receiver(m2m_changed, sender=models.Room.participants.through)
def update_assigned_user_handler(sender, instance, action, reverse, **kwargs):
# ここにイベントが発火した際の処理を書く
先ほど同様に、引数の詳細に関してはコチラを参照してください。
また、ManyToManyFieldは関連する両方のモデル(今回の場合は、RoomとUser)から変更が可能ですが、不具合の混入を抑えるためにイベントが発火した際に処理するケースをforward relationだけに制限します。(詳細は、コチラを参照)
上記の条件において、イベントが発火した際の処理は以下のようになります。ここで、簡単のためorder
はconfigを作成/削除後に割り振り直しています。(orderに同じ番号が割り当たるのを防ぐため)
# order再設定用の関数
def _update_order(room):
# roomに割り当てられたconfigをすべて取得し、通し番号をorderに割り当てる
for idx, config in enumerate(room.configs.all(), 1):
config.order = idx
config.save()
@receiver(m2m_changed, sender=models.Room.participants.through)
def update_assigned_user_handler(sender, instance, action, reverse, **kwargs):
# 追加/削除されたparticipantsの主キーを取得
pk_set = kwargs.get('pk_set', None)
# forward relationのみを対象に処理
if not reverse and pk_set is not None:
pks = list(pk_set)
# participantsにユーザが追加された場合
if action == 'post_add':
# 該当するユーザを取得
users = models.User.objects.filter(pk__in=pks)
for owner in users:
# ユーザごとにconfigを作成する
_ = models.Config.objects.get_or_create(room=instance, owner=owner)
# orderの割り振り直し
_update_order(instance)
# participantsからユーザが削除された場合
elif action == 'post_remove':
# 対象のroomにおいて、削除されたユーザのconfigを取得
queryset = models.Config.objects.filter(room=instance, owner__pk__in=pks)
# configを削除
queryset.delete()
# orderの割り振り直し
_update_order(instance)
以上を踏まえ、最終的に出来上がるsignals.py
は以下のようになります。
from django.db.models.signals import post_save, m2m_changed
from django.dispatch import receiver
from . import models
def _update_order(room):
for idx, config in enumerate(room.configs.all(), 1):
config.order = idx
config.save()
@receiver(post_save, sender=models.Room)
def create_new_room_handler(sender, instance, created, **kwargs):
if created:
for idx, owner in enumerate(instance.participants.all(), 1):
_ = models.Config.objects.create(room=instance, owner=owner, order=idx)
@receiver(m2m_changed, sender=models.Room.participants.through)
def update_assigned_user_handler(sender, instance, action, reverse, **kwargs):
pk_set = kwargs.get('pk_set', None)
if not reverse and pk_set is not None:
pks = list(pk_set)
if action == 'post_add':
users = models.User.objects.filter(pk__in=pks)
for owner in users:
_ = models.Config.objects.get_or_create(room=instance, owner=owner)
_update_order(instance)
elif action == 'post_remove':
queryset = models.Config.objects.filter(room=instance, owner__pk__in=pks)
queryset.delete()
_update_order(instance)
(補足)「関連する両方のモデルから変更が可能」の意味
ForeignKey(外部キー)やManyToManyField(多対多)に要素を割り当てる方法は2パターンあり、ここではその方法を具体例を用いて解説します。
例えば、以下のようなモデルがある場合を考えます。
class User(AbstractBaseUser, PermissionsMixin):
screen_name = models.CharField(
gettext_lazy('screen name'),
max_length=128,
default='',
blank=True,
help_text=gettext_lazy('Option. 128 characters or fewer.'),
)
email = models.EmailField(gettext_lazy('E-mail address'), unique=True)
# 以下省略
class Room(models.Model):
participants = models.ManyToManyField(User, related_name='rooms', verbose_name=gettext_lazy('Participants'), blank=True)
この場合において、participantsにuserを追加してみます。
# データ準備
hoge = User(screen_name='hoge', email='hoge@local.jp')
room = Room()
# 【パターン1】participantsに対して追加する場合
room.participants.add(user)
# 【パターン2】roomsに対して追加する場合
hoge.rooms.add(room)
このように2パターンの追加方法があり、パターン1をforward relation(前方関係)、パターン2をreverse relation(後方関係)と呼びます。
signalの登録
イベントが発火した際にsignals.pyで定義した関数が呼び出されるよう、設定を行います。
変更箇所は、chat/apps.pyとconfigs/settings.pyの2つとなります。
chat/apps.pyの更新
apps.pyを以下のように修正します。
from django.apps import AppConfig
class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'chat'
def ready(self): # 追加
from . import signals # 追加
config/settings.pyの更新
INSTALLED_APPS
を以下のように修正します。
INSTALLED_APPS = [
'daphne',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
# Third-party app
'channels',
# local apps
'accounts',
'chat.apps.ChatConfig', # <----- chatをchat.apps.ChatConfigに変更する
]
signalに対する変更は以上になります。
動作確認用の実装
signalの設定は完了しましたが、現状ではorder
やoffset
が管理画面以外から変更できないため、変更用ページを作成します。
今回のケースでは該当のルームに割り当てられた複数のconfigを更新することになるため、少しトリッキーな方法で対応します。
複数のconfigの更新方法
今回は、Djangoで提供されているModel formsetsを用いて、以下の2ステップで対処します。
通常のforms.ModelFormでformを作成
通常のformを作成する時と同様に定義します。今回、ownerの変更は回避したいため、HiddenInputで処理しています。
class _ConfigForm(forms.ModelForm):
class Meta:
model = models.Config
fields = ('owner', 'order', 'offset')
labels = {
'order': gettext_lazy('Ordering'),
'offset': gettext_lazy('Offset'),
}
widgets = {
'owner': forms.HiddenInput(),
'order': forms.NumberInput(attrs={
'placeholder': gettext_lazy('Enter the ordering'),
'class': 'form-control',
'min': '1',
'step': '1',
}),
'offset': forms.NumberInput(attrs={
'placeholder': gettext_lazy('Enter the offset'),
'class': 'form-control',
'min': '0',
'max': '10',
'step': '1',
}),
}
forms.BaseModelFormSetを用いて複数のformを更新
複数のconfigをまとめて更新するため、forms.BaseModelFormSet
とforms.modelformset_factory
を用いてformset
を用意します。
ここで、order
が一意になっていることをチェックしたいため、以下のように独自にcleanメソッドを定義します。
class _BaseConfigFormSet(forms.BaseModelFormSet):
def clean(self):
super().clean()
# if errors exist, this process is interruptedinterrupted
if any(self.errors):
return
# それぞれのformからorderの値を取得
orders = [form.cleaned_data.get('order') for form in self.forms if form not in self.deleted_forms]
# 集合を用いて重複を排除
uniq = set(orders)
# リストと集合の要素数が一致していない場合は、重複が含まれるため例外処理
if len(orders) != len(uniq):
raise forms.ValidationError(gettext_lazy("Set the participant's order to uniquely determine the move number."))
以上の定義結果を用いて、複数のconfigを更新できるようなConfigFormSet
を定義します。
ConfigFormSet = forms.modelformset_factory(model=models.Config, form=_ConfigForm, formset=_BaseConfigFormSet, extra=0, max_num=0)
forms.BaseModelFormSet
とforms.modelform_factory
の詳細や使い方は公式サイトを参考にしてください。
以上の実装をまとめたchat/forms.pyは以下のようになります。
from django import forms
from django.utils.translation import gettext_lazy
from . import models
User = models.User
# 中略
class _ConfigForm(forms.ModelForm):
class Meta:
model = models.Config
fields = ('owner', 'order', 'offset')
labels = {
'order': gettext_lazy('Ordering'),
'offset': gettext_lazy('Offset'),
}
widgets = {
'owner': forms.HiddenInput(),
'order': forms.NumberInput(attrs={
'placeholder': gettext_lazy('Enter the ordering'),
'class': 'form-control',
'min': '1',
'step': '1',
}),
'offset': forms.NumberInput(attrs={
'placeholder': gettext_lazy('Enter the offset'),
'class': 'form-control',
'min': '0',
'max': '10',
'step': '1',
}),
}
class _BaseConfigFormSet(forms.BaseModelFormSet):
def clean(self):
super().clean()
# if errors exist, this process is interruptedinterrupted
if any(self.errors):
return
orders = [form.cleaned_data.get('order') for form in self.forms if form not in self.deleted_forms]
uniq = set(orders)
if len(orders) != len(uniq):
raise forms.ValidationError(gettext_lazy("Set the participant's order to uniquely determine the move number."))
ConfigFormSet = forms.modelformset_factory(model=models.Config, form=_ConfigForm, formset=_BaseConfigFormSet, extra=0, max_num=0)
chat/views.pyの更新
上記で定義したConfigFormSet
を用いたviewは、以下のようになります。この時のポイントは以下の3点になります。
room
に紐づくconfigを更新対象としたいため、更新対象のモデルはmodels.Roomとする。- ConfigFormSetには該当するconfig一式を与えたいため、
get_form_kwargs
で無理やりquerysetを更新する。(トリッキーな対応) room
に紐づくconfigの更新をしたいため、form_valid
で明示的にform(=ConfigFormSetのインスタンス)のsaveメソッドを呼び出す。
class UpdateConfig(LoginRequiredMixin, OnlyRoomHostMixin, UpdateView):
raise_exception = True
model = models.Room
form_class = forms.ConfigFormSet
template_name = 'chat/config_form.html'
context_object_name = 'room' # Do not use this object in this template
success_url = reverse_lazy('chat:index')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
# 該当するroomのインスタンスからconfigの情報を抽出する
kwargs['queryset'] = kwargs['instance'].configs.all().order_by('order')
# 以降の処理でinstanceを用いた処理が実行されないように、kwargsから削除
del kwargs['instance']
return kwargs
def form_valid(self, form):
# 更新済みのconfigを保存するため、form(= ConfigFormSetのインスタンス)のsaveメソッドを明示的に呼び出す
form.save()
return super().form_valid(form)
def get_success_url(self):
return self.success_url
chat/urls.pyの更新
続いて、urls.pyを更新します。今回は、configの更新を対象にしているため、以下のような名称にしました。
from django.urls import path
from . import views
app_name = 'chat'
urlpatterns = [
path('', views.Index.as_view(), name='index'),
path('create/room', views.CreateRoom.as_view(), name='create_room'),
path('update/room/', views.UpdateRoom.as_view(), name='update_room'),
path('delete/room/', views.DeleteRoom.as_view(), name='delete_room'),
path('enter/room/', views.EnterRoom.as_view(), name='enter_room'),
path('update/config/', views.UpdateConfig.as_view(), name='update_config'), # 追加部分
]
テンプレートの作成・更新
まずは、configの更新画面を作成します。
先ほど、owner
はHiddenInput
で表示させない方針としました。一方、更新時にどのconfigを対象としているかが読み取れないのは不便であるため、configと紐づくユーザの情報は開示できるようにします。
そこで、外部キーであるowner
からユーザ名を取得するためのカスタムテンプレートタグを作成します。
カスタムテンプレートタグの作成
custom_templatetags/chat_utils.py
を作成し、以下に示す内容を定義します。
from django import template
from django.contrib.auth import get_user_model
import re
register = template.Library()
User = get_user_model()
@register.filter(name='conv_fkey2user')
def convert_from_foreignkey_to_usermodel(boundfield):
matched = re.search('(?<=value=")(\d+)', str(boundfield))
user = User.objects.get(pk=int(matched.group(0)))
return user
上記では、以下の2つを実行する処理を実装しています。
- HiddenInputで与えられるHTMLのinputタグに対し、正規表現を用いて
value="【primary keyの値】"
となっている箇所を探す。 - primary keyの値をもとにユーザのインスタンスを取得する。
上記のテンプレートタグを作成したら、settings.pyに登録しておいて下さい。
templates/chat/config_form.htmlの作成
カスタムテンプレートタグも利用し、テンプレートを作成すると以下のようになります。
{% extends 'base.html' %}
{% load chat_utils %}
{% block content %}
<div class="row justify-content-center">
<div class="col-12">
<form method="POST">
{% csrf_token %}
{% if form.non_form_errors %}
<div class="mb-2 d-flex flex-column justify-content-left">
{% for error in form.non_form_errors %}
<div class="text-danger">
<label>Error:</label><span>{{ error }}</span>
</div>
{% endfor %}
</div>
{% endif %}
{{ form.management_form }}
<div class="row">
<div class="col-12">
{% for item in form %}
<div class="row mt-2">
<div class="col-12">
<label for="{{ item.owner.id_for_label }}" class="form-label">
{% with owner=item.owner|conv_fkey2user %}
<u>{{ item.owner.label }}: <span>{{ owner|stringformat:"s" }}</span></u>
{% endwith %}
</label>
{{ item.owner }}
</div>
</div>
<div class="row">
{% include 'custom_form.html' with field=item.order className="col-6" %}
{% include 'custom_form.html' with field=item.offset className="col-6" %}
<div>
{{ item.id }}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<hr />
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<button type="submit" class="btn btn-primary btn-block">Register/Update</button>
</div>
<div class="col-6">
<a href="{% url 'chat:index' %}" class="btn btn-secondary btn-block">Cancel</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
ここで、custom_form.html
はtemplates/custom_form.html
に定義されたファイルで、以下のような内容になっています。
<div {% if className %}class="{{ className }}"{% endif %}>
{% if colLabel|default:"" and colForm|default:"" %}
<label for="{{ field.id_for_label }}" class="{{ colLabel }} col-form-label">
{{ field.label }}
</label>
<div class="{{ colForm }}">
{{ field }}
</div>
{% else %}
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}
</label>
{{ field }}
{% endif %}
{% if field.help_text %}
<span class="custom-helptext">
{{ field.help_text }}
</span>
{% endif %}
{% if field.errors %}
<div class="d-flex flex-column justify-content-left">
{% for error in field.errors %}
<div class="text-danger">
{{ error }}
</div>
{% endfor %}
</div>
{% endif %}
</div>
templates/chat/chat_room.htmlの更新
次にchat_room.html
を更新します。
今回は、configの内容が確認できれば良いため、以下のようにconfigの内容を出力するコードを追加しました。
<div class="row justify-content-left mt-3">
{% for conf in room.configs.all|dictsort:"order" %}
<div class="col-6">
<span>{{ conf.owner|stringformat:"s" }}</span>
<span>(order: {{ conf.order }}, offset: {{ conf.offset }})</span>
</div>
{% endfor %}
</div>
templates/chat/index.htmlの更新
最後に作成したルームに紐づくconfigを更新するためのリンクを追加します。
<div class="dropdown mt-2">
<a class="btn btn-outline-secondary btn-block dropdown-toggle" href="#" role="button"
id="dropdownMenuLink{{ forloop.counter }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Control
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink{{ forloop.counter }}">
<a href="{% url 'chat:update_room' room.pk %}" class="dropdown-item">Update Room</a>
{# 追加箇所:ここから #}
<a href="{% url 'chat:update_config' room.pk %}" class="dropdown-item">Update Config</a>
{# ここまで #}
<div class="dropdown-divider"></div>
<button type="button" class="dropdown-item delete-room-modal-button"
data-deleteurl="{% url 'chat:delete_room' room.pk %}">
<span style="color: red">Delete</span>
</button>
</div>
</div>
動作確認
実装結果をもとに動作確認を行っていきます。
事前準備
スーパーユーザでログインし、4人分のユーザを作成しておきます。
スーパーユーザでログインした後は、http://localhost:8000/admin
にアクセスできるようになります。アクセスすると以下のような画面が表示されることを確認してください。
一通りユーザを追加すると以下のようになると思います。
以上で事前準備は完了です。
ルームXの作成
以降ではAさんのアカウントを用いて、作業を進めます。
また、分かりやすいようにするため、スーパーユーザで管理画面を開いた画面も並べて表示しておきます。chatにアクセスすると以下のような画面(右側)が表示されることを確認してください。
実際にルームXを作成します。
上記の図に合わせると、設定内容は以下のようになりますね。
テーブルの変数名 | 内容 |
---|---|
name | ルームX |
description | room X |
participants | Aさん、Bさん |
一通り入力したらRegister/Updateを押下します。
ルームXの新規作成に合わせて該当するconfigも作成されていることが確認できました。
ルームYの作成
同様の手順でルームYも作成してみます。
上記の図に合わせると、設定内容は以下のようになりますね。
テーブルの変数名 | 内容 |
---|---|
name | ルームY |
description | room Y |
participants | Cさん、Aさん |
一通り入力したらRegister/Updateを押下します。
ルームYの新規作成に合わせて該当するconfigも作成されていることが確認できました。また、Aさんのconfigもルームごとに独立して定義されていますね。
参加者の更新
つづいて、participantsを更新した時の挙動を確認します。
ルームXの設定情報を以下のように変更します。
テーブルの変数名 | 内容 |
---|---|
name | ルームX |
description | room X |
participants | Aさん(削除)、Bさん、Cさん(追加) |
一通り入力したらRegister/Updateを押下します。更新後、participantsの更新内容にあわせてconfigも更新されていることが確認できましたね。
configの内容に対する挙動
今回の実装では、orderの小さい順にユーザ名が表示されるようにしています。
ここでは、configに割り当てられたorderを更新することで表示される順番が変わることを確認します。
プルダウンメニューにあるUpdate Configを押下します。
今回は、Bさんのorderの値を1から5に変更してみましょう。
一通り入力したらRegister/Updateを押下します。更新後、ルームXにアクセスすると、表示される順番が変わっていることが確認できると思います。
config変更時のエラー検出
最後に、config設定時のエラー検出ができることを確認します。
今回は、BaseModelFormSetを継承したクラスにおいて、order
が一意になっていることを確認する処理を入れました。(参考:コチラ)
再度configの更新ページにアクセスし、Bさんのorderの値を2に変更(BさんとCさんのorderの値を揃える)してみます。
一通り入力したらRegister/Updateを押下してみてください。今回のケースではorderの値が重複しない、という制約に反するためエラーが表示されることが確認できると思います。
以上で一通り必要な動作確認が完了しました。
まとめ
今回は、Djangoのsignalについて実装例を交えて解説しました。
デザインパターンの一種をDjangoで実装した例となり、汎用性は高い機能だと思います。ただ、不具合が混入しやすいため使用時は十分注意してください。
また、利用例がイマイチだったため用途がイメージできていない方もいらっしゃると思います。別のシステムとして「GameClock」をDjangoで実装したことがあるため、興味がある方はご覧ください。
友人向けに作成しましたが、今はあまり使っていないみたいです。サイト自体を開示&利用する許可はもらっています。(サーバ自体が非力なので途中で落ちる可能性がある、と話していました。)