【Django】並び替え・ソート機能を実装する|簡単な掲示板アプリの作成

Djangoで掲示板の投稿を並び替えるソート機能の実装方法を解説します。テーブルの項目名をクリックすると、IDや投稿日時順に昇順・降順で並び替えができるようになります。URLのクエリパラメータでソート条件を受け取り、Djangoの`order_by`メソッドでデータを制御する基本的な流れを学べます。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • Python: 3.10.11
  • Django: 5.0.3

サンプルコード

/app/urls.py

/app/urls.py
1     path('<int:pk>/comment/', login_required(views.comment_create), name='comment_create'),
2     path('<int:board_pk>/comment/<int:comment_pk>/delete/', login_required(views.comment_delete), name='comment_delete'),
3     path('my_boards/', login_required(views.my_boards), name='my_boards'),
4-    path('search/', views.board_search, name='search')
5+    path('search/', views.board_search, name='search'),
6+    path('sort/', views.board_sort, name='sort')
7 ]
8

/app/views.py

/app/views.py
1 
2     return render(request, 'index.html', {'boards': boards})
3 
4+def board_sort(request):
5+    sort_by = request.GET.get('sort')
6+    direction = request.GET.get('direction')
7+
8+    # 次のソート順を制御
9+    if direction == 'asc':
10+        next_direction = 'desc'
11+    else:
12+        next_direction = 'asc'
13+
14+    # ソート順をクエリパラメーターで制御
15+    if sort_by:
16+        if direction == 'desc':
17+            boards = Board.objects.all().order_by(f'-{sort_by}')
18+        else:
19+            boards = Board.objects.all().order_by(sort_by)
20+    else:
21+        boards = Board.objects.all()  # デフォルトのソート順
22+
23+    context = {
24+        'boards': boards,
25+        'sort_by': sort_by,
26+        'direction': direction,
27+        'next_direction': next_direction
28+    }
29+    return render(request, 'index.html', context)
30 
31 # ログインページのビュー
32 class CustomLoginView(LoginView):
33

/templates/base.html

/templates/base.html
1     <title>{% block title %}掲示板アプリ{% endblock %}</title>
2     <!-- BootstrapのCDNを読み込む -->
3     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
4+    <!-- Bootstrapのアイコンを読み込む -->
5+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
6     <!-- CSSファイルを読み込む -->
7     <link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
8 </head>
9

/templates/direction.html

/templates/direction.html
1+{% if sort_field == field %}
2+    {% if direction == 'asc' %}
3+        <i class="bi bi-arrow-down-circle-fill"></i>
4+    {% else %}
5+        <i class="bi bi-arrow-up-circle-fill"></i>
6+    {% endif %}
7+{% else %}
8+    <i class="bi bi-dash-circle-fill"></i>
9+{% endif %}
10

/templates/index.html

/templates/index.html
1             </colgroup>
2             <thead>
3                 <tr>
4-                    <th scope="col">掲示板ID</th>
5-                    <th scope="col">タイトル</th>
6-                    <th scope="col">投稿日時</th>
7-                    <th scope="col">更新日時</th>
8+                    <th scope="col">
9+                        掲示板ID
10+                        <a href="{% url 'sort' %}?sort=id&direction={{ next_direction|default:'asc' }}">
11+                            {% include 'direction.html' with sort_field='id' field=sort_by direction=direction %}
12+                        </a>
13+                    </th>
14+                    <th scope="col">
15+                        タイトル
16+                        <a href="{% url 'sort' %}?sort=title&direction={{ next_direction|default:'asc' }}">
17+                            {% include 'direction.html' with sort_field='title' field=sort_by direction=direction %}
18+                        </a>
19+                    </th>
20+                    <th scope="col">
21+                        投稿日時
22+                        <a href="{% url 'sort' %}?sort=created_at&direction={{ next_direction|default:'asc' }}">
23+                            {% include 'direction.html' with sort_field='created_at' field=sort_by direction=direction %}
24+                        </a>
25+                    </th>
26+                    <th scope="col">
27+                        更新日時
28+                        <a href="{% url 'sort' %}?sort=updated_at&direction={{ next_direction|default:'asc' }}">
29+                            {% include 'direction.html' with sort_field='updated_at' field=sort_by direction=direction %}
30+                        </a>
31+                    </th>
32                 </tr>
33             </thead>
34             <tbody>
35

コード解説

変更点: ソート機能用のURLを追加

/app/urls.py
1-    path('search/', views.board_search, name='search')
2+    path('search/', views.board_search, name='search'),
3+    path('sort/', views.board_sort, name='sort')

これは、アプリケーションのURL設定ファイルです。urlpatternsというリストに、新しいURLパターンを追加しました。 path('sort/', views.board_sort, name='sort')という一行は、「ユーザーが /sort/ というURLにアクセスしたとき、views.pyファイルの中にあるboard_sortという関数を呼び出してください」という指示をDjangoに与えています。 name='sort'の部分は、このURLパターンに「sort」という名前を付けています。これにより、HTMLテンプレート側で {% url 'sort' %} と書くだけで、/sort/ というURLを簡単に参照できるようになり、非常に便利です。

変更点: 並び替え処理を行うビュー関数を追加

/app/views.py
1+def board_sort(request):
2+    sort_by = request.GET.get('sort')
3+    direction = request.GET.get('direction')
4+
5+    # 次のソート順を制御
6+    if direction == 'asc':
7+        next_direction = 'desc'
8+    else:
9+        next_direction = 'asc'
10+
11+    # ソート順をクエリパラメーターで制御
12+    if sort_by:
13+        if direction == 'desc':
14+            boards = Board.objects.all().order_by(f'-{sort_by}')
15+        else:
16+            boards = Board.objects.all().order_by(sort_by)
17+    else:
18+        boards = Board.objects.all()  # デフォルトのソート順
19+
20+    context = {
21+        'boards': boards,
22+        'sort_by': sort_by,
23+        'direction': direction,
24+        'next_direction': next_direction
25+    }
26+    return render(request, 'index.html', context)

並び替えの実際の処理を行うための新しい関数 board_sortviews.pyに追加しました。この関数の処理の流れは以下の通りです。

  1. request.GET.get('sort')request.GET.get('direction')を使って、URLの末尾に付加されたクエリパラメータ(例: ?sort=id&direction=asc)から、「どの項目で(sort)」「どちらの向きに(direction)」並び替えるかの情報を受け取ります。
  2. 次にクリックされたときの並び順を決定します。もし現在の向きが昇順(asc)なら、次は降順(desc)になるようにnext_directionという変数を設定します。そうでなければ、次は昇順(asc)になるように設定します。これにより、クリックするたびに昇順と降順が切り替わります。
  3. sort_byに値が入っている場合(つまり、並び替えの項目が指定されている場合)、Board.objects.all()で全ての掲示板データを取得し、.order_by()メソッドで並び替えを実行します。
    • 向きが降順(desc)の場合は、項目名の前にハイフン-を付けた文字列(例: '-id')を.order_by()に渡すことで降順ソートになります。
    • それ以外の場合は、項目名をそのまま渡すことで昇順ソートになります。
  4. 最後に、並び替えた掲示板データboardsや、現在のソート情報、次のソート情報をcontextという辞書にまとめます。
  5. このcontextindex.htmlテンプレートに渡してHTMLを生成し、ユーザーのブラウザに表示します。これにより、同じHTMLファイルを使って、データが並び替えられた状態のページを見せることができます。

変更点: ソート方向を示すアイコン用のCSSを追加

/templates/base.html
1     <title>{% block title %}掲示板アプリ{% endblock %}</title>
2     <!-- BootstrapのCDNを読み込む -->
3     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
4+    <!-- Bootstrapのアイコンを読み込む -->
5+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
6     <!-- CSSファイルを読み込む -->
7     <link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
8 </head>

これは全ページの基礎となるbase.htmlテンプレートです。ここに追加された<link>タグは、Bootstrap IconsというWebアイコン集を利用するために必要なCSSファイルを読み込んでいます。 CDN(Content Delivery Network)という仕組みを使って外部のサーバーからファイルを読み込むことで、自分のプロジェクトにファイルをダウンロードしなくても、手軽に上向き矢印や下向き矢印などのアイコンを表示できるようになります。今回のソート機能で、現在の並び順を視覚的に分かりやすくするために使用します。

変更点: ソート方向アイコンを表示する部品テンプレートを作成

/templates/direction.html
1+{% if sort_field == field %}
2+    {% if direction == 'asc' %}
3+        <i class="bi bi-arrow-down-circle-fill"></i>
4+    {% else %}
5+        <i class="bi bi-arrow-up-circle-fill"></i>
6+    {% endif %}
7+{% else %}
8+    <i class="bi bi-dash-circle-fill"></i>
9+{% endif %}

direction.htmlという新しいHTMLファイルをテンプレートフォルダに作成しました。これは、ソートの状態に応じて適切なアイコンを表示するための「部品」として機能します。

  • まず{% if sort_field == field %}で、この部品が使われているテーブルの列が、現在ソート対象となっている列かどうかを判断します。
  • もしソート対象の列だった場合、次に{% if direction == 'asc' %}で、並び順が昇順か降順かを判断します。
    • 昇順(asc)なら、下向き矢印のアイコン(<i class="bi bi-arrow-down-circle-fill"></i>)を表示します。
    • 降順なら、上向き矢印のアイコン(<i class="bi bi-arrow-up-circle-fill"></i>)を表示します。
  • もしソート対象の列でなかった場合は、ハイフン記号のアイコン(<i class="bi bi-dash-circle-fill"></i>)を表示します。

このように処理を部品化することで、一覧ページの各項目で同じコードを何度も書く必要がなくなり、コードがすっきりと見やすくなります。

変更点: テーブルのヘッダーにソート用リンクとアイコンを追加

/templates/index.html
1-                    <th scope="col">掲示板ID</th>
2+                    <th scope="col">
3+                        掲示板ID
4+                        <a href="{% url 'sort' %}?sort=id&direction={{ next_direction|default:'asc' }}">
5+                            {% include 'direction.html' with sort_field='id' field=sort_by direction=direction %}
6+                        </a>
7+                    </th>
8(以下、タイトル、投稿日時、更新日時の各項目も同様に変更)

掲示板一覧を表示するindex.htmlのテーブルヘッダー部分を変更しました。これまでは単なる文字だった列名(例:「掲示板ID」)を、クリックして並び替えができるようにしています。

  1. <a>タグでテキストとアイコンを囲み、クリック可能なリンクにしています。
  2. href属性の中身が重要です。
    • {% url 'sort' %}: urls.pyで設定したname='sort'のURL(/sort/)を呼び出します。
    • ?sort=id: クエリパラメータを付けて、「idという項目でソートしてください」という情報をURLに含めます。
    • &direction={{ next_direction|default:'asc' }}: views.pyから渡された「次に適用する並び順(next_direction)」をURLに含めます。|default:'asc'は、もし変数が空だった場合にascを初期値として使う設定です。
  3. {% include 'direction.html' ... %}の部分で、先ほど作成したアイコン表示用の部品テンプレートdirection.htmlを読み込んでいます。
    • with sort_field='id' field=sort_by direction=direction の部分で、direction.htmlに3つの変数を渡しています。
      • sort_field='id': この列が「id」であることを伝えます。
      • field=sort_by: 現在ソートされている列名(views.pyから渡された値)を伝えます。
      • direction=direction: 現在のソート方向(views.pyから渡された値)を伝えます。
    • これにより、direction.htmlは受け取った情報をもとに、現在のソート状態に合った適切なアイコンを表示することができます。

この変更を「タイトル」「投稿日時」「更新日時」の各ヘッダーにも適用することで、全ての列で並び替え機能が使えるようになります。

おわりに

今回は、URLのクエリパラメータを利用して、掲示板の投稿を並び替える機能を実装しました。バックエンドではrequest.GETで受け取った情報をもとに.order_by()メソッドで簡単に昇順・降順を制御し、フロントエンドでは<a>タグで次のソート条件を含んだURLを生成する、という一連の流れを学びましたね。現在のソート状態をアイコンで示すためにincludeタグでテンプレートを部品化したように、コードの再利用性を高める工夫も体験できました。このリクエストに応じてデータを操作し、結果を返すという基本的な仕組みは、様々な機能開発の土台となりますのでしっかり覚えておきましょう。

関連コンテンツ