🔎

[Django]ModelChoiceFieldでアクティブなもののみ表示させる方法

2021/09/07に公開

はじめに

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__ でないといけません。

サンプルコード

こちらに掲載しています。

https://github.com/ikemo3/django-example/pull/11

Discussion