【Django】簡単な掲示板アプリの作り方を解説|初心者がアプリ作成をしながらCRUD機能を学ぶのにおすすめ

Djangoを使い、簡単な掲示板アプリの作り方を解説します。この記事では、Web開発の基本となるCRUD(データの作成・表示・更新・削除)機能を実装する流れを学べます。モデルでのデータベース設計から、ビューでの処理、テンプレートでの画面表示、フォームの使い方まで、Djangoの基礎を実践的に習得できます。

作成日: 更新日:

開発環境

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

サンプルコード

/.gitignore

/.gitignore
1 
2 # Ignore Django migration files
3 **/migrations/*.py
4-**/migrations/*.pyc
5+**/migrations/*.pyc
6+!**/__init__.py
7

/app/forms.py

/app/forms.py
1+# app/forms.py
2+from django import forms
3+from .models import Board
4+
5+class BoardForm(forms.ModelForm):
6+    class Meta:
7+        model = Board
8+        fields = ['title', 'content']
9

/app/models.py

/app/models.py
1 from django.db import models
2 
3-# Create your models here.
4+class Board(models.Model):
5+    title = models.CharField(max_length=200)
6+    content = models.TextField()
7+    created_at = models.DateTimeField(auto_now_add=True)
8+    updated_at = models.DateTimeField(auto_now=True)
9+
10+    def __str__(self):
11+        return self.title
12

/app/urls.py

/app/urls.py
1+# app/urls.py
2+from django.urls import path
3+from . import views
4+
5+urlpatterns = [
6+    path('', views.index, name='index'),
7+    path('new/', views.new, name='new'),
8+    path('create/', views.create, name='create'),
9+    path('<int:pk>/', views.show, name='show'),
10+    path('<int:pk>/edit/', views.edit, name='edit'),
11+    path('<int:pk>/update/', views.update, name='update'),
12+    path('<int:pk>/delete/', views.delete, name='delete'),
13+]
14

/app/views.py

/app/views.py
1-from django.shortcuts import render
2+# app/views.py
3+from django.shortcuts import render, redirect, get_object_or_404
4+from .models import Board
5+from .forms import BoardForm
6 
7-# Create your views here.
8+def index(request):
9+    boards = Board.objects.all().order_by('-updated_at')
10+    return render(request, 'index.html', {'boards': boards})
11+
12+def new(request):
13+    form = BoardForm()
14+    return render(request, 'new.html', {'form': form})
15+
16+def create(request):
17+    if request.method == 'POST':
18+        form = BoardForm(request.POST)
19+        if form.is_valid():
20+            form.save()
21+            return redirect('index')
22+    else:
23+        form = BoardForm()
24+    return render(request, 'new.html', {'form': form})
25+
26+def show(request, pk):
27+    board = Board.objects.get(pk=pk)
28+    return render(request, 'show.html', {'board': board})
29+
30+def edit(request, pk):
31+    board = Board.objects.get(pk=pk)
32+    form = BoardForm(instance=board)
33+    return render(request, 'edit.html', {'form': form, 'board': board})
34+
35+def update(request, pk):
36+    board = Board.objects.get(pk=pk)
37+    if request.method == 'POST':
38+        form = BoardForm(request.POST, instance=board)
39+        if form.is_valid():
40+            form.save()
41+            return redirect('show', pk=pk)
42+    else:
43+        form = BoardForm(instance=board)
44+    return render(request, 'edit.html', {'form': form, 'board': board})
45+
46+def delete(request, pk):
47+    board = get_object_or_404(Board, pk=pk)
48+    if request.method == 'POST':
49+        board.delete()
50+        return redirect('index')
51+    return redirect('index', pk=pk)
52

/config/settings.py

/config/settings.py
1 """
2 
3 from pathlib import Path
4+import os
5 
6 # Build paths inside the project like this: BASE_DIR / 'subdir'.
7 BASE_DIR = Path(__file__).resolve().parent.parent
8     "django.contrib.sessions",
9     "django.contrib.messages",
10     "django.contrib.staticfiles",
11+    "app"
12 ]
13 
14 MIDDLEWARE = [
15 TEMPLATES = [
16     {
17         "BACKEND": "django.template.backends.django.DjangoTemplates",
18-        "DIRS": [],
19+        "DIRS": [os.path.join(BASE_DIR, 'templates')],
20         "APP_DIRS": True,
21         "OPTIONS": {
22             "context_processors": [
23 # https://docs.djangoproject.com/en/5.0/howto/static-files/
24 
25 STATIC_URL = "static/"
26+STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
27 
28 # Default primary key field type
29 # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
30

/config/urls.py

/config/urls.py
1     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
2 """
3 from django.contrib import admin
4-from django.urls import path
5+from django.urls import path, include
6 
7 urlpatterns = [
8     path("admin/", admin.site.urls),
9+    path('', include('app.urls')),
10 ]
11

/static/css/styles.css

/static/css/styles.css
1+/* 全体的に適用 */
2+* {
3+    padding: 0;
4+    margin: 0;
5+    box-sizing: border-box;
6+    text-decoration: none;
7+    color: inherit;
8+    list-style-position: inside;
9+}
10+
11+/* フォントデザイン */
12+body {
13+    font-size: 16px;
14+    color: #333333;
15+    font-family: "Hiragino Kaku Gothic ProN", sans-serif;
16+}
17

/templates/base.html

/templates/base.html
1+<!-- staticテンプレートの読み込み -->
2+{% load static %}
3+<!-- baseテンプレート -->
4+<!DOCTYPE html>
5+<html lang="ja">
6+<head>
7+    <meta charset="UTF-8">
8+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
9+    <title>{% block title %}掲示板アプリ{% endblock %}</title>
10+    <!-- BootstrapのCDNを読み込む -->
11+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
12+    <!-- CSSファイルを読み込む -->
13+    <link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
14+</head>
15+<body>
16+    <!-- ヘッダーの内容 -->
17+    <header class="bg-success">
18+        <div class="container">
19+            <div class="pt-3 pb-1 mb-2 text-white">
20+                <h1 class="display-4">掲示板アプリ</h1>
21+            </div>
22+        </div>
23+    </header>
24+
25+    <!-- 各ページの内容 -->
26+    <main>
27+        <div class="container">
28+            <div class="mb-2">
29+                {% block content %}{% endblock %}
30+            </div>
31+        </div>
32+    </main>
33+
34+    <!-- フッターの内容 -->
35+    <footer class="bg-secondary">
36+        <div class="container">
37+            <div class="pt-3 pb-1 text-white">
38+                <p class="text-center">&copy; 2024 掲示板アプリ</p>
39+            </div>
40+        </div>
41+    </footer>
42+</body>
43+</html>
44

/templates/edit.html

/templates/edit.html
1+{% extends 'base.html' %}
2+
3+{% block title %}{{ board.title }}の編集画面{% endblock %}
4+
5+{% block content %}
6+    <section>
7+        <h2>{{ board.title }}の編集画面</h2>
8+        <form action="{% url 'update' pk=board.pk %}" method="post" class="mt-4">
9+            {% csrf_token %}
10+            <div class="mb-3">
11+                <label for="{{ form.title.id_for_label }}" class="form-label">タイトル</label></br>
12+                {{ form.title }}
13+            </div>
14+            <div class="mb-3">
15+                <label for="{{ form.content.id_for_label }}" class="form-label">内容</label></br>
16+                {{ form.content }}
17+            </div>
18+            <button type="submit" class="btn btn-success">掲示板を更新する</button>
19+            <a href="{% url 'show' pk=board.id %}" class="btn btn-primary">詳細ページに戻る</a>
20+        </form>
21+    </section>
22+{% endblock %}
23

/templates/index.html

/templates/index.html
1+{% extends 'base.html' %}
2+
3+{% block title %}みんなの投稿一覧{% endblock %}
4+
5+{% block content %}
6+    <section>
7+        <h2>みんなの投稿一覧</h2>
8+        <table class="table">
9+            <colgroup>
10+                <col style="width: 10%">
11+                <col style="width: 50%">
12+                <col style="width: 20%">
13+                <col style="width: 20%">
14+            </colgroup>
15+            <thead>
16+                <tr>
17+                    <th scope="col">掲示板ID</th>
18+                    <th scope="col">タイトル</th>
19+                    <th scope="col">投稿日時</th>
20+                    <th scope="col">更新日時</th>
21+                </tr>
22+            </thead>
23+            <tbody>
24+                {% for board in boards %}
25+                    <tr>
26+                        <td scope="row">{{ board.id }}</td>
27+                        <td><a href="{% url 'show' pk=board.id %}">{{ board.title }}</a></td>
28+                        <td>{{ board.created_at }}</td>
29+                        <td>{{ board.updated_at }}</td>
30+                    </tr>
31+                {% endfor %}
32+            </tbody>
33+        </table>
34+        <a href="{% url 'new' %}" class="btn btn-primary">新規投稿作成</a>
35+    </section>
36+{% endblock %}
37

/templates/new.html

/templates/new.html
1+{% extends 'base.html' %}
2+
3+{% block title %}新規投稿画面{% endblock %}
4+
5+{% block content %}
6+    <section>
7+        <h2>新規投稿画面</h2>
8+        <form action="{% url 'create' %}" method="post" class="mt-4">
9+            {% csrf_token %}
10+            <div class="mb-3">
11+                <label for="{{ form.title.id_for_label }}" class="form-label">タイトル</label></br>
12+                {{ form.title }}
13+            </div>
14+            <div class="mb-3">
15+                <label for="{{ form.content.id_for_label }}" class="form-label">内容</label></br>
16+                {{ form.content }}
17+            </div>
18+            <a href="{% url 'index' %}" class="btn btn-primary">一覧に戻る</a>
19+            <button type="submit" class="btn btn-success">投稿する</button>
20+        </form>
21+    </section>
22+{% endblock %}
23

/templates/show.html

/templates/show.html
1+{% extends 'base.html' %}
2+
3+{% block title %}{{ board.title }}の詳細画面{% endblock %}
4+
5+{% block content %}
6+    <section>
7+        <h2>{{ board.title }}の詳細画面</h2>
8+        <p>{{ board.content }}</p>
9+        <p>
10+            作成日時: {{ board.created_at }}</br>
11+            更新日時: {{ board.updated_at }}
12+        </p>
13+        <a href="{% url 'index' %}" class="btn btn-primary">一覧に戻る</a>
14+        <a href="{% url 'edit' pk=board.id %}" class="btn btn-success">編集する</a>
15+        <form action="{% url 'delete' pk=board.pk %}" method="post" class="d-inline">
16+            {% csrf_token %}
17+            <input type="hidden" name="action" value="delete">
18+            <button type="submit" class="btn btn-danger">削除する</button>
19+        </form>
20+    </section>
21+{% endblock %}
22

コード解説

変更点: Gitの管理対象外設定の更新

/.gitignore
1 # Ignore Django migration files
2 **/migrations/*.py
3-**/migrations/*.pyc
4+**/migrations/*.pyc
5+!**/__init__.py
6

プロジェクトのバージョン管理ツールであるGitが、どのファイルを追跡対象から除外するかを設定する.gitignoreファイルです。ここでは、Djangoのデータベース設計図の変更履歴であるマイグレーションファイルのうち、Pythonが自動生成するコンパイル済みのファイル (*.pyc) をGitの管理対象から除外するように設定しています。また、!**/__init__.pyという記述により、__init__.pyファイルだけは除外設定の例外とし、管理対象に含めるようにしています。これにより、不要なファイルがリポジトリに含まれるのを防ぎます。

変更点: 投稿内容を扱うフォームの新規作成

/app/forms.py
1+# app/forms.py
2+from django import forms
3+from .models import Board
4+
5+class BoardForm(forms.ModelForm):
6+    class Meta:
7+        model = Board
8+        fields = ['title', 'content']
9

ユーザーが掲示板に投稿する際の入力フォームを定義するforms.pyファイルを新しく作成しました。forms.ModelFormを継承することで、モデルの定義に基づいてフォームを自動的に生成できます。class Metaの中で、model = BoardとすることでBoardモデル(後述)と連携させ、fields = ['title', 'content']で、フォームに表示する入力項目をタイトルと内容に限定しています。これにより、データベースの構造と連動した入力フォームを簡単に作成できます。

変更点: 掲示板データのモデル定義

/app/models.py
1 from django.db import models
2 
3-# Create your models here.
4+class Board(models.Model):
5+    title = models.CharField(max_length=200)
6+    content = models.TextField()
7+    created_at = models.DateTimeField(auto_now_add=True)
8+    updated_at = models.DateTimeField(auto_now=True)
9+
10+    def __str__(self):
11+        return self.title
12

アプリケーションが扱うデータ構造を定義するmodels.pyファイルです。Boardという名前のモデルクラスを作成し、これがデータベースのテーブル設計図となります。

  • title: CharFieldは短いテキスト用のフィールドで、max_length=200で最大200文字という制限を設けています。
  • content: TextFieldは長いテキスト用のフィールドです。
  • created_at: DateTimeFieldは日時を保存するフィールドです。auto_now_add=Trueを指定すると、データが最初に作成された日時が自動的に保存されます。
  • updated_at: こちらも日時フィールドですが、auto_now=Trueを指定すると、データが更新されるたびにその日時が自動的に保存されます。 __str__メソッドは、このオブジェクトが文字列として表現される際に何を表示するかを定義するもので、ここでは投稿のタイトルを返すようにしています。

変更点: アプリケーションのURL設定

/app/urls.py
1+# app/urls.py
2+from django.urls import path
3+from . import views
4+
5+urlpatterns = [
6+    path('', views.index, name='index'),
7+    path('new/', views.new, name='new'),
8+    path('create/', views.create, name='create'),
9+    path('<int:pk>/', views.show, name='show'),
10+    path('<int:pk>/edit/', views.edit, name='edit'),
11+    path('<int:pk>/update/', views.update, name='update'),
12+    path('<int:pk>/delete/', views.delete, name='delete'),
13+]
14

掲示板アプリ (app) 内のURLと、それに対応する処理(ビュー関数)を紐付けるurls.pyファイルを新しく作成しました。urlpatternsというリストの中に、path関数を使って各URLパターンを定義しています。

  • path('', views.index, name='index'): ルートURL(例: /)にアクセスがあった場合、views.pyindex関数を呼び出します。name='index'は、このURL設定に名前を付けるもので、後でテンプレートなどからURLを呼び出す際に便利です。
  • <int:pk>: URLの一部を可変にできます。ここでは整数 (int) をpk(Primary Keyの略)という名前の変数として受け取り、ビュー関数に渡します。これにより、「どの投稿か」を識別できます。
  • このファイルでは、一覧表示、新規作成、詳細表示、編集、削除といった掲示板の各機能に対応するURLが定義されています。

変更点: CRUD機能を実現するビューの作成

/app/views.py
1-from django.shortcuts import render
2+# app/views.py
3+from django.shortcuts import render, redirect, get_object_or_404
4+from .models import Board
5+from .forms import BoardForm
6 
7-# Create your views here.
8+def index(request):
9+    boards = Board.objects.all().order_by('-updated_at')
10+    return render(request, 'index.html', {'boards': boards})
11+
12+def new(request):
13+    form = BoardForm()
14+    return render(request, 'new.html', {'form': form})
15+
16+def create(request):
17+    if request.method == 'POST':
18+        form = BoardForm(request.POST)
19+        if form.is_valid():
20+            form.save()
21+            return redirect('index')
22+    else:
23+        form = BoardForm()
24+    return render(request, 'new.html', {'form': form})
25+
26+def show(request, pk):
27+    board = Board.objects.get(pk=pk)
28+    return render(request, 'show.html', {'board': board})
29+
30+def edit(request, pk):
31+    board = Board.objects.get(pk=pk)
32+    form = BoardForm(instance=board)
33+    return render(request, 'edit.html', {'form': form, 'board': board})
34+
35+def update(request, pk):
36+    board = Board.objects.get(pk=pk)
37+    if request.method == 'POST':
38+        form = BoardForm(request.POST, instance=board)
39+        if form.is_valid():
40+            form.save()
41+            return redirect('show', pk=pk)
42+    else:
43+        form = BoardForm(instance=board)
44+    return render(request, 'edit.html', {'form': form, 'board': board})
45+
46+def delete(request, pk):
47+    board = get_object_or_404(Board, pk=pk)
48+    if request.method == 'POST':
49+        board.delete()
50+        return redirect('index')
51+    return redirect('index', pk=pk)

Webアプリケーションの主要な処理を記述するviews.pyファイルです。URL設定 (urls.py) に基づいて呼び出され、必要な処理を行い、結果を画面に表示するためのテンプレートを返します。

  • index: 全ての掲示板投稿 (Board.objects.all()) を取得し、更新日時が新しい順 (order_by('-updated_at')) に並べ替えて、index.htmlテンプレートに渡します。(Read機能)
  • new: 新規投稿用の空のフォーム (BoardForm()) を作成し、new.htmlテンプレートに渡します。
  • create: POSTメソッドでデータが送信された場合、そのデータでフォームを検証 (is_valid()) します。検証が通ればデータを保存 (save()) し、一覧ページにリダイレクトします。(Create機能)
  • show: URLから受け取ったpkを使い、特定の投稿データ (Board.objects.get(pk=pk)) を1件取得して、show.htmlテンプレートに渡します。(Read機能)
  • edit: pkで指定された既存の投稿データを取得し、その内容が入力された状態のフォーム (BoardForm(instance=board)) を作成して、edit.htmlテンプレートに渡します。
  • update: edit画面からPOSTメソッドでデータが送信された際、既存の投稿データを上書き更新します。処理の流れはcreateと似ていますが、instance=boardを指定することで新規作成ではなく更新となります。(Update機能)
  • delete: pkで指定された投稿を削除します。安全のため、POSTリクエストが来た時のみ削除処理 (board.delete()) を実行します。(Delete機能)

変更点: プロジェクト全体の設定変更

/config/settings.py
1 from pathlib import Path
2+import os
3 
4 # Build paths inside the project like this: BASE_DIR / 'subdir'.
5 BASE_DIR = Path(__file__).resolve().parent.parent
6     "django.contrib.sessions",
7     "django.contrib.messages",
8     "django.contrib.staticfiles",
9+    "app"
10 ]
11 
12 MIDDLEWARE = [
13 TEMPLATES = [
14     {
15         "BACKEND": "django.template.backends.django.DjangoTemplates",
16-        "DIRS": [],
17+        "DIRS": [os.path.join(BASE_DIR, 'templates')],
18         "APP_DIRS": True,
19         "OPTIONS": {
20             "context_processors": [
21 # https://docs.djangoproject.com/en/5.0/howto/static-files/
22 
23 STATIC_URL = "static/"
24+STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
25 
26 # Default primary key field type
27 # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

Djangoプロジェクト全体の設定を行うsettings.pyファイルです。3つの重要な変更を加えています。

  1. INSTALLED_APPS: "app" を追加しました。これにより、Djangoプロジェクトが今回作成したappというアプリケーションの存在を認識し、その中のモデルやURLなどを利用できるようになります。
  2. TEMPLATES"DIRS": [os.path.join(BASE_DIR, 'templates')] を追加しました。これは、DjangoがHTMLテンプレートファイルを探す場所として、プロジェクトのルートディレクトリ直下にあるtemplatesフォルダも参照するように指示する設定です。
  3. STATICFILES_DIRS: [os.path.join(BASE_DIR, 'static')] を追加しました。これは、CSSやJavaScript、画像ファイルなどの静的(スタティック)ファイルを探す場所として、プロジェクトのルートディレクトリ直下にあるstaticフォルダも参照するように指示する設定です。

変更点: プロジェクト全体のURL設定の変更

/config/urls.py
1 from django.contrib import admin
2-from django.urls import path
3+from django.urls import path, include
4 
5 urlpatterns = [
6     path("admin/", admin.site.urls),
7+    path('', include('app.urls')),
8 ]

プロジェクト全体の入り口となるurls.pyファイルです。include関数をインポートし、path('', include('app.urls'))という行を追加しました。これは、ルートURL(例: http://example.com/)以下のアクセスがあった場合に、その後のURLの解決をappアプリケーション内のurls.pyファイルに委任するという設定です。これにより、アプリケーションごとにURL設定を分割でき、プロジェクトの管理がしやすくなります。

変更点: 基本的なCSSスタイルの追加

/static/css/styles.css
1+/* 全体的に適用 */
2+* {
3+    padding: 0;
4+    margin: 0;
5+    box-sizing: border-box;
6+    text-decoration: none;
7+    color: inherit;
8+    list-style-position: inside;
9+}
10+
11+/* フォントデザイン */
12+body {
13+    font-size: 16px;
14+    color: #333333;
15+    font-family: "Hiragino Kaku Gothic ProN", sans-serif;
16+}

アプリケーションの見た目を整えるためのCSSファイルをstatic/css/styles.cssとして新しく作成しました。ここでは、全ての要素の余白をリセットしたり、基本的なフォントサイズや色、フォントの種類を指定したりといった、サイト全体の基本的なスタイルを定義しています。

変更点: 全ページの基礎となるベーステンプレートの作成

/templates/base.html
1+<!-- staticテンプレートの読み込み -->
2+{% load static %}
3+<!-- baseテンプレート -->
4+<!DOCTYPE html>
5+<html lang="ja">
6+<head>
7+    <meta charset="UTF-8">
8+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
9+    <title>{% block title %}掲示板アプリ{% endblock %}</title>
10+    <!-- BootstrapのCDNを読み込む -->
11+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
12+    <!-- CSSファイルを読み込む -->
13+    <link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
14+</head>
15+<body>
16+    <!-- ヘッダーの内容 -->
17+    <header class="bg-success">
18+        <div class="container">
19+            <div class="pt-3 pb-1 mb-2 text-white">
20+                <h1 class="display-4">掲示板アプリ</h1>
21+            </div>
22+        </div>
23+    </header>
24+
25+    <!-- 各ページの内容 -->
26+    <main>
27+        <div class="container">
28+            <div class="mb-2">
29+                {% block content %}{% endblock %}
30+            </div>
31+        </div>
32+    </main>
33+
34+    <!-- フッターの内容 -->
35+    <footer class="bg-secondary">
36+        <div class="container">
37+            <div class="pt-3 pb-1 text-white">
38+                <p class="text-center">&copy; 2024 掲示板アプリ</p>
39+            </div>
40+        </div>
41+    </footer>
42+</body>
43+</html>

全てのHTMLページで共通する部分(ヘッダー、フッター、CSSの読み込みなど)をまとめた、土台となるbase.htmlを新しく作成しました。

  • {% load static %}: staticフォルダ内のファイル(CSSなど)を読み込むためのおまじないです。
  • {% static 'css/styles.css' %}: 先ほど作成したCSSファイルを読み込んでいます。
  • {% block title %}{% block content %}: これらは「ブロック」と呼ばれる領域で、このbase.htmlを継承する他のテンプレートファイルが、この部分を独自の内容で上書きできるようになっています。これにより、各ページで異なるタイトルやメインコンテンツを表示しつつ、共通のレイアウトを維持できます。
  • BootstrapのCDNを読み込むことで、手軽に見栄えの良いデザインを適用しています。

変更点: 投稿編集ページのテンプレート作成

/templates/edit.html
1+{% extends 'base.html' %}
2+
3+{% block title %}{{ board.title }}の編集画面{% endblock %}
4+
5+{% block content %}
6+    <section>
7+        <h2>{{ board.title }}の編集画面</h2>
8+        <form action="{% url 'update' pk=board.pk %}" method="post" class="mt-4">
9+            {% csrf_token %}
10+            <div class="mb-3">
11+                <label for="{{ form.title.id_for_label }}" class="form-label">タイトル</label></br>
12+                {{ form.title }}
13+            </div>
14+            <div class="mb-3">
15+                <label for="{{ form.content.id_for_label }}" class="form-label">内容</label></br>
16+                {{ form.content }}
17+            </div>
18+            <button type="submit" class="btn btn-success">掲示板を更新する</button>
19+            <a href="{% url 'show' pk=board.id %}" class="btn btn-primary">詳細ページに戻る</a>
20+        </form>
21+    </section>
22+{% endblock %}

投稿を編集するための画面 (edit.html) を新しく作成しました。

  • {% extends 'base.html' %}: base.htmlのレイアウトを継承します。
  • {% block title %}{% block content %}: base.htmlで定義したブロックを、このページ独自の内容で上書きしています。
  • {{ form.title }}{{ form.content }}: views.pyから渡されたフォームオブジェクトを使って、タイトルと内容の入力欄を自動的に描画します。
  • action="{% url 'update' pk=board.pk %}": フォームの送信先URLをurls.pyで定義したupdateという名前のURLに動的に設定しています。board.pkを渡すことで、どの投稿を更新するのかを明示しています。
  • {% csrf_token %}: 不正なサイトからのPOSTリクエストを防ぐための、Djangoに必須のセキュリティ対策です。

変更点: 投稿一覧ページのテンプレート作成

/templates/index.html
1+{% extends 'base.html' %}
2+
3+{% block title %}みんなの投稿一覧{% endblock %}
4+
5+{% block content %}
6+    <section>
7+        <h2>みんなの投稿一覧</h2>
8+        <table class="table">
9+            <colgroup>
10+                <col style="width: 10%">
11+                <col style="width: 50%">
12+                <col style="width: 20%">
13+                <col style="width: 20%">
14+            </colgroup>
15+            <thead>
16+                <tr>
17+                    <th scope="col">掲示板ID</th>
18+                    <th scope="col">タイトル</th>
19+                    <th scope="col">投稿日時</th>
20+                    <th scope="col">更新日時</th>
21+                </tr>
22+            </thead>
23+            <tbody>
24+                {% for board in boards %}
25+                    <tr>
26+                        <td scope="row">{{ board.id }}</td>
27+                        <td><a href="{% url 'show' pk=board.id %}">{{ board.title }}</a></td>
28+                        <td>{{ board.created_at }}</td>
29+                        <td>{{ board.updated_at }}</td>
30+                    </tr>
31+                {% endfor %}
32+            </tbody>
33+        </table>
34+        <a href="{% url 'new' %}" class="btn btn-primary">新規投稿作成</a>
35+    </section>
36+{% endblock %}

全ての投稿を一覧表示するためのindex.htmlを新しく作成しました。

  • {% for board in boards %}: views.pyから渡された投稿データのリスト (boards) を1つずつ取り出し、繰り返し処理を行います。
  • {{ board.id }}, {{ board.title }}: ループで取り出した各投稿オブジェクトのプロパティ(IDやタイトルなど)を表示しています。
  • <a href="{% url 'show' pk=board.id %}">: 各投稿のタイトルをクリックすると、その投稿の詳細ページ (show) に遷移するようにリンクを設定しています。
  • {% endfor %}: forループの終わりを示します。
  • <a href="{% url 'new' %}">: 新規投稿ページへ遷移するためのリンクです。

変更点: 新規投稿ページのテンプレート作成

/templates/new.html
1+{% extends 'base.html' %}
2+
3+{% block title %}新規投稿画面{% endblock %}
4+
5+{% block content %}
6+    <section>
7+        <h2>新規投稿画面</h2>
8+        <form action="{% url 'create' %}" method="post" class="mt-4">
9+            {% csrf_token %}
10+            <div class="mb-3">
11+                <label for="{{ form.title.id_for_label }}" class="form-label">タイトル</label></br>
12+                {{ form.title }}
13+            </div>
14+            <div class="mb-3">
15+                <label for="{{ form.content.id_for_label }}" class="form-label">内容</label></br>
16+                {{ form.content }}
17+            </div>
18+            <a href="{% url 'index' %}" class="btn btn-primary">一覧に戻る</a>
19+            <button type="submit" class="btn btn-success">投稿する</button>
20+        </form>
21+    </section>
22+{% endblock %}

新しく投稿を作成するための画面 (new.html) を新しく作成しました。基本的な構造は編集画面 (edit.html) と似ていますが、フォームの送信先がaction="{% url 'create' %}"となっており、新規作成処理を行うビューを呼び出す点が異なります。

変更点: 投稿詳細ページのテンプレート作成

/templates/show.html
1+{% extends 'base.html' %}
2+
3+{% block title %}{{ board.title }}の詳細画面{% endblock %}
4+
5+{% block content %}
6+    <section>
7+        <h2>{{ board.title }}の詳細画面</h2>
8+        <p>{{ board.content }}</p>
9+        <p>
10+            作成日時: {{ board.created_at }}</br>
11+            更新日時: {{ board.updated_at }}
12+        </p>
13+        <a href="{% url 'index' %}" class="btn btn-primary">一覧に戻る</a>
14+        <a href="{% url 'edit' pk=board.id %}" class="btn btn-success">編集する</a>
15+        <form action="{% url 'delete' pk=board.pk %}" method="post" class="d-inline">
16+            {% csrf_token %}
17+            <input type="hidden" name="action" value="delete">
18+            <button type="submit" class="btn btn-danger">削除する</button>
19+        </form>
20+    </section>
21+{% endblock %}

個別の投稿内容を詳しく表示するためのshow.htmlを新しく作成しました。

  • {{ board.title }}, {{ board.content }}など: views.pyから渡された特定の投稿データ (board) の内容を表示しています。
  • 「編集する」ボタンと「削除する」ボタンが配置されています。
  • 削除ボタンは<form>タグで囲まれています。これは、安全のために削除処理をPOSTリクエストで行うためです。ボタンをクリックすると、deleteという名前のURLにPOSTリクエストが送信され、views.pydelete関数が実行されます。

おわりに

おわりに、Djangoを使って簡単な掲示板アプリを作りながら、Web開発の基本となるCRUD機能を一通り実装しました。models.pyでデータの設計図を定義し、views.pyでデータの作成・表示・更新・削除といった処理を記述しました。そしてtemplatesに作成したHTMLファイルでは、ビューから渡されたデータを受け取り、投稿の一覧や詳細画面を表示しました。この記事を通して、Djangoのモデル、ビュー、テンプレートが連携してアプリケーションが動作する、という基本的な仕組みを実践的に学べたはずです。

関連コンテンツ

関連IT用語