Djangoで、通常のフォームセットを使う

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年12月1日15:00に更新(約12日前)
2018年12月1日8:40に作成(約12日前)

概要

Djangoではフォームセットという、複数のフォームをまとめて扱うための機能があります。

モデルと紐づいたフォームを複数扱うモデルフォームセットや、ForeignKey利用時に活躍するインラインフォームセット等もあります。

今回はモデルフォームではなくforms.Formを複数扱う通常のフォームセットを紹介します。

通常のフォームセットの利用例

マッチングサイトめいたものを考えましょう。人の好みというのは難しく、結婚を考えていれば猶更複雑になることでしょう。

以下は非常にシンプルなページですが、相手の一覧表示と、それを絞り込むための検索フォームがあります。 検索フォームが1つ

「年収が600万円以上の男性」は簡単に検索できます。しかし、「年収が600万円以上の男性、もしくは年収300万円以上で身長が180以上体重70以下、もしくは年収不問でルックスがイケメン」といった条件はどうでしょうか。おそらく、フォーム1つで管理するのは難しいでしょう。

こうなってくると、フォームセットが欲しくなります。フォームセットを使えば、以下のように複数の条件を入力できます。 検索フォームが沢山になった

あとは、これらの条件でOR検索をすれば良さそうですね。

models.py

プロフィールを表す、シンプルなモデルを例に作っていきます。

from django.db import models


GENDER_CHOICES = [
    ('1', '女性'),
    ('2', '男性'),
]


class Profile(models.Model):
    name = models.CharField('名前', max_length=100)
    gender = models.CharField('性別', max_length=1, choices=GENDER_CHOICES)
    yearly_income = models.IntegerField('年収(万円)')
    height = models.FloatField('身長')
    weight = models.FloatField('体重')

もし実用的なものを作りたければ更にフィールドを増やしたり、会員登録機能だったり、ログインユーザーとの紐づけも必要になるでしょう。興味があればDjangoで、会員登録機能を自作するも御覧ください。

forms.py

検索フォームなので、モデルフォームではなく通常のフォームとして定義します。

from django import forms
from .models import GENDER_CHOICES

GENDER_CHOICES = GENDER_CHOICES + [('', '---------')]

class ProfileSearchForm(forms.Form):
    gender = forms.ChoiceField(label='性別', choices=GENDER_CHOICES, required=False)
    yearly_income = forms.IntegerField(label='年収(以上)', required=False)
    height = forms.FloatField(label='身長(以上)', required=False)
    weight = forms.FloatField(label='体重(以下)', required=False)


ProfileSearchFormSet = forms.formset_factory(ProfileSearchForm, extra=3)

通常のフォームのフォームセットを使うには、forms.formset_factory()を使います。

身長等の各入力欄は指定しないこともあるので、各フィールドはrequired=Falseとしておきましょう。

検索条件が一つで良い場合は残り2つのフォームは全て空欄にしたいのですが、、GENDER_CHOICESは現状男性か女性のどちらかを必ず入力する必要があります。なので、フォームで使うときだけGENDER_CHOICES = GENDER_CHOICES + [('', '--------')]のようにし、空の選択欄を作成しておきます。

extra=3としてフォームを3つ用意していますが、ボタンを押すとフォームが増えるようにしたい場合はDjangoで、自作ページのインラインフォームに追加ボタンも参考にしてみてください。

views.py

ビューはちょっと複雑です。関数ビューで作成しました。

from django.db.models import Q
from django.shortcuts import render
from .forms import ProfileSearchFormSet
from .models import Profile


def index(request):
    profile_list = Profile.objects.all()
    formset = ProfileSearchFormSet(request.POST or None)
    if request.method == 'POST':
        # 全ての入力欄はrequired=Falseなので、必ずTrueになる。
        formset.is_valid()

        # Qオブジェクトを格納するリスト
        queries = []

        # 各フォームの入力をもとに、Qオブジェクトとして検索条件を作っていく
        for form in formset:
            # Qオブジェクトの引数になる。
            # {gender: 1, height__gte: 170} → Q(gender=1, height__gte=170)
            q_kwargs = {}
            gender = form.cleaned_data.get('gender')
            if gender:
                q_kwargs['gender'] = gender

            yearly_income = form.cleaned_data.get('yearly_income')
            if yearly_income:
                q_kwargs['yearly_income__gte'] = yearly_income

            height = form.cleaned_data.get('height')
            if height:
                q_kwargs['height__gte'] = height

            weight = form.cleaned_data.get('weight')
            if weight:
                q_kwargs['weight__lte'] = weight

            # ここは、そのフォームに入力があった場合にのみ入る。
            # フォームが空なら、q_kwargsは空のままです。
            if q_kwargs:
                q = Q(**q_kwargs)
                queries.append(q)

        if queries:
            # filter(Q(...) | Q(...) | Q(...))を動的に行っている。
            base_query = queries.pop()
            for query in queries:
                base_query |= query
            profile_list = profile_list.filter(base_query)

    context = {
        'profile_list': profile_list,
        'formset': formset,
    }
    return render(request, 'app/profile_list.html', context)

Djangoで、OR検索でも説明したように、OR検索をしたい場合はQオブジェクトを使います

やりたいことは以下のようなコードです。

# 身長180以上の男か、年収1000万円以上の男
profile_list = profile_list.filter(Q(gender=1, height__gte=180) | Q(gender=1, yearly_income__gte=1000))

今回問題なのが、Qオブジェクトの数が決まっていないことです。1フォームにだけ入力されていれば1つで済みますが、3つならばQオブジェクトは3つ必要です。つまり、Qオブジェクトを動的に作成する必要があるのです。

なので、入力のあるフォームの数だけQオブジェクトを作成し、queriesリストに格納していきます。そして、for文を使って|でのORを取っていくという流れです。

profile_list.html

テンプレートはシンプルに済みました。

{% extends 'app/base.html' %}

{% block content %}
    <form action="" method="POST">
        {{ formset.management_form }}
        {% for form in formset %}
            {{ form }}<br>
        {% endfor %}
        {% csrf_token %}
        <button type="submit">検索</button>
    </form>
    <hr>

    {% for profile in profile_list %}
        名前: {{ profile.name }}
        年収: {{ profile.yearly_income }}
        身長: {{ profile.height }}
        体重: {{ profile.weight }}
        <hr>
    {% endfor %}
{% endblock %}
Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする