Django模板与表单

Web开发专题 · 掌握Django的模板渲染与表单处理

专题:Python Web开发系统学习

关键词:Python, Web开发, Django模板, 模板继承, ModelForm, 表单验证, 自定义标签, Django表单

一、Django模板系统

Django的模板系统(Template System)是其"模型-模板-视图"(MTV)架构中的核心组件之一。它将页面的表现逻辑与业务逻辑分离,让开发者能够以声明式的方式生成动态HTML内容。Django模板使用自己专属的模板语言(Django Template Language, DTL),语法简洁且功能强大。

1.1 模板配置

在Django项目的 settings.py 中,通过 TEMPLATES 配置项对模板引擎进行全局设置。这是Django 1.8之后引入的统一模板配置入口:

TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], # 全局模板目录 'APP_DIRS': True, # 是否在每个app中查找templates子目录 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]

DIRS 是一个列表,用于指定除每个应用内部的 templates 目录之外的额外模板搜索路径。通常将项目级别的公共模板(如 base.html、导航栏、页脚等)放在此目录中。APP_DIRS 设置为 True 时,Django会自动在每个已安装的应用目录下查找 templates 子文件夹。两者配合使用,既支持全局模板复用,又允许每个应用维护自己的私有模板。

模板查找的顺序是:首先在 DIRS 指定的目录中依次查找,如果未找到,再在 APP_DIRS 启用的各个应用的 templates 目录中按 INSTALLED_APPS 的顺序查找。因此需要注意模板命名的唯一性——不同应用中同名的模板文件可能产生覆盖。

1.2 模板变量

模板变量使用双花括号语法 {{ variable }} 来输出视图传递过来的数据。变量名可以包含字母、数字和下划线,但不能以数字开头。Django支持点号(.)查找机制,可以对变量进行属性访问、字典键查找和列表索引操作:

{{ name }} {# 输出字符串变量 #} {{ user.username }} {# 访问对象的属性 #} {{ user.profile.bio }} {# 链式属性访问 #} {{ article_list.0 }} {# 列表第一个元素 #} {{ article_list.0.title }} {# 列表第一个元素的title属性 #} {{ data.key }} {# 字典键查找 #} {{ items|length }} {# 结合过滤器使用 #}

点号查找的执行顺序是:字典键查找 → 属性或方法查找 → 列表索引查找。如果最终未能找到对应值,Django不会直接抛出错误,而是使用 TEMPLATE_STRING_IF_INVALID 配置的值(默认为空字符串)。

1.3 模板标签

模板标签使用 {% tag %} 语法,用于执行控制流逻辑、循环、加载外部内容等操作。标签与变量不同,它不直接输出值,而是执行某种操作或控制模板的渲染流程。大部分标签需要闭合标记:

{% if user.is_authenticated %} <p>欢迎回来,{{ user.username }}!</p> {% endif %} {% for article in articles %} <h2>{{ article.title }}</h2> {% endfor %} {% url 'article_detail' article.id %}

1.4 过滤器

过滤器用于在变量输出前对其进行转换或格式化,使用管道符 | 连接。过滤器可以链式调用,也可以传递参数。Django内置了数十个常用过滤器:

{{ title|lower }} {# 全部转为小写 #} {{ title|upper }} {# 全部转为大写 #} {{ article.created_at|date:"Y-m-d H:i" }} {# 日期格式化 #} {{ content|truncatewords:30 }} {# 截断至30个单词 #} {{ content|linebreaks }} {# 将\n转换为<br>和<p> #} {{ price|floatformat:2 }} {# 浮点数格式化,保留2位小数 #} {{ bio|default:"这个用户很懒,什么都没写" }} {# 默认值 #} {{ html_content|safe }} {# 标记为安全字符串,不转义 #} {{ items|length }} {# 获取序列长度 #} {{ items|join:", " }} {# 用逗号连接列表 #}

需要特别注意的是 |safe 过滤器。出于安全考虑,Django默认会对所有变量输出进行HTML转义(例如将 < 转为 &lt;)。当确信内容是安全可信的HTML时,使用 |safe 可以关闭转义。此外,可以使用 {% autoescape off %} 标签块临时关闭某段模板内容的转义。

1.5 模板注释

Django模板支持单行注释 {# 注释内容 #} 和多行注释 {% comment %}...{% endcomment %}。注释中的内容不会出现在渲染后的HTML中,适用于在模板中添加开发说明或临时禁用某段代码:

{# 这是单行注释,不会出现在HTML中 #} {% comment "可选的理由说明" %} <div> <p>这段内容被注释掉了,不会渲染</p> </div> {% endcomment %}

二、模板继承与包含

模板继承是Django模板系统最强大的特性之一。它允许创建一个包含通用结构和元素的"骨架"基础模板,然后让子模板继承这个骨架并填充各自特有的区块内容。这种机制极大地提高了代码复用率,减少了重复劳动。

2.1 基础模板与 extends

基础模板使用 {% block %} 标签定义可被子模板覆盖的区域。子模板在文件开头使用 {% extends "base.html" %} 声明继承关系,然后重新定义同名的 block 内容:

{# ===== base.html ===== #} <!DOCTYPE html> <html> <head> <title>{% block title %}默认标题{% endblock %}</title> <link rel="stylesheet" href="style.css"> {% block extra_head %}{% endblock %} </head> <body> <header> <h1>我的网站</h1> <nav>{% block nav %}{% endblock %}</nav> </header> <main> {% block content %} <p>这里是默认内容</p> {% endblock %} </main> <footer>{% block footer %}© 2026 版权所有{% endblock %}</footer> </body> </html> {# ===== article_list.html ===== #} {% extends "base.html" %} {% block title %}文章列表 - 我的网站{% endblock %} {% block content %} <h2>最新文章</h2> {% for article in articles %} <div class="article-item"> <h3>{{ article.title }}</h3> <p>{{ article.summary }}</p> </div> {% endfor %} {% endblock %}

{% extends %} 必须是子模板中的第一个模板标签,否则Django将无法正确解析继承关系。一个子模板只能继承一个父模板,不支持多重继承。但可以通过多层继承链实现更灵活的结构:base.html → section_base.html → specific_page.html。

2.2 block.super 保留父块内容

当子模板需要保留父模板块中原有内容的基础上追加新内容时,可以使用 {{ block.super }} 变量。这个变量会渲染出父模板中对应 block 的内容:

{# 在子模板中保留父模板footer内容并追加 #} {% block footer %} {{ block.super }} <p><a href="/about/">关于我们</a></p> {% endblock %}

这在需要扩展而非完全覆盖父模板区块时非常有用,例如在 extra_head 区块中添加额外的CSS或JS文件,同时保留父模板中已有的资源引用。

2.3 包含标签与 include

{% include %} 标签允许将一个独立的模板文件嵌入到当前模板中。被包含的模板会使用当前模板的上下文进行渲染(除非使用 only 关键字隔离上下文):

{# ===== article_list.html ===== #} {% for article in articles %} {% include "article_item.html" %} {% empty %} <p>暂无文章</p> {% endfor %} {# ===== article_item.html ===== #} <div class="article-card"> <h3><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h3> <p class="meta">{{ article.created_at|date:"Y-m-d" }} | {{ article.author }}</p> <p>{{ article.summary|truncatewords:50 }}</p> </div>

使用 {% include "template_name" with key=value only %} 可以限制只传递特定变量到被包含的模板,避免变量名冲突或意外泄漏上下文。包含标签适合封装可复用的UI组件,如分页器、侧边栏小工具、评论区块等。

2.4 模板加载机制

Django的模板加载遵循一套明确的搜索规则。当调用 render(request, 'template_name.html', context) 时,Django的TemplateLoader按照以下顺序查找模板文件:

为了提高性能,模板加载器会缓存已解析的模板对象(当 DEBUG=False 时)。在开发环境中(DEBUG=True),每次请求都会重新加载模板文件以便于调试。此外,可以通过编写自定义的模板加载器(继承 django.template.loaders.base.Loader)来从数据库或其他存储后端加载模板。

三、内置模板标签

Django内置了丰富的模板标签,涵盖循环、条件判断、URL处理、静态文件管理、国际化等各个方面。熟练掌握这些内置标签是高效使用Django模板的基础。

3.1 循环与条件标签

{# {% for %} 循环 #} {% for item in item_list %} <li>{{ forloop.counter }}. {{ item.name }}</li> {% empty %} <li>列表为空</li> {% endfor %} {# forloop 内置变量 #} {{ forloop.counter }} {# 从1开始的循环计数 #} {{ forloop.counter0 }} {# 从0开始的循环计数 #} {{ forloop.first }} {# 是否第一次迭代 #} {{ forloop.last }} {# 是否最后一次迭代 #} {{ forloop.revcounter }} {# 剩余迭代次数(从总数到1) #} {# {% if %} 条件判断 #} {% if user.is_authenticated and user.is_active %} <p>欢迎,{{ user.username }}</p> {% elif user.is_anonymous %} <p>请先登录</p> {% else %} <p>账号异常</p> {% endif %}

{% with %} 标签用于在模板中创建临时变量,减少重复的复杂表达式计算:

{% with total=items|length %} <p>共 {{ total }} 条记录</p> {% if total > 100 %} <p>数据量较大,建议分页查看</p> {% endif %} {% endwith %}

3.2 URL 标签

{% url %} 标签根据视图函数的名称或URL模式的命名空间生成对应的绝对路径。这种方式避免了在模板中硬编码URL,当路由发生变化时只需修改 urls.py 而无需逐个修改模板:

{# 无参数 #} <a href="{% url 'home' %}">首页</a> {# 位置参数 #} <a href="{% url 'article_detail' article.id %}">详情</a> {# 关键字参数 #} <a href="{% url 'article_detail' pk=article.id %}">详情</a> {# 命名空间 #} <a href="{% url 'blog:article_detail' article.id %}">详情</a> {# 获取URL字符串并存入变量 #} {% url 'article_edit' article.id as edit_url %} <a href="{{ edit_url }}">编辑</a>

3.3 静态文件标签

{% static %} 标签用于生成静态文件的URL。在模板中使用此标签前,需要先执行 {% load static %} 加载 static 标签库:

{% load static %} <link rel="stylesheet" href="{% static 'css/style.css' %}"> <script src="{% static 'js/app.js' %}"></script> <img src="{% static 'images/logo.png' %}" alt="Logo">

静态文件的URL前缀由 STATIC_URL 配置项决定(通常为 /static/)。在生产环境中,配合 django.contrib.staticfiles 应用和 collectstatic 命令,可以将所有静态文件聚合到单一目录以便于Web服务器(如Nginx)高效托管。

3.4 国际化标签

Django内置了完善的国际化(i18n)支持,模板中使用 {% trans %}{% blocktrans %} 标签来标记待翻译的字符串:

{% load i18n %} {# 简单字符串翻译 #} <h1>{% trans "Welcome to our site" %}</h1> {# 带变量的翻译 #} {% blocktrans with name=user.username %} Hello, {{ name }}! You have {{ count }} messages. {% endblocktrans %} {# 指定目标语言 #} {% language "zh-hans" %} {% trans "Hello" %} {% endlanguage %}

国际化的完整工作流程包括:在代码和模板中使用翻译标记 → 运行 makemessages 生成 .po 文件 → 翻译其中的字符串 → 运行 compilemessages 编译为 .mo 文件 → 在 settings.py 中配置 LOCALE_PATHSLANGUAGES

3.5 CSRF Token 标签

为了防止跨站请求伪造(CSRF)攻击,Django要求所有使用POST方法的表单必须包含 {% csrf_token %} 标签。该标签会在表单中生成一个隐藏的 input 字段,其值为一个随机生成的token:

<form method="post" action="{% url 'login' %}"> {% csrf_token %} <input type="text" name="username" placeholder="用户名"> <input type="password" name="password" placeholder="密码"> <button type="submit">登录</button> </form>

Django在服务端验证提交的CSRF token是否与当前会话中存储的token一致。如果验证失败,将返回403错误。CSRF保护默认是开启的,通过 django.middleware.csrf.CsrfViewMiddleware 中间件实现。如果需要关闭特定视图的CSRF保护(如构建API时),可以使用 @csrf_exempt 装饰器。

四、自定义模板标签与过滤器

当Django内置的模板标签和过滤器无法满足需求时,可以通过创建自定义标签和过滤器来扩展模板语言的功能。自定义标签和过滤器被组织在Python模块中,存放在每个应用的 templatetags 目录下。

4.1 标签库注册与目录结构

自定义标签库需要遵循特定的目录结构。在每个应用的目录中创建 templatetags 包(包含 __init__.py 文件),然后在此包中创建模块文件:

blog/ ├── __init__.py ├── models.py ├── views.py ├── templatetags/ │ ├── __init__.py │ └── blog_extras.py # 自定义标签/过滤器 └── templates/ └── blog/ └── article_list.html

在模板中使用 {% load blog_extras %} 即可加载自定义标签库中的所有标签和过滤器。

4.2 自定义过滤器

自定义过滤器本质上是一个接收一个或两个参数的Python函数,通过 @register.filter 装饰器注册:

# blog/templatetags/blog_extras.py from django import template from django.utils.html import format_html register = template.Library() @register.filter def truncate_chars(value, max_length): """按字符数截断字符串,超出部分用...代替""" if len(value) <= max_length: return value return value[:max_length] + '...' @register.filter(name='format_date') def format_date(value): """格式化日期为友好的中文格式""" return value.strftime('%Y年%m月%d日') @register.filter(is_safe=True) def rich_text(value): """安全地将Markdown风格文本转为HTML""" from markdown import markdown return markdown(value) # 在模板中使用 {% load blog_extras %} <p>{{ article.content|truncate_chars:100 }}</p> <p>发布于:{{ article.created_at|format_date }}</p>

自定义过滤器的 @register.filter 装饰器支持多个参数:name(模板中使用的名称,默认为函数名)、is_safe(标记结果为安全HTML,不转义)、needs_autoescape(是否需要自动转义上下文信息)。

4.3 简单标签

简单标签使用 @register.simple_tag 装饰器,可以接受任意数量的参数,并将处理结果作为字符串返回。适合生成非HTML片段的动态数据:

@register.simple_tag def total_views(user): """计算用户所有文章的总阅读量""" return Article.objects.filter(author=user).aggregate( total=Sum('views') )['total'] or 0 @register.simple_tag(name='query_url', takes_context=True) def query_url(context, key, value): """在现有GET参数基础上追加或覆盖参数""" request = context['request'] params = request.GET.copy() params[key] = value return '?' + params.urlencode() # 在模板中使用 {% load blog_extras %} <p>总阅读量:{% total_views request.user %}</p> <a href="{% query_url 'page' 2 %}">下一页</a>

takes_context=True 参数使标签能够访问模板上下文(包括 requestuser 等由上下文处理器注入的变量)。此时标签函数的第一个参数必须是 context

4.4 包含标签

包含标签使用 @register.inclusion_tag 装饰器,它返回一个上下文字典并用指定的模板来渲染HTML片段。包含标签非常适合封装那些需要反复出现的复杂UI组件:

@register.inclusion_tag('blog/pagination.html') def pagination(page_obj, nearby=2): """生成分页导航组件""" page_range = [] num_pages = page_obj.paginator.num_pages current = page_obj.number start = max(1, current - nearby) end = min(num_pages, current + nearby) if start > 1: page_range.append(1) if start > 2: page_range.append('...') for i in range(start, end + 1): page_range.append(i) if end < num_pages: if end < num_pages - 1: page_range.append('...') page_range.append(num_pages) return {'page_range': page_range, 'current': current} # blog/pagination.html(包含标签使用的模板) <nav class="pagination"> {% for page in page_range %} {% if page == '...' %} <span>...</span> {% elif page == current %} <span class="current">{{ page }}</span> {% else %} <a href="?page={{ page }}">{{ page }}</a> {% endif %} {% endfor %} </nav> # 在模板中使用 {% load blog_extras %} {% pagination articles 3 %}

五、Django表单系统

Django的表单系统提供了一套完整的工具链,用于处理HTML表单的生成、数据验证、错误处理和清洗。它将表单的渲染逻辑与验证逻辑统一封装在Form类中,大大简化了Web开发中常见的表单处理任务。

5.1 forms.Form 基础表单

forms.Form 是所有手动定义表单的基类。开发者通过声明式的方式定义表单字段,Django自动处理字段的验证、清洗和渲染:

# forms.py from django import forms from django.core.validators import RegexValidator class RegistrationForm(forms.Form): username = forms.CharField( label='用户名', max_length=30, min_length=3, required=True, help_text='3-30个字符,字母、数字和下划线', validators=[RegexValidator(r'^[a-zA-Z0-9_]+$', '只能包含字母、数字和下划线')], widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入用户名'}) ) email = forms.EmailField( label='电子邮箱', required=True, widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': '请输入邮箱'}) ) age = forms.IntegerField( label='年龄', required=True, min_value=1, max_value=150, widget=forms.NumberInput(attrs={'class': 'form-control'}) ) gender = forms.ChoiceField( label='性别', choices=[('M', '男'), ('F', '女'), ('O', '其他')], widget=forms.RadioSelect ) bio = forms.CharField( label='个人简介', required=False, max_length=500, widget=forms.Textarea(attrs={'rows': 4, 'class': 'form-control'}) ) agree_terms = forms.BooleanField( label='同意服务条款', required=True, error_messages={'required': '请先同意服务条款'} )

5.2 常用表单字段

字段类说明默认Widget
CharField字符串字段,支持 max_length、min_lengthTextInput
IntegerField整数字段,支持 max_value、min_valueNumberInput
FloatField浮点数字段NumberInput
DecimalField十进制数字段,支持 max_digits、decimal_placesNumberInput
BooleanField布尔字段(复选框)CheckboxInput
ChoiceField单选下拉框,需要提供 choices 参数Select
MultipleChoiceField多选字段SelectMultiple
DateField日期字段,自动解析为 datetime.dateDateInput
DateTimeField日期时间字段DateTimeInput
EmailField邮箱字段,自带邮箱格式验证EmailInput
URLFieldURL字段,自带URL格式验证URLInput
FileField文件上传字段ClearableFileInput
ImageField图片上传字段(需安装Pillow)ClearableFileInput

5.3 字段参数详解

每个表单字段类型都接受一组通用参数,用于控制字段的行为和外观:

5.4 表单验证

Django表单的验证流程是分层进行的,理解这一流程对于正确实现自定义验证逻辑至关重要:

# forms.py — 表单验证示例 from django import forms from django.core.exceptions import ValidationError class RegistrationForm(forms.Form): password = forms.CharField( label='密码', min_length=8, widget=forms.PasswordInput ) confirm_password = forms.CharField( label='确认密码', widget=forms.PasswordInput ) # 阶段1:字段级验证 — clean_<fieldname>() def clean_password(self): password = self.cleaned_data.get('password') # 自定义强度检查 if not any(char.isdigit() for char in password): raise ValidationError('密码必须包含至少一个数字') if not any(char.isupper() for char in password): raise ValidationError('密码必须包含至少一个大写字母') return password # 阶段2:表单级验证 — clean() def clean(self): cleaned_data = super().clean() password = cleaned_data.get('password') confirm = cleaned_data.get('confirm_password') if password and confirm and password != confirm: raise ValidationError('两次输入的密码不一致') return cleaned_data # 阶段3:自定义验证器(在字段定义时指定) phone = forms.CharField( label='手机号', validators=[RegexValidator( r'^1[3-9]\d{9}$', '请输入有效的11位手机号码' )] )

完整的验证流程是:先进行字段内置类型验证(如DateField检查日期格式)→ 调用 clean_<fieldname>() 方法 → 调用字段的 validators 列表中的验证器 → 最后调用表单的 clean() 方法进行跨字段验证。验证通过的数据存放于 cleaned_data 字典中。

最佳实践:字段级验证(clean_<field>)用于验证单个字段的约束条件,表单级验证(clean)用于验证字段之间的依赖关系(如密码一致性检查)。自定义验证器(Validator)适用于可复用的验证逻辑(如手机号格式、邮箱格式等)。

5.5 在模板中渲染表单

Django提供了多种预定义的表单渲染方式,同时也支持完全手动的字段渲染以实现定制化布局:

{# 方式一:快速渲染 — as_p / as_table / as_ul #} <form method="post"> {% csrf_token %} {{ form.as_p }} {# 每个字段用<p>包裹 #} {{ form.as_table }} {# 每个字段用<tr>包裹 #} {{ form.as_ul }} {# 每个字段用<li>包裹 #} <button type="submit">提交</button> </form> {# 方式二:手动渲染(推荐,更灵活) #} <form method="post" novalidate> {% csrf_token %} <div class="form-group"> {{ form.username.label_tag }} {{ form.username }} {% if form.username.errors %} <ul class="error-list"> {% for error in form.username.errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} {% if form.username.help_text %} <small class="help-text">{{ form.username.help_text }}</small> {% endif %} </div> <div class="form-group"> {{ form.email.label_tag }} {{ form.email }} {{ form.email.errors }} </div> <button type="submit">注册</button> </form> {# 方式三:循环渲染(适用于字段统一样式) #} <form method="post"> {% csrf_token %} {% for field in form %} <div class="field-wrapper"> {{ field.label_tag }} {{ field }} {% if field.help_text %} <p class="help">{{ field.help_text }}</p> {% endif %} {% for error in field.errors %} <p class="error">{{ error }}</p> {% endfor %} </div> {% endfor %} <button type="submit">提交</button> </form>

手动渲染方式不仅能够精确控制表单的HTML结构和CSS类,还能在每个字段周围自由添加交互元素、错误提示和帮助文本,是实现复杂UI设计的最佳选择。

六、ModelForm

ModelForm是Django表单系统中极为重要的组成部分,它能够根据数据库模型(Model)自动生成对应的表单字段,并处理数据持久化。ModelForm极大地减少了CRUD操作中的重复代码。

6.1 从模型自动生成表单

通过继承 forms.ModelForm 并在内部 Meta 类中指定模型,ModelForm可以自动分析模型的字段定义并生成匹配的表单字段:

# models.py from django.db import models class Article(models.Model): title = models.CharField('标题', max_length=200) slug = models.SlugField('URL别名', unique=True) category = models.ForeignKey('Category', on_delete=models.CASCADE, verbose_name='分类') tags = models.ManyToManyField('Tag', verbose_name='标签') content = models.TextField('正文') status = models.CharField('状态', max_length=10, choices=[('draft', '草稿'), ('published', '已发布')], default='draft') views = models.IntegerField('阅读量', default=0) created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) # forms.py from django import forms from .models import Article class ArticleForm(forms.ModelForm): class Meta: model = Article fields = ['title', 'slug', 'category', 'tags', 'content', 'status'] # 或者用 exclude 排除不需要的字段 # exclude = ['views', 'created_at', 'updated_at'] widgets = { 'content': forms.Textarea(attrs={'rows': 10, 'class': 'editor'}), 'tags': forms.CheckboxSelectMultiple, } labels = { 'slug': 'URL别名', 'content': '文章正文', } help_texts = { 'slug': '用于生成URL的唯一标识,只能包含字母、数字和连字符', } error_messages = { 'slug': { 'unique': '此URL别名已被使用,请更换一个', }, }

6.2 fields 与 exclude

ModelForm的 Meta 类中,fieldsexclude 是两个互斥的属性,用于控制表单包含哪些模型字段:

推荐始终使用 fields 白名单模式而非 exclude 黑名单模式。这遵循了"显式优于隐式"的Python哲学,能够避免因模型字段变更而导致意外暴露敏感字段的安全风险。

6.3 save() 方法

ModelForm 的 save() 方法封装了模型实例的创建或更新逻辑。当传入 commit=False 参数时,save() 会返回模型实例但不提交到数据库,这对需要在保存前进行额外处理的情况非常有用:

# views.py from django.shortcuts import render, redirect from .forms import ArticleForm def article_create(request): if request.method == 'POST': form = ArticleForm(request.POST) if form.is_valid(): # 基础用法:直接保存 article = form.save() # 进阶用法:commit=False,手动处理后再保存 # article = form.save(commit=False) # article.author = request.user # 设置外键或额外字段 # article.save() # 保存模型实例 # form.save_m2m() # 保存多对多关系 return redirect('article_detail', pk=article.pk) else: form = ArticleForm() return render(request, 'blog/article_form.html', {'form': form}) def article_update(request, pk): article = Article.objects.get(pk=pk) if request.method == 'POST': form = ArticleForm(request.POST, instance=article) if form.is_valid(): form.save() return redirect('article_detail', pk=article.pk) else: form = ArticleForm(instance=article) return render(request, 'blog/article_form.html', {'form': form})

save() 被调用时,如果表单没有 instance 参数,Django会创建一个新的模型实例(INSERT);如果传入了 instance,则更新该实例(UPDATE)。save(commit=False) 在需要设置模型自动字段(如 created_by 外键)时尤为重要。

关键提醒:使用 save(commit=False) 时,如果模型有多对多字段(ManyToManyField),必须在手动调用 instance.save() 之后再调用 form.save_m2m(),否则多对多关系不会被持久化。如果 commit=True(默认值),Django会自动处理多对多关系。

6.4 表单与文件上传

处理包含文件上传的表单时,需要在表单定义中使用 FileFieldImageField,同时在视图的 render 函数中设置 enctype="multipart/form-data",视图函数需要接收 request.FILES

# models.py class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) avatar = models.ImageField('头像', upload_to='avatars/') resume = models.FileField('简历', upload_to='resumes/', blank=True) # forms.py class ProfileForm(forms.ModelForm): class Meta: model = Profile fields = ['avatar', 'resume'] # views.py def profile_edit(request): profile = request.user.profile if request.method == 'POST': form = ProfileForm(request.POST, request.FILES, instance=profile) # 注意:必须传入 request.FILES 作为第二个参数 if form.is_valid(): form.save() return redirect('profile_detail') else: form = ProfileForm(instance=profile) return render(request, 'accounts/profile_form.html', {'form': form}) {# 模板中必须设置 enctype #} <form method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form.as_p }} <button type="submit">保存</button> </form>

文件上传涉及的关键配置还包括:MEDIA_ROOT(文件存储的物理路径)、MEDIA_URL(文件访问的URL前缀)、以及 settings.py 中的文件大小限制配置。生产环境中通常使用专用的存储后端(如阿里云OSS、AWS S3)来托管用户上传的文件。

七、核心要点总结

1. 模板是MTV架构的"视图层":Django模板系统实现了表现逻辑与业务逻辑的分离,模板文件专注于HTML结构的设计,而视图函数负责数据处理和上下文准备。

2. 模板继承是代码复用的核心:通过 {% extends %}{% block %} 实现模板继承,通过 {% include %} 实现组件化复用,通过 {{ block.super }} 保留父模板内容。

3. 内置标签覆盖了绝大部分场景:循环、条件、URL反转、静态文件、国际化、CSRF保护等内置标签能够满足90%以上的模板开发需求。

4. 自定义标签满足个性化需求:过滤器(filter)、简单标签(simple_tag)、包含标签(inclusion_tag)三种形式,按需选择,将重复逻辑封装到templatetags包中。

5. 表单系统的三层验证机制:字段级验证(clean_<field>)→ 验证器列表(validators)→ 表单级验证(clean),层层递进确保数据质量。

6. ModelForm 是CRUD开发的利器:自动从模型生成表单,处理字段类型映射、数据验证和持久化。使用 fields 白名单而非 exclude,使用 save(commit=False) 处理额外逻辑。