[Django]ModelChoiceFieldでアクティブなもののみ表示させる方法
はじめに
Djangoの追加・編集画面で外部キー(ForeignKey)に紐づく値を選択させるときはModelChoiceFieldを使いますが、そのときに、アクティブなもののみ表示させるための方法です。
よく使いそうなパターンですが、まとまっている記事が見つからなかったため作ってみました。
クラス構成
次のようなクラス構成を考えます。
モデル
Payeeには有効かどうかのフラグを持ちます。
そして、PaymentモデルにPayeeへの参照を含んでいます。
class Payee(models.Model):
"""支払先"""
name = CharField(max_length=20, verbose_name="支払先名称")
is_active = BooleanField(verbose_name="有効?", default=True)
class Meta:
verbose_name = verbose_name_plural = "支払先"
class Payment(models.Model):
"""支払い"""
date = DateField(verbose_name="支払日")
payee = ForeignKey(Payee, verbose_name="支払先", on_delete=models.PROTECT)
amount = PositiveIntegerField(verbose_name="金額")
class Meta:
verbose_name = verbose_name_plural = "支払い"
ビュー
追加、更新用ビューに、それぞれフォームを定義しています。
class MedicalPaymentAddView(CreateView):
model = Payment
form_class = PaymentAddForm
class MedicalPaymentEditView(UpdateView):
model = Payment
form_class = PaymentEditForm
フォーム
payeeをModelChoiceFieldで定義しています。
現在は ...
としていますが、このquerysetをどう定義するかが鍵です。
class PaymentAddForm(ModelForm):
payee = ModelChoiceField(label="支払先", queryset=...)
class Meta:
model = Payment
fields = ("date", "payee", "amount")
class PaymentEditForm(ModelForm):
payee = ModelChoiceField(label="支払先", queryset=...)
class Meta:
model = Payment
fields = ("date", "payee", "amount")
追加フォームの実装
追加フォームの初期値には、有効なもののみ表示するのが適切でしょう。
その場合コードは次のようになります。
class PaymentAddForm(ModelForm):
payee = ModelChoiceField(label="支払先", queryset=Payee.objects.filter(is_active=True))
class Meta:
model = Payment
fields = ("date", "payee", "amount")
更新フォームの実装
更新フォームの初期値には次のような仕様が望ましいと考えられます。
- 基本は有効なもののみ表示する
- ただし、有効でなくても編集するインスタンスのpayeeである場合は表示する
2番目を入れている理由は、過去に購入した支出で、もう使わない支払先なので有効にしていないが、その支出を編集したい場合があるからです。例えば追加ビューと同様に次のように書くと、支払いに紐づく支払先が有効でない場合に「未選択」として表示されてしまいます。
class PaymentEditForm(ModelForm):
payee = ModelChoiceField(label="支払先", queryset=Payee.objects.filter(is_active=True))
class Meta:
model = Payment
fields = ("date", "payee", "amount")
これを実装するためには、次のようにします。
class PaymentEditForm(ModelForm):
payee = ModelChoiceField(label="支払先", queryset=Payee.objects.none())
def __init__(self, instance, *args, **kwargs):
super().__init__(instance=instance, *args, **kwargs)
self.fields["payee"].queryset = Payee.objects.filter(Q(is_active=True) | Q(id=instance.payee_id))
class Meta:
model = Payment
fields = ("date", "payee", "amount")
ModelChoiceField()ではquerysetを定義せず、__init__
で定義しています。
正確には、querysetは必須項目のため、空のQuerySetで初期化しています(Payee.objects.all()
でもOK)。
そして __init__
では「有効なもの」あるいはidがPayment.payee_idであることのフィルタを入れています。
instanceを参照する必要があるため、 __init__
でないといけません。
サンプルコード
こちらに掲載しています。
Discussion