Django

Pythonフレームワーク「Django」を利用したアプリで検索機能を追加してみた

Django(ジャンゴ)に含まれるデータベースとアクセスするModelには、条件に合ったオブジェクトを取得するfilterメソッドがある。

今回は、作成済の「Django」を利用したアプリケーションに、filterメソッドを利用した検索機能を追加してみたので、そのサンプルプログラムを共有する。

なお、filterメソッドについては、以下のサイトを参照のこと。
https://yu-nix.com/archives/django-filter/

サンプルプログラムの作成

作成したサンプルプログラムの構成は、以下の通り。
サンプルプログラムの構成
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。

demoフォルダ下、templatesフォルダ下の検索画面(search.html)の内容は以下の通り。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>検索画面</title>
    <!-- BootStrap CSS/JSファイルの読み込み -->
    {% load static %}
    <link rel="stylesheet" href="{% static 'demo/css/bootstrap.min.css' %}">
    <script src="{% static 'demo/js/bootstrap.bundle.min.js' %}"></script>
</head>
<body>
    <!-- 画面サイズを画面幅いっぱい(container-fluid)とし、marginを上下左右に設定 -->
    <div class="container-fluid m-4">
        <!-- タイトル行の文字サイズを設定 -->
        <p class="fs-4">検索条件を指定し、「検索」ボタンを押下してください。</p>

        <!-- action属性のURLで、(demoフォルダ内)urls.pyの画面遷移先のname属性の値を指定している -->
        <form action="{% url 'search' %}" method="post">
            <!-- 下記csrf_tokenは、CSRF対策を行うことでform送信時エラーを防ぐために設定 -->
            {% csrf_token %}

            <!-- 表の幅を、画面横幅の9/12の長さに設定 -->
            <div class="col-9">
                <!-- テーブルの枠線を非表示に設定 -->
                <table class="table table-borderless">
                    <!-- 名前 -->
                    <tr>
                        <!-- 画面幅が広くなるにつれてラベルのサイズを狭める設定 -->
                        <td class="col-md-4 col-lg-2">
                            <!-- フォームとラベルの関連付けを設定 -->
                            <label for="name" class="form-label">
                                {{ form.name.label_tag }}
                            </label>
                        </td>
                        <td>
                            <span id="name" class="form-label">
                                {{ form.name }}
                            </span>
                        </td>
                    </tr>
                    <!-- 生年月日(From, To) -->
                    <tr>
                        <td align="left" valign="top">
                            {{ form.birth_day_from_year.label_tag }}
                        </td>
                        <td>
                            {{ form.birth_day_from_year }}年
                            {{ form.birth_day_from_month }}月
                            {{ form.birth_day_from_day }}日~
                            {{ form.birth_day_to_year }}年
                            {{ form.birth_day_to_month }}月
                            {{ form.birth_day_to_day }}日
                            <!-- 生年月日の入力チェックエラーを表示 -->
                            <span class="text-danger">
                                <br/>{{ form.non_field_errors.0 }}
                            </span>
                        </td>
                    </tr>
                    <!-- 性別 -->
                    <tr>
                        <td>{{ form.sex.label_tag }}</td>
                        <td>
                            {% for sex_choice in form.sex %}
                                <span class="me-2">
                                    {{ sex_choice.choice_label }}
                                    {{ sex_choice.tag }}
                                </span>
                            {% endfor %}
                        </td>
                    </tr>
                </table>
            </div>

            <!-- marginを上に設定 -->
            <div class="mt-5">
                <!-- ボタンの色を青色に設定 -->
                <input type="submit" name="next" value="検索" class="btn btn-primary"/>
            </div>
        </form>
    </div>
</body>
</html>



また、demoフォルダ下、view.pyの内容は以下の通りで、検索画面表示処理(IndexView)、検索処理(SearchView)、一覧画面表示処理(UserListView)を追加している。なお、検索処理ではModelのfilterメソッドを利用している。

from django.shortcuts import render
from .forms import InputModelForm
from .forms import SearchForm
from .models import UserData
from django.db.models import Max
from datetime import datetime as dt
from django.views.generic import TemplateView
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.views.generic import ListView
from django.views.generic.edit import DeleteView
from django.views.generic.edit import UpdateView
from django.db.models import Q
from django.core.cache import cache

# クラス定数を定義
SEX_LABEL_IDX = 1  # 性別のラベルを取得するためのインデックス


class InputView(TemplateView):
    """ 入力画面を表示するView """
    template_name = 'demo/input.html'

    def get_context_data(self):
        """ 入力画面に渡すフォーム値を初期化 """
        context = super().get_context_data()
        context['form'] = InputModelForm()
        return context

    def post(self, request):
        """ 入力画面にpostリクエストで遷移 """
        return render(request, self.template_name, self.get_context_data())


class ConfirmView(FormView):
    """ 入力画面で確認ボタンが押下された時の処理を定義するView """
    template_name = 'demo/input.html'  # form_invalidメソッド実行時に表示する画面
    form_class = InputModelForm  # 利用するFormクラス
    success_url = 'demo/confirm.html'  # form_validメソッド実行時の画面遷移先

    def get_context_data(self, form):
        """ 入力チェックエラー時に、入力画面にuser_idの値を渡す """
        context = super().get_context_data()
        context['user_id'] = self.request.POST['user_id']
        return context

    def form_valid(self, form):
        """ フォームの入力チェックを行い、エラーがない場合 """
        # 確認画面で表示するための入力値(性別)の値を生成
        lbl_sex = UserData.sex_data[int(form.cleaned_data['sex']) - 1][SEX_LABEL_IDX]

        # 確認画面に渡す各変数を定義し、確認画面に遷移
        context = {
            'input_form': form,
            'lbl_sex': lbl_sex,
            'lbl_checked': '確認済',
            'user_id': self.request.POST['user_id']
        }
        return render(self.request, self.success_url, context)

    def form_invalid(self, form):
        """ フォームの入力チェックを行い、エラーがある場合 """
        # フォーム値はそのままで入力画面に戻る
        return super().form_invalid(form)


class RegistView(FormView):
    """ 確認画面でボタンが押下された時の処理を定義するView """
    template_name = 'demo/input.html'  # 戻るボタン押下時に表示する画面
    form_class = InputModelForm  # 利用するFormクラス
    success_url = reverse_lazy('complete')  # 送信ボタン押下後の画面遷移先

    def get_context_data(self, form):
        """ 戻るボタン押下時に、入力画面にフォーム値を渡す """
        context = super().get_context_data()
        context['input_form'] = form
        context['user_id'] = self.request.POST['user_id']
        return context

    def form_valid(self, form):
        # 送信ボタンが押下された場合
        if "send" in self.request.POST:
            # 入力データを生成
            user_data = UserData()
            # user_idの値をリクエストから取得
            user_id = self.request.POST['user_id']

            # user_idが設定済(更新)の場合
            if user_id:
                user_data.id = int(user_id)
            # user_idが未設定(追加)の場合
            else:
                # USER_DATAテーブルに登録されているidの最大値を取得
                max_id_dict = UserData.objects.all().aggregate(Max('id'))
                # USER_DATAテーブルに登録されているデータが無い場合、id=1とする
                user_data.id = (max_id_dict['id__max'] or 0) + 1

            # 入力データを保存
            input_form = InputModelForm(self.request.POST, instance=user_data)
            input_form.save()

            # 完了画面に遷移
            return super().form_valid(form)

        # 戻るボタンが押下された場合
        elif "back" in self.request.POST:
            # 確認画面でのフォーム値を入力画面に渡す
            return super().form_invalid(form)


class CompleteView(TemplateView):
    """ 完了画面を表示するView """
    template_name = 'demo/complete.html'


class UserDeleteView(DeleteView):
    """ 一覧画面で削除リンクが押下された時の処理を定義するView """
    template_name = 'demo/delete_confirm.html'  # 削除確認画面を表示
    model = UserData  # UserDataモデル(user_dataテーブルと紐づける)
    success_url = reverse_lazy('index')  # 削除ボタン押下後の画面遷移先


class UserUpdateView(UpdateView):
    """ データ更新時の入力画面を表示するView """
    template_name = 'demo/input.html'
    model = UserData  # UserDataモデル(user_dataテーブルと紐づける)
    form_class = InputModelForm  # 利用するFormクラス


class IndexView(TemplateView):
    """ 検索画面を表示するView """
    template_name = 'demo/search.html'

    def get_context_data(self):
        """ 検索画面に渡すフォーム値を設定 """
        context = super().get_context_data()

        # キャッシュに検索条件データがあれば設定し、キャッシュをクリア
        context['form'] = SearchForm(cache.get('search_form'))
        cache.clear()
        return context


class SearchView(FormView):
    """ 検索画面で検索ボタンが押下された時の処理を定義するView """
    template_name = 'demo/search.html'  # form_invalidメソッド実行時に表示する画面
    form_class = SearchForm  # 利用するFormクラス
    success_url = 'demo/list.html'  # form_validメソッド実行時の画面遷移先

    def get_context_data(self, form):
        context = super().get_context_data()
        return context

    def form_valid(self, form):
        """ フォームの入力チェックを行い、エラーがない場合、検索処理を行う """
        search_name = Q()  # 検索条件(名前)
        search_birthday_from = Q()  # 検索条件(生年月日From)
        search_birthday_to = Q()  # 検索条件(生年月日To)
        search_sex = Q()  # 検索条件(性別)

        # 指定された名前を含むデータを検索
        if form.cleaned_data['name']:
            search_name = Q(name__icontains=form.cleaned_data['name'])

        # 指定された生年月日(From)以降のデータを検索
        if form.cleaned_data['birth_day_from_year']:
            search_birthday_from 
              = Q(birth_year__gt=int(form.cleaned_data['birth_day_from_year'])) \
                | Q(birth_year=int(form.cleaned_data['birth_day_from_year'])
                  , birth_month__gt=int(form.cleaned_data['birth_day_from_month'])) \
                | Q(birth_year=int(form.cleaned_data['birth_day_from_year'])
                  , birth_month=int(form.cleaned_data['birth_day_from_month'])
                  , birth_day__gte=int(form.cleaned_data['birth_day_from_day']))

        # 指定された生年月日(To)以前のデータを検索
        if form.cleaned_data['birth_day_to_year']:
            search_birthday_to 
              = Q(birth_year__lt=int(form.cleaned_data['birth_day_to_year'])) \
                | Q(birth_year=int(form.cleaned_data['birth_day_to_year'])
                  , birth_month__lt=int(form.cleaned_data['birth_day_to_month'])) \
                | Q(birth_year=int(form.cleaned_data['birth_day_to_year'])
                  , birth_month=int(form.cleaned_data['birth_day_to_month'])
                  , birth_day__lte=int(form.cleaned_data['birth_day_to_day']))

        # 指定された性別と同じデータを検索
        if form.cleaned_data['sex']:
            search_sex = Q(sex=form.cleaned_data['sex'])

        # 検索条件に合うデータを検索し一覧に表示
        user_list = UserData.objects.filter(search_name, search_birthday_from
                        , search_birthday_to, search_sex)
        context = {
            'user_list': user_list
        }

        # 検索条件・一覧検索結果をキャッシュに格納し、一覧画面に遷移
        cache.set('search_form', self.request.POST)
        cache.set('user_list', user_list)
        return render(self.request, self.success_url, context)

    def form_invalid(self, form):
        """ フォームの入力チェックを行い、エラーがある場合 """
        # フォーム値はそのままで検索画面に戻る
        return super().form_invalid(form)


class UserListView(TemplateView):
    """ 一覧画面を表示するView """
    template_name = 'demo/list.html'  # 一覧画面のHTML

    def get_context_data(self):
        """ 一覧画面に渡す検索結果をキャッシュから取得し設定 """
        context = super().get_context_data()
        context['user_list'] = cache.get('user_list')
        return context

さらに、demoフォルダ下、urls.pyの内容は以下の通りで、検索画面表示パス(index)、検索処理のパス(search)、一覧画面表示パス(list)を追加している。

from django.urls import path
from . import views

urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('list', views.UserListView.as_view(), name='list'),
    path('search', views.SearchView.as_view(), name='search'),
    path('input', views.InputView.as_view(), name='input'),
    path('confirm', views.ConfirmView.as_view(), name='confirm'),
    path('regist', views.RegistView.as_view(), name='regist'),
    path('complete', views.CompleteView.as_view(), name='complete'),
    path('<int:pk>/update', views.UserUpdateView.as_view(), name='update'),
    path('<int:pk>/delete', views.UserDeleteView.as_view(), name='delete'),
]
「FlexClip」はテンプレートとして利用できる動画・画像・音楽などが充実した動画編集ツールだったテンプレートとして利用できるテキスト・動画・画像・音楽など(いずれも著作権フリー)が充実している動画編集ツールの一つに、「FlexCli...

また、demoフォルダ下、forms.pyの内容は以下の通りで、検索画面で利用するFormクラス(SearchForm)を追加している。

from django import forms
from .models import UserData
from django.core.validators import RegexValidator
import datetime


def check_date(year, month, day):
    """ 年月日が存在する日付の場合はTrue、存在しない日付の場合はFalseを返す """
    try:
        new_date_str = "%04d/%02d/%02d" % (year, month, day)
        datetime.datetime.strptime(new_date_str, "%Y/%m/%d")
        return True
    except ValueError:
        return False


class UserDataModelForm(forms.ModelForm):
    """ user_dataテーブルへアクセスするためのモデルに準じたフォーム """

    class Meta:
        # ベースとなるモデルクラス(UserData)を指定
        model = UserData
        # ベースとなるモデルクラスのうち、表示するフィールドを指定
        fields = ('name', 'birth_year', 'birth_month', 'birth_day', 'sex', 'memo')
        labels = {
            'name': '名前',
            'birth_day': '生年月日',
            'sex': '性別',
            'memo': 'メモ',
        }
        widgets = {
            'name': forms.TextInput(attrs={'size': '13', 'maxlength': '13'}),
            'birth_year': forms.Select,
            'birth_month': forms.Select,
            'birth_day': forms.Select,
            'sex': forms.RadioSelect(),
            'memo': forms.Textarea(attrs={'rows': 6, 'cols': 40}),
        }

    def clean(self):
        """ フォーム内の項目の入力チェック処理 """
        cleaned_data = super().clean()
        birth_year = cleaned_data.get('birth_year')
        birth_month = cleaned_data.get('birth_month')
        birth_day = cleaned_data.get('birth_day')

        # 生年月日が存在しない日付の場合、エラーを返す
        if not check_date(birth_year, birth_month, birth_day):
            raise forms.ValidationError('生年月日を正しく入力してください。')
        return cleaned_data


class InputModelForm(UserDataModelForm):
    """ user_dataテーブルへアクセスするためのモデルに準じたフォームに、項目(check)を追加 """
    """ views.pyからは、このInputModelFormクラスを参照している """
    check = forms.BooleanField(label='入力確認')

    class Meta(UserDataModelForm.Meta):
        fields = UserDataModelForm.Meta.fields + ('check',)


class SearchForm(forms.Form):
    """ 検索画面のフォーム """
    # 名前
    name = forms.CharField(label='名前', max_length=40, required=False)

    # 生年月日(From)
    blank_choice = [('', ''), ]
    birth_day_from_year = forms.CharField(label='生年月日'
              , widget=forms.TextInput(attrs={'size': '4'})
              , max_length=4, required=False)
    birth_day_from_month = forms.ChoiceField(choices=blank_choice 
              + UserData.birth_month_data, required=False)
    birth_day_from_day = forms.ChoiceField(choices=blank_choice 
              + UserData.birth_day_data, required=False)

    # 生年月日(To)
    birth_day_to_year = forms.CharField(widget=forms.TextInput(
              attrs={'size': '4'}), max_length=4, required=False)
    birth_day_to_month = forms.ChoiceField(choices=blank_choice 
              + UserData.birth_month_data, required=False)
    birth_day_to_day = forms.ChoiceField(choices=blank_choice 
              + UserData.birth_day_data, required=False)

    # 性別
    sex = forms.ChoiceField(label='性別'
              , choices=UserData.sex_data
              , widget=forms.RadioSelect(), required=False)

    def clean(self):
        """ フォーム内の項目の入力チェック処理 """
        cleaned_data = super().clean()

        # 生年月日(From)_文字列
        birth_day_from_year = cleaned_data.get('birth_day_from_year')
        birth_day_from_month = cleaned_data.get('birth_day_from_month')
        birth_day_from_day = cleaned_data.get('birth_day_from_day')
        # 生年月日(To)_文字列
        birth_day_to_year = cleaned_data.get('birth_day_to_year')
        birth_day_to_month = cleaned_data.get('birth_day_to_month')
        birth_day_to_day = cleaned_data.get('birth_day_to_day')

        # 生年月日(From)_数値
        int_birth_day_from_year = int(birth_day_from_year or "0")
        int_birth_day_from_month = int(birth_day_from_month or "0")
        int_birth_day_from_day = int(birth_day_from_day or "0")
        # 生年月日(To)_数値
        int_birth_day_to_year = int(birth_day_to_year or "0")
        int_birth_day_to_month = int(birth_day_to_month or "0")
        int_birth_day_to_day = int(birth_day_to_day or "0")

        # 生年月日(From)が入力ありで存在しない日付の場合、エラーを返す
        if not (not birth_day_from_year and not birth_day_from_month 
                and not birth_day_from_day):
            if not check_date(int_birth_day_from_year, int_birth_day_from_month
                            , int_birth_day_from_day):
                raise forms.ValidationError('生年月日(From)を正しく入力してください。')

        # 生年月日(To)が入力ありで存在しない日付の場合、エラーを返す
        if not (not birth_day_to_year and not birth_day_to_month 
                and not birth_day_to_day):
            if not check_date(int_birth_day_to_year, int_birth_day_to_month
                            , int_birth_day_to_day):
                raise forms.ValidationError('生年月日(To)を正しく入力してください。')

        # 生年月日(From), 生年月日(To)が入力ありで生年月日(From)>生年月日(To)の場合、エラーを返す
        birth_day_from_str = "%04d/%02d/%02d" \
             % (int_birth_day_from_year, int_birth_day_from_month, int_birth_day_from_day)
        birth_day_to_str = "%04d/%02d/%02d" \
             % (int_birth_day_to_year, int_birth_day_to_month, int_birth_day_to_day)
        if birth_day_from_year and birth_day_from_month and birth_day_from_day:
            if birth_day_to_year and birth_day_to_month and birth_day_to_day:
                if birth_day_from_str > birth_day_to_str:
                    raise forms.ValidationError('生年月日(From)が生年月日(To)より大きくなっています。')

        return cleaned_data

さらに、入力画面(input.html)、削除確認画面(delete_confirm.html)の内容は以下の通りで、一覧画面に遷移するための「戻る」ボタンを追加している。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>入力画面</title>
    <!-- BootStrap CSS/JSファイルの読み込み -->
    {% load static %}
    <link rel="stylesheet" href="{% static 'demo/css/bootstrap.min.css' %}">
    <script src="{% static 'demo/js/bootstrap.bundle.min.js' %}"></script>
</head>
<body>
    <!-- 画面サイズを画面幅いっぱい(container-fluid)とし、marginを上下左右に設定 -->
    <div class="container-fluid m-4">
        <!-- タイトル行の文字サイズを設定 -->
        <p class="fs-4">下記必要事項を記載の上、「確認」ボタンを押下してください。</p>

        <!-- action属性のURLで、(demoフォルダ内)urls.pyの画面遷移先のname属性の値を指定している -->
        <form action="{% url 'confirm' %}" method="post">
            <!-- 下記csrf_tokenは、CSRF対策を行うことでform送信時エラーを防ぐために設定 -->
            {% csrf_token %}

            <!-- 表の幅を、画面横幅の9/12の長さに設定 -->
            <div class="col-9">
                <!-- テーブルの枠線を非表示に設定 -->
                <table class="table table-borderless">
                    <!-- 名前 -->
                    <tr>
                        <!-- 画面幅が広くなるにつれてラベルのサイズを狭める設定 -->
                        <td class="col-md-4 col-lg-2">
                            <!-- フォームとラベルの関連付けを設定 -->
                            <label for="name" class="form-label">
                                {{ form.name.label_tag }}
                            </label>
                        </td>
                        <td>
                        <span id="name" class="form-label">
                            {{ form.name }}
                        </span>
                        </td>
                    </tr>
                    <!-- 生年月日 -->
                    <tr>
                        <td>{{ form.birth_day.label_tag }}</td>
                        <td>
                            {{ form.birth_year }}年
                            {{ form.birth_month }}月
                            {{ form.birth_day }}日
                            <!-- 生年月日の入力チェックエラーを表示 -->
                            <span class="text-danger">
                                {{ form.non_field_errors.0 }}
                            </span>
                        </td>
                    </tr>
                    <!-- 性別 -->
                    <tr>
                        <td>{{ form.sex.label_tag }}</td>
                        <td>
                            {% for sex_choice in form.sex %}
                            <span class="me-2">
                            {{ sex_choice.choice_label }}
                            {{ sex_choice.tag }}
                        </span>
                            {% endfor %}
                        </td>
                    </tr>
                    <!-- メモ -->
                    <tr>
                        <td>
                            <label for="memo" class="form-label">
                                {{ form.memo.label_tag }}
                            </label>
                        </td>
                        <td>
                        <span id="memo" class="form-label">
                            {{ form.memo }}
                        </span>
                        </td>
                    </tr>
                    <!-- 入力確認 -->
                    <tr>
                        <td>
                            <label for="check" class="form-label">
                                {{ form.check.label_tag }}
                            </label>
                        </td>
                        <td>
                        <span id="check" class="form-label">
                            {{ form.check }}
                        </span>
                        </td>
                    </tr>
                </table>
            </div>

            <!-- ユーザーID -->
            <!-- 一覧画面で更新リンクを押下したため、object.idが設定されている場合 -->
            {% if object.id %}
                <input type="hidden" name="user_id" value="{{ object.id }}"/>
            <!-- 入力画面で入力チェックエラーが発生したり、確認画面で戻るボタンを
                     押下したため、user_idが設定されている場合 -->
            {% elif user_id %}
                <input type="hidden" name="user_id" value="{{ user_id }}"/>
            <!-- 一覧画面でデータ追加ボタンを押下したため、object.idもuser_idも
                     設定されていない場合 -->
            {% else %}
                <input type="hidden" name="user_id" value=""/>
            {% endif %}

            <!-- marginを上に設定 -->
            <div class="mt-5">
                <!-- ボタンの色を青色に設定 -->
                <input type="submit" name="confirm" value="確認" class="btn btn-primary"/>
                <!-- marginを左に設定 -->
                <span class="ms-4">
                    <a href="{% url 'list' %}" class="btn btn-primary">戻る</a>
                </span>
            </div>
        </form>
    </div>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>削除確認画面</title>
    <!-- BootStrap CSS/JSファイルの読み込み -->
    {% load static %}
    <link rel="stylesheet" href="{% static 'demo/css/bootstrap.min.css' %}">
    <script src="{% static 'demo/js/bootstrap.bundle.min.js' %}"></script>
</head>
<body>
    <!-- 画面サイズを画面幅いっぱい(container-fluid)とし、marginを上下左右に設定 -->
    <div class="container-fluid m-4">
        <!-- タイトル行の文字サイズを設定 -->
        <p class="fs-4">下記データを削除する場合は、「削除」ボタンを押下してください。</p>

        <form method="post">
            <!-- 下記csrf_tokenは、CSRF対策を行うことでform送信時エラーを防ぐために設定 -->
            {% csrf_token %}

            <!-- 表の幅を、画面横幅の8/12の長さに設定 -->
            <div class="col-8">
                <!-- テーブルの枠線を非表示に設定 -->
                <table class="table table-borderless">
                    <!-- 名前 -->
                    <tr>
                        <!-- 画面幅が広い場合にラベルのサイズを狭める設定 -->
                        <td class="col-md-4 col-lg-2">
                            名前:
                        </td>
                        <td>{{ object.name }}</td>
                    </tr>
                    <!-- 生年月日 -->
                    <tr>
                        <td>生年月日:</td>
                        <td>
                            {{ object.birth_year }}年
                            {{ object.birth_month }}月
                            {{ object.birth_day }}日
                        </td>
                    </tr>
                    <!-- 性別 -->
                    <tr>
                        <td>性別:</td>
                        <td>
                            {% if object.sex|stringformat:"s" == "1" %}
                                男
                            {% elif object.sex|stringformat:"s" == "2" %}
                                女
                            {% endif %}
                        </td>
                    </tr>
                    <!-- メモ -->
                    <tr>
                        <td>メモ:</td>
                        <td>
                            <!-- 「| default:""」で、Noneを空文字に置き換える -->
                            <!-- テキストエリアの改行を有効にするため、「| linebreaksbr」を付与 -->
                            {{ object.memo | default:"" | linebreaksbr }}
                        </td>
                    </tr>
                </table>
            </div>

            <!-- marginを上に設定 -->
            <div class="mt-5">
                <!-- 削除ボタンの色を赤色に設定 -->
                <input type="submit" value="削除" class="btn btn-danger"/>
                <!-- marginを左に設定 -->
                <span class="ms-4">
                    <!-- 戻るボタンの色を青色に設定 -->
                    <a href="{% url 'list' %}" class="btn btn-primary">戻る</a>
                </span>
            </div>
        </form>
    </div>
</body>
</html>

また、一覧画面(list.html)、完了画面(complete.html)の内容は以下の通りで、「検索画面に戻る」ボタンを追加している。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>一覧画面</title>
    <!-- BootStrap CSS/JSファイルの読み込み -->
    {% load static %}
    <link rel="stylesheet" href="{% static 'demo/css/bootstrap.min.css' %}">
    <script src="{% static 'demo/js/bootstrap.bundle.min.js' %}"></script>
</head>
<body>
    <!-- 画面サイズを画面幅いっぱい(container-fluid)とし、marginを上下左右に設定 -->
    <div class="container-fluid m-4">
        <!-- タイトル行の文字サイズを設定 -->
        <p class="fs-4">ユーザーデータテーブル(user_data)の検索データ</p>

        <!-- 表の幅を、画面横幅の11/12の長さに設定 -->
        <div class="col-11">
            <!-- テーブルの1行おきに背景色を設定 -->
            <table class="table table-striped">
                <!-- テーブルのタイトル行を水色に設定 -->
                <tr class="table-info">
                    <th>ID</th>
                    <th>名前</th>
                    <th>生年月日</th>
                    <th>性別</th>
                    <th></th>
                    <th></th>
                </tr>
                <!-- 検索条件に合うデータを一覧画面に表示 -->
                {% for user in user_list %}
                <tr>
                    <td>{{ user.id }}</td>
                    <td>{{ user.name }}</td>
                    <td>{{ user.birth_year }}年
                        {{ user.birth_month }}月
                        {{ user.birth_day }}日
                    </td>
                    <td>
                        {% if user.sex|stringformat:"s" == "1" %}
                        男
                        {% elif user.sex|stringformat:"s" == "2" %}
                        女
                        {% endif %}
                    </td>
                    <td>
                        <a href="{% url 'update' user.id %}">更新</a>
                    </td>
                    <td>
                        <a href="{% url 'delete' user.id %}">削除</a>
                    </td>
                </tr>
                {% endfor %}
            </table>
        </div>

        <!-- action属性のURLで、(demoフォルダ内)urls.pyの画面遷移先のname属性の値を指定している -->
        <form action="{% url 'input' %}" method="post">
            <!-- 下記csrf_tokenは、CSRF対策を行うことでform送信時エラーを防ぐために設定 -->
            {% csrf_token %}
            <!-- marginを上に設定 -->
            <div class="mt-5">
                <!-- ボタンの色を青色に設定 -->
                <input type="submit" name="next" value="データ追加" class="btn btn-primary"/>
                <!-- marginを左に設定 -->
                <span class="ms-4">
                    <input type="button" name="back" value="検索画面に戻る" 
                        class="btn btn-primary" onclick="location.href={% url 'index' %}"/>
                </span>
            </div>
        </form>
    </div>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>完了画面</title>
    <!-- BootStrap CSS/JSファイルの読み込み -->
    {% load static %}
    <link rel="stylesheet" href="{% static 'demo/css/bootstrap.min.css' %}">
    <script src="{% static 'demo/js/bootstrap.bundle.min.js' %}"></script>
</head>
<body>
    <!-- 画面サイズを画面幅いっぱい(container-fluid)とし、marginを上下左右に設定 -->
    <div class="container-fluid m-4">
        <!-- タイトル行の文字サイズを設定 -->
        <p class="fs-4">お申し込みが完了しました。</p>

        <!-- marginを上に設定 -->
        <div class="mt-5">
            <!-- ボタンの色を青色に設定 -->
            <a href="{% url 'index' %}" class="btn btn-primary">検索画面に戻る</a>
        </div>
    </div>
</body>
</html>

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/python/tree/master/django-search/djangoApp



サンプルプログラムの実行

サンプルプログラムの実行結果は以下の通りで、検索処理を正常に行えることが確認できる。

1) 実行前のuser_dataテーブルの中身は、以下の通り。

select * from user_data
サンプルプログラムの実行結果_1

2) コマンドプロンプトでDjangoプロジェクト名のフォルダに移動し、コマンドプロンプトで「python manage.py runserver」コマンドを実行して、Webサーバーを起動する。
サンプルプログラムの実行結果_2

3) Webブラウザを起動し、「http://127.0.0.1:8000/demo/」にアクセスすると、以下のように、検索画面(search.html)が表示されることが確認できる。また、何も条件を入力せず「検索」ボタンを押下すると、以下のように、user_dataテーブルの中身が全て一覧画面(list.html)に表示されることが確認できる。
サンプルプログラムの実行結果_3_1

サンプルプログラムの実行結果_3_2

4) 一覧画面(list.html)で「データ追加」ボタンを押下すると、以下のように、入力画面(input.html)⇒確認画面(confirm.html)⇒完了画面(complete.html)に遷移することが確認できる。
サンプルプログラムの実行結果_4_1

サンプルプログラムの実行結果_4_2 サンプルプログラムの実行結果_4_3 サンプルプログラムの実行結果_4_4

5) 完了画面(complete.html)で「検索画面に戻る」ボタンを押下すると、検索画面(search.html)に戻ることが確認できる。
サンプルプログラムの実行結果_5_1

サンプルプログラムの実行結果_5_2

6) データ追加後のuser_dataテーブルの中身は、以下の通り。

select * from user_data
サンプルプログラムの実行結果_6

7) 検索画面で生年月日の範囲を指定し「検索」ボタンを押下すると、指定された範囲の生年月日をもつデータが検索され、一覧画面(list.html)で「検索画面に戻る」ボタンを押下すると、検索画面に戻ることが確認できる。
サンプルプログラムの実行結果_7_1

サンプルプログラムの実行結果_7_2 サンプルプログラムの実行結果_7_3

8) 検索画面で名前を指定し「検索」ボタンを押下すると、指定された名前を含むデータが検索されることが確認できる。
サンプルプログラムの実行結果_8_1

サンプルプログラムの実行結果_8_2

9) 検索画面で性別を指定し「検索」ボタンを押下すると、指定された性別をもつデータが検索されることが確認できる。
サンプルプログラムの実行結果_9_1

サンプルプログラムの実行結果_9_2

10) 検索画面で不正な生年月日を指定すると、以下のように、エラーメッセージが表示されることが確認できる。
サンプルプログラムの実行結果_10_1

サンプルプログラムの実行結果_10_2 サンプルプログラムの実行結果_10_3

11) 一覧画面で「削除」リンクを押下すると、以下のように、削除処理が行われることが確認できる。
サンプルプログラムの実行結果_11_1

サンプルプログラムの実行結果_11_2 サンプルプログラムの実行結果_11_3

12) データ削除後のuser_dataテーブルの中身は、以下の通り。

select * from user_data
サンプルプログラムの実行結果_12

要点まとめ

  • Django(ジャンゴ)に含まれるデータベースとアクセスするModelには、条件に合ったオブジェクトを取得するfilterメソッドがあり、検索機能で利用できる。