Python 现代开发学习笔记

(十七)Django 模板、控件与页面组合

1. 沿已有模板知识读新写法

Django 模板也用 {{ }} 输出、{% %} 控制,但不是 Jinja 的同一套语法。模板不直接写 Python 函数调用,{% url %} 生成地址,{% static %} 生成静态路径,{% csrf_token %} 生成表单令牌。

<!-- 项目文件:reading_project/src/library/templates/base.html -->
{% load static %}
<!doctype html><html lang="zh-CN">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{% block title %}阅读记录站{% endblock %} · Django</title>
  <link rel="stylesheet" href="{% static 'library/style.css' %}"></head>
<body><main>
  <header class="page-header"><div><p class="eyebrow">PYTHON 3.12 / DJANGO</p>
    <a class="brand" href="{% url 'library:index' %}">阅读记录站</a><p class="muted">书架、阅读备注与管理后台。</p></div>
    <nav aria-label="账号导航">
      {% if user.is_authenticated %}
        <span>你好,{{ user.username }}</span><a href="{% url 'library:index' %}">我的书架</a>
        {% if user.is_staff %}<a href="{% url 'admin:index' %}">管理后台</a>{% endif %}
        <form method="post" action="{% url 'logout' %}">{% csrf_token %}<button class="secondary">退出登录</button></form>
      {% else %}<a href="{% url 'login' %}">登录</a><a href="{% url 'signup' %}">注册</a>{% endif %}
    </nav>
  </header>
  {% for message in messages %}<p class="notice" role="status">{{ message }}</p>{% endfor %}
  {% block content %}{% endblock %}
  <footer>Django 页面 · 用户与权限 · SQLite 持久化</footer>
</main></body></html>

user、perms 与 messages 来自上下文处理器。登录导航用 user.is_authenticated 判断,后台入口看 user.is_staff;这些显示判断帮助组织页面,实际接口仍执行权限检查。

2. BoundField 生成控件

<!-- 项目文件:reading_project/src/library/templates/_fields.html -->
<!-- BoundField 输出带转义和校验属性的控件;错误紧邻对应字段。 -->
{{ form.non_field_errors }}
{% for field in form %}
  <div class="form-field">
    <label for="{{ field.id_for_label }}">{{ field.label }}</label>
    {{ field }}
    {{ field.errors }}
    {% if field.help_text %}<div class="muted help">{{ field.help_text }}</div>{% endif %}
  </div>
{% endfor %}

迭代表单得到 BoundField,它关联字段规则、本次值、控件和错误。输出 {{ field }} 生成 <input> 或 <textarea>,用户值在控件生成时转义。id_for_label 连接标签和输入,field.errors 放在对应控件旁,整体错误单独显示。

<!-- 项目文件:reading_project/src/library/templates/library/book_form.html -->
{% extends 'base.html' %}
{% block title %}保存阅读记录{% endblock %}
{% block content %}<section class="panel form-panel"><a href="{% url 'library:index' %}">返回书架</a>
  <h1>{% if book %}修改书籍{% else %}添加书籍{% endif %}</h1>
  <form method="post">{% csrf_token %}{% include '_fields.html' %}<button type="submit">保存书籍</button></form>
</section>{% endblock %}

绑定失败的表单保留这次尝试值;未绑定编辑表单取实例初值。数据库记录与输入框值可以暂时不同,直到事务提交才合并成成功结果。

3. 详情显示标签、备注和历史

<!-- 项目文件:reading_project/src/library/templates/library/detail.html -->
{% extends 'base.html' %}
{% block title %}{{ book.title }}{% endblock %}
{% block content %}<section class="panel detail-panel">
  <a href="{% url 'library:index' %}">返回书架</a><h1>{{ book.title }}</h1>
  <p class="status">{{ book.finished|yesno:'已读,未读' }}</p><p class="muted">所属用户:{{ book.owner.username }}</p>
  <p>标签:{% for tag in book.tags.all %}{{ tag.name }} {% empty %}未设置{% endfor %}</p>
  <div class="notes">{{ book.notes|linebreaksbr }}</div>
  <p><a class="button" href="{% url 'library:edit' book.pk %}">修改书籍</a></p>
  <details class="delete-area"><summary>删除这本书</summary><p>确认后将删除这本书及其修改记录。</p>
    <form method="post" action="{% url 'library:delete' book.pk %}">{% csrf_token %}<button class="danger">确认删除</button></form></details>
  <h2 class="history-title">修改记录</h2><ul>{% for change in book.changes.all %}
    <li>{{ change.get_action_display }} · {{ change.created_at|date:'Y-m-d H:i' }}</li>{% empty %}<li>还没有修改记录。</li>{% endfor %}</ul>
</section>{% endblock %}

book.tags.all 在模板中调用无参数方法,使用视图已经预取的关联结果。yesno 把布尔值转换为指定文字,date 按格式显示时间;linebreaksbr 在转义备注文本后把换行组织成可显示的换行标记。普通书名和备注不加 safe。

视图传入模型和表单,父模板填导航与消息,BoundField 按绑定数据生成已转义控件,详情使用预取标签与修改记录生成完整 HTML

笔记正文与全部题解 · Ctrl / ⌘ K