記事一覧へ戻る 本の順番で続きを読む

Django テンプレートとフォーム - ユーザーインターフェース

Python3上級 | 2026/02/18 21:20

Django テンプレートとフォーム

テンプレートの基本

{# blog/templates/blog/base.html #}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}ブログ{% endblock %}</title>
    {% block extra_css %}{% endblock %}
</head>
<body>
    <header>
        <nav>
            <a href="{% url 'blog:post_list' %}">記事一覧</a>
            {% if user.is_authenticated %}
                <span>{{ user.username }}</span>
                <a href="{% url 'logout' %}">ログアウト</a>
            {% else %}
                <a href="{% url 'login' %}">ログイン</a>
            {% endif %}
        </nav>
    </header>
    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>

テンプレート継承

{# blog/templates/blog/post_list.html #}
{% extends 'blog/base.html' %}

{% block title %}記事一覧{% endblock %}

{% block content %}
<h1>記事一覧</h1>
{% for post in posts %}
    <article>
        <h2><a href="{% url 'blog:post_detail' slug=post.slug %}">{{ post.title }}</a></h2>
        <p>{{ post.body|truncatewords:30 }}</p>
        <time>{{ post.created_at|date:'Y年m月d日' }}</time>
    </article>
{% empty %}
    <p>記事がありません。</p>
{% endfor %}

{# ページネーション #}
{% if page_obj.has_previous %}
    <a href="?page={{ page_obj.previous_page_number }}">前のページ</a>
{% endif %}
<span>{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
    <a href="?page={{ page_obj.next_page_number }}">次のページ</a>
{% endif %}
{% endblock %}

テンプレートタグとフィルター

{# 変数の表示 #}
{{ post.title }}
{{ post.body|linebreaksbr }}     {# 改行をbrタグに変換 #}
{{ post.body|truncatechars:100 }} {# 100文字で切り詰め #}
{{ post.created_at|date:'Y/m/d H:i' }}
{{ price|floatformat:0 }}         {# 小数点以下を丸める #}
{{ items|length }}                {# 要素数 #}
{{ text|default:'未設定' }}        {# Noneや空文字の場合のデフォルト #}

{# 条件分岐 #}
{% if post.status == 'published' %}
    <span class="badge">公開中</span>
{% elif post.status == 'draft' %}
    <span class="badge">下書き</span>
{% endif %}

{# ループ #}
{% for post in posts %}
    {{ forloop.counter }}. {{ post.title }}
{% endfor %}

{# インクルード #}
{% include 'blog/_sidebar.html' with recent_posts=posts %}

{# 静的ファイル #}
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<img src="{% static 'images/logo.png' %}" alt="ロゴ">

フォームクラス

# blog/forms.py
from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'body', 'category', 'status']
        widgets = {
            'title': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'タイトルを入力'
            }),
            'body': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 10
            }),
        }

    def clean_title(self):
        title = self.cleaned_data['title']
        if len(title) < 3:
            raise forms.ValidationError('タイトルは3文字以上必要です')
        return title

フォームの表示

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}

    {% for field in form %}
    <div class="form-group">
        <label for="{{ field.id_for_label }}">{{ field.label }}</label>
        {{ field }}
        {% if field.errors %}
            {% for error in field.errors %}
                <span class="error">{{ error }}</span>
            {% endfor %}
        {% endif %}
    </div>
    {% endfor %}

    <button type="submit">保存</button>
</form>

まとめ

  • テンプレート継承(extends/block)でレイアウトを共通化
  • {% url %} でURL逆引き、{% static %} で静的ファイル参照
  • {% csrf_token %} でCSRF対策(POSTフォームに必須)
  • ModelForm でモデルと連動したフォームを簡単に作成
  • フィルター(date, truncatewords等)でテンプレート内のデータ整形