Django 模板语言提供多种内置标签和过滤器,满足应用的展示逻辑需求。如果核心模板原语不能覆盖所需功能,可以用 Python 定义自定义标签和过滤器,扩展模板引擎,再通过 {% load %} 在模板中使用。
代码布局
最常见的做法是把自定义标签和过滤器放在 Django 应用中。如果与现有应用有关,应放入该应用;否则可创建新应用。将应用加入 INSTALLED_APPS 后,下述约定位置中的标签就可以在模板中加载。
在 models.py、views.py 的同级创建 templatetags 目录,并加入 __init__.py,确保它是 Python 包。添加模块后,需要重启开发服务器,它不会因此自动重启。
标签和过滤器位于该目录的模块内。模块文件名就是后续加载名称,应避免与其他应用冲突。例如:
polls/
__init__.py
models.py
templatetags/
__init__.py
poll_extras.py
views.py
模板中使用:
{% load poll_extras %}
应用必须位于 INSTALLED_APPS,load 才能工作。这是一项安全特性:同一主机可以承载多个模板库的 Python 代码,而不必让每个 Django 安装都能访问所有库。
templatetags 中模块数量没有限制。load 加载的是 Python 模块名对应的标签/过滤器,而不是应用名称。
合法标签库必须有模块级变量 register,它是 template.Library 实例,用来注册全部标签和过滤器:
from django import template
register = template.Library()
也可通过 DjangoTemplates 的 libraries 参数注册模块,便于用不同于模块名的标签加载名称,或不安装应用就注册标签。
内置过滤器和标签源码提供大量示例,分别位于 django/template/defaultfilters.py 和 django/template/defaulttags.py。load 的详细行为见官方文档。
编写自定义过滤器
过滤器是接受一个或两个参数的 Python 函数:输入变量值(不一定是字符串),以及可选的参数值(可有默认值)。例如 {{ var|foo:"bar" }} 会把 var 和字符串 bar 传给 foo。
模板语言没有异常处理,过滤器抛出的异常会暴露为服务器错误。因此,有合理回退值时应避免异常;但若输入表明模板有明确错误,抛出异常也可能比隐藏问题的静默失败更合适。
示例定义:
def cut(value, arg):
"""Removes all values of arg from the given string"""
return value.replace(arg, "")
用法:
{{ somevariable|cut:"0" }}
大多数过滤器不接收额外参数,此时从函数定义中省略:
def lower(value): # Only one argument.
"""Converts a string into all lowercase"""
return value.lower()
注册过滤器
通过 Library 实例注册,使 Django 模板语言能使用它:
register.filter("cut", cut)
register.filter("lower", lower)
Library.filter() 接收过滤器名字符串和 Python 函数对象,第二项不能是函数名称字符串。也可以使用装饰器:
@register.filter(name="cut")
def cut(value, arg):
return value.replace(arg, "")
@register.filter
def lower(value):
return value.lower()
省略 name 时,Django 使用函数名。注册还支持 is_safe、needs_autoescape、expects_localtime 三个关键字参数,后面分别介绍。
需要字符串的过滤器
如果第一个参数只接受字符串,使用 stringfilter 装饰器,先把对象转换为字符串:
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter
@stringfilter
def lower(value):
return value.lower()
这样传入整数不会因没有 lower 方法而产生 AttributeError。
过滤器与自动转义
编写过滤器时,应考虑 Django 的自动转义。模板中存在两种字符串:
- 普通 Python 字符串:开启自动转义时输出会转义,关闭时原样呈现。
- 安全字符串:已标记无需进一步输出转义,必要转义已经完成。通常用于希望客户端按原始 HTML 解释的输出。
安全字符串类型为 SafeString,可以这样检测:
from django.utils.safestring import SafeString
if isinstance(value, SafeString):
# Do something with the "safe" string.
...
过滤器代码有两种情况。
第一种:不引入输入原本没有的不安全 HTML 字符。 这些字符包括 <、>、单引号、双引号和 &。可让 Django 负责自动转义,注册时设置 is_safe=True:
@register.filter(is_safe=True)
def myfilter(value):
return value
这表示安全输入经该过滤器后仍安全;不安全输入则在必要时自动转义。可理解为过滤器不会引入不安全 HTML。
许多普通字符串操作会将 SafeData 变回普通 str。逐个捕获很困难,因此 Django 在过滤器完成后修复这一属性。比如向输入末尾追加 xx,不引入危险字符,应标记安全:
@register.filter(is_safe=True)
def add_xx(value):
return "%s xx" % value
自动转义开启时,输入未标记安全,输出就会转义。is_safe 默认 False,无需时可省略。
判断时要小心:删除字符可能留下未闭合 HTML 标签或实体。例如删去 <a> 的 >,得到 <a,输出需转义;删除 & 的分号得到 &,原文指出需进一步转义。多数场景没这么复杂,但审查时应留意。
标记 is_safe 会把返回值强制转为字符串。如果应返回布尔或其他非字符串值,可能发生意外,例如 False 变成字符串 'False'。
第二种:过滤器手工完成必要转义。 引入新的 HTML 标记时,应自行处理输入,再标记输出安全,避免新增 HTML 被再次转义。使用 django.utils.safestring.mark_safe()。
仅标记安全还不够,必须保证实际安全,并根据自动转义开关处理,使模板作者可在开启或关闭模式使用。
注册时设 needs_autoescape=True,Django 会传入 autoescape 关键字参数,开启时为 True,关闭时为 False;默认不传。建议函数参数 autoescape 默认 True,让 Python 直接调用也默认安全。
例如强调字符串首字符:
from django import template
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
register = template.Library()
@register.filter(needs_autoescape=True)
def initial_letter_filter(text, autoescape=True):
first, other = text[0], text[1:]
if autoescape:
esc = conditional_escape
else:
esc = lambda x: x
result = "<strong>%s</strong>%s" % (esc(first), esc(other))
return mark_safe(result)
我们依据 autoescape 决定是否调用 conditional_escape,关闭时使用恒等函数。conditional_escape 与 escape 类似,但只转义非 SafeData 输入;SafeData 原样返回。最后将结果标记安全,保留新增 HTML。手工处理自动转义并返回安全字符串后,is_safe 不会进一步改变行为。
复用内置过滤器时避免 XSS。 当前内置过滤器默认 autoescape=True,确保正确转义;旧版默认可能为 None,复用时须显式传 True。例如组合 urlize 和 linebreaksbr:
from django.template.defaultfilters import linebreaksbr, urlize
@register.filter(needs_autoescape=True)
def urlize_and_linebreaks(text, autoescape=True):
return linebreaksbr(urlize(text, autoescape=autoescape), autoescape=autoescape)
以下两种模板用法等价:
{{ comment|urlize_and_linebreaks }}
{{ comment|urlize|linebreaksbr }}
过滤器与时区
处理 datetime 对象的过滤器通常应设置 expects_localtime=True:
@register.filter(expects_localtime=True)
def businesshours(value):
try:
return 9 <= value.hour < 17
except AttributeError:
return ""
如果首参数是感知时区的 datetime,Django 会根据模板时区转换规则,在适当时转换到当前时区后再传入。
编写自定义模板标签
标签比过滤器复杂,可以执行更广泛的操作。Django 提供多种简化工具,先介绍这些工具,再介绍需要完整底层能力时如何从头编写。
简单标签
许多标签接收字符串或模板变量,依据参数和外部信息处理后返回结果。例如 current_time 接收格式字符串,返回对应时间。
Library 的 simple_tag 接收任意参数函数,包装 render 和必要部分,并注册到模板系统:
import datetime
from django import template
register = template.Library()
@register.simple_tag
def current_time(format_string):
return datetime.datetime.now().strftime(format_string)
注意:
- 调用函数前已检查参数数量,无需重复。
- 参数外层引号已移除,收到的是普通字符串。
- 模板变量参数传入的是当前值,而不是变量对象。
模板上下文开启自动转义时,simple_tag 会对输出调用 conditional_escape,保证 HTML 正确并防止 XSS。只有完全确信安全时,才使用 mark_safe 避免额外转义。构造小 HTML 片段时,强烈建议使用 format_html 而不是 mark_safe。
需要当前上下文时,注册 takes_context=True:
@register.simple_tag(takes_context=True)
def current_time(context, format_string):
timezone = context["timezone"]
return your_get_current_time_method(timezone, format_string)
首参数必须叫 context,详细机制与 inclusion_tag 相同。可指定其他标签名:
register.simple_tag(lambda x: x - 1, name="minusone")
@register.simple_tag(name="minustwo")
def some_function(value):
return value - 2
simple_tag 支持任意数量的位置和关键字参数:
@register.simple_tag
def my_tag(a, b, *args, **kwargs):
warning = kwargs["warning"]
profile = kwargs["profile"]
...
return ...
模板参数以空格分隔,关键字用等号,必须放在位置参数之后:
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
可以通过 as 将结果保存为模板变量,再自行决定输出位置:
{% current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>
简单块标签
simple_block_tag 可以将已渲染的模板片段传给自定义函数。它与 simple_tag 类似,但额外接收 content,即标签内部已渲染内容。
例如生成图表:
from django import template
from myapp.charts import render_chart
register = template.Library()
@register.simple_block_tag
def chart(content):
return render_chart(source=content)
content 包含以下 chart 与 endchart 之间的内容:
{% chart %}
digraph G {
label = "Chart for {{ request.user }}"
A -> {B C}
}
{% endchart %}
内部其他标签与变量先渲染,调用 render_chart 前 request.user 已解析。闭合标签默认 end 加标签名,可用 end_name 自定义:
@register.simple_block_tag(end_name="endofchart")
def chart(content):
return render_chart(source=content)
相应模板:
{% chart %}
digraph G {
label = "Chart for {{ request.user }}"
A -> {B C}
}
{% endofchart %}
首参数必须叫 content,收到渲染后的字符串。传给标签的变量不会像 with 那样加入内部内容渲染上下文。
与 simple_tag 一样,它会验证参数、必要时去掉引号、适当转义输出;支持 takes_context(此时 context 为首参数,content 紧随其后)、自定义 name、任意位置与关键字参数,以及 as 保存结果。content 已由 Django 渲染,因此已经转义;输出转义规则与 simple_tag 相同。
完整示例
下面创建支持多个级别和复杂内容的消息框。文件 testapp/templatetags/testapptags.py:
from django import template
from django.utils.html import format_html
register = template.Library()
@register.simple_block_tag(takes_context=True)
def msgbox(context, content, level):
format_kwargs = {
"level": level.lower(),
"level_title": level.capitalize(),
"content": content,
"open": " open" if level.lower() == "error" else "",
"site": context.get("site", "My Site"),
}
result = """
<div class="msgbox {level}">
<details{open}>
<summary>
<strong>{level_title}</strong>: Please read for <i>{site}</i>
</summary>
<p>
{content}
</p>
</details>
</div>
"""
return format_html(result, **format_kwargs)
视图 testapp/views.py:
from django.shortcuts import render
def simpleblocktag_view(request):
return render(request, "test.html", context={"site": "Important Site"})
模板 testapp/templates/test.html:
{% extends "base.html" %}
{% load testapptags %}
{% block content %}
{% msgbox level="error" %}
Please fix all errors. Further documentation can be found at
<a href="http://example.com">Docs</a>.
{% endmsgbox %}
{% msgbox level="info" %}
More information at: <a href="http://othersite.com">Other Site</a>/
{% endmsgbox %}
{% endblock %}
原文展示的渲染输出:
<div class="msgbox error">
<details open>
<summary>
<strong>Error</strong>: Please read for <i>Important Site</i>
</summary>
<p>
Please fix all errors. Further documentation can be found at
<a href="http://example.com">Docs</a>.
</p>
</details>
</div>
<div class="msgbox info">
<details>
<summary>
<strong>Info</strong>: Please read for <i>Important Site</i>
</summary>
<p>
More information at: <a href="http://othersite.com">Other Site</a>
</p>
</details>
</div>
包含标签(inclusion tags)
另一类常见标签通过渲染其他模板展示数据。例如 Django 管理后台“新增/修改”表单底部按钮,外观相同但链接随对象变化;适合使用小模板填入当前对象信息,后台称之为 submit_row。这类称为包含标签。
为教程中的 Poll 输出选项列表,调用方式:
{% show_results poll %}
示例输出:
<ul>
<li>First choice</li>
<li>Second choice</li>
<li>Third choice</li>
</ul>
先定义接收参数并返回字典的函数。只需字典,它将作为模板片段的上下文:
def show_results(poll):
choices = poll.choice_set.all()
return {"choices": choices}
创建输出模板。模板由标签作者指定,不由使用模板的人选择:
<ul>
{% for choice in choices %}
<li> {{ choice }} </li>
{% endfor %}
</ul>
若文件叫 results.html,且位于模板加载器搜索路径,注册如下:
# Here, register is a django.template.Library instance, as before
@register.inclusion_tag("results.html")
def show_results(poll): ...
也可使用 Template 实例注册:
from django.template.loader import get_template
t = get_template("results.html")
register.inclusion_tag(t)(show_results)
如果包含标签需要大量参数,模板作者难以记忆顺序,可以设置 takes_context。此时无需必填标签参数,底层函数接收调用时的模板上下文。
例如上下文总含 home_link 与 home_title:
@register.inclusion_tag("link.html", takes_context=True)
def jump_link(context):
return {
"link": context["home_link"],
"title": context["home_title"],
}
首参数必须叫 context。link.html 可为:
Jump directly to <a href="{{ link }}">{{ title }}</a>.
加载库后,无参数调用:
{% jump_link %}
takes_context 默认 False;True 时自动传上下文,无需在标签中显式传入。这是它与前一个例子的差别。
inclusion_tag 也支持任意位置与关键字参数:
@register.inclusion_tag("my_template.html")
def my_tag(a, b, *args, **kwargs):
warning = kwargs["warning"]
profile = kwargs["profile"]
...
return ...
关键字参数仍在位置参数之后:
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
高级自定义标签
如果前述工具不足以实现功能,Django 也允许完整访问内部机制,从头构建标签。
快速概览
模板系统分编译和渲染两阶段。编译时,把原始模板文本拆成节点;每个节点是 django.template.Node 实例,有 render 方法。编译后的模板是节点列表,渲染时依次用给定上下文调用各节点 render,再拼接结果。
自定义标签需定义如何把原始标签转换为 Node(编译函数),以及 Node.render 的行为。
编写编译函数
解析器遇到标签时,调用 Python 函数,传入标签内容与解析器。该函数返回相应 Node 实例。
完整实现 current_time,接受 strftime 格式参数。在其他工作前确定语法:
<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>
解析参数并创建节点:
from django import template
def do_current_time(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, format_string = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
"%r tag requires a single argument" % token.contents.split()[0]
)
if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
raise template.TemplateSyntaxError(
"%r tag's argument should be in quotes" % tag_name
)
return CurrentTimeNode(format_string[1:-1])
注意:
- parser 是模板解析器,本例不使用。
- token.contents 是原始标签内容;本例为
'current_time "%Y-%m-%d %I:%M %p"'。 - split_contents 按空格拆分但保持引号内字符串完整;普通 split 会连引号内空格一起拆,应始终优先使用 split_contents。
- 语法错误应抛 TemplateSyntaxError,并提供有帮助的消息。
- 错误信息用 tag_name,而非写死名称,避免函数与标签名耦合;token.contents.split()[0] 即使没有参数也能得到标签名。
- 返回 CurrentTimeNode,把所需信息传给节点;format_string[1:-1] 去掉格式参数首尾引号。
- 解析机制较底层,是为速度。开发者曾尝试 EBNF 等上层框架,但使模板引擎太慢。
编写渲染器
第二步定义带 render 方法的 Node 子类:
import datetime
from django import template
class CurrentTimeNode(template.Node):
def __init__(self, format_string):
self.format_string = format_string
def render(self, context):
return datetime.datetime.now().strftime(self.format_string)
init 接收编译函数传来的 format_string;参数与选项都应通过构造函数交给 Node。实际工作在 render 中完成。
render 通常应静默失败,尤其生产环境。但某些情况,特别是 context.template.engine.debug 为 True 时,可抛异常帮助调试。例如部分内置标签遇到参数数量或类型错误会抛 TemplateSyntaxError。
编译与渲染解耦,使同一模板可针对不同上下文反复渲染,无需反复解析。
自动转义
除前述 simple_tag 外,标签输出不自动经过转义过滤器。
若 render 把结果存进上下文变量,而非直接返回字符串,应在适当时使用 mark_safe。变量最终输出时受当时自动转义设置影响,需免于进一步转义的内容必须正确标记。
如果为子渲染创建新上下文,应继承当前 autoescape 值:
from django.template import Context
def render(self, context):
# ...
new_context = Context({"var": obj}, autoescape=context.autoescape)
# ... Do something with new_context ...
自行渲染模板的完整形式:
def render(self, context):
t = context.template.engine.get_template("small_fragment.html")
return t.render(Context({"var": obj}, autoescape=context.autoescape))
若不传 context.autoescape,新上下文始终自动转义;在 autoescape off 块中,这可能不是所需行为。
线程安全
节点解析后,render 可以调用任意次数。多线程环境中,同一节点可能同时用不同上下文响应不同请求,因此必须保证线程安全。
不要在节点自身存储可变渲染状态。例如内置 cycle 每次渲染轮换字符串:
{% for o in some_list %}
<tr class="{% cycle 'row1' 'row2' %}">
...
</tr>
{% endfor %}
朴素实现可能为:
import itertools
from django import template
class CycleNode(template.Node):
def __init__(self, cyclevars):
self.cycle_iter = itertools.cycle(cyclevars)
def render(self, context):
return next(self.cycle_iter)
两个模板并行渲染时:线程1第一次返回 row1,线程2第一次返回 row2;线程1第二次仍 row1,线程2第二次仍 row2。迭代发生在全局,每个线程看到的值不轮换,并非预期。
Django 提供与当前渲染上下文关联、类似字典的 render_context,存放 Node 在各次 render 调用之间的状态:
class CycleNode(template.Node):
def __init__(self, cyclevars):
self.cyclevars = cyclevars
def render(self, context):
if self not in context.render_context:
context.render_context[self] = itertools.cycle(self.cyclevars)
cycle_iter = context.render_context[self]
return next(cycle_iter)
不会在节点生命周期内变化的信息可以安全保存为属性,例如 cyclevars。当前模板特有的状态(如迭代位置)应存入 render_context。
用 self 作键限定节点范围,避免同一模板多个 CycleNode 相互覆盖。如果有多个状态变量,可让 render_context[self] 是字典。
注册标签
在模块的 Library 实例上注册:
register.tag("current_time", do_current_time)
tag 接收标签名字符串与编译函数对象,不能传函数名字符串。省略名称时使用编译函数名。也支持装饰器:
@register.tag(name="current_time")
def do_current_time(parser, token): ...
@register.tag
def shout(parser, token): ...
向标签传递模板变量
split_contents 拆出的参数均为字符串字面量。传动态内容需要额外解析。假设要将对象的 DateTimeField 交给标签格式化:
<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p>
split_contents 返回:标签名 format_time;无外层引号的字符串 blog_entry.date_updated;包含外层引号的格式字符串 '"%Y-%m-%d %I:%M %p"'。
编译函数为:
from django import template
def do_format_time(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, date_to_be_formatted, format_string = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
"%r tag requires exactly two arguments" % token.contents.split()[0]
)
if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
raise template.TemplateSyntaxError(
"%r tag's argument should be in quotes" % tag_name
)
return FormatTimeNode(date_to_be_formatted, format_string[1:-1])
渲染器还要获得 blog_entry.date_updated 的真实值。用 django.template.Variable,以变量名称构造,再调用 resolve(context):
class FormatTimeNode(template.Node):
def __init__(self, date_to_be_formatted, format_string):
self.date_to_be_formatted = template.Variable(date_to_be_formatted)
self.format_string = format_string
def render(self, context):
try:
actual_date = self.date_to_be_formatted.resolve(context)
return actual_date.strftime(self.format_string)
except template.VariableDoesNotExist:
return ""
如果当前上下文无法解析变量,抛出 VariableDoesNotExist。
在上下文中设置变量
比起直接输出,标签设置模板变量通常更灵活,模板作者可以复用结果。在 render 中向上下文字典赋值:
import datetime
from django import template
class CurrentTimeNode2(template.Node):
def __init__(self, format_string):
self.format_string = format_string
def render(self, context):
context["current_time"] = datetime.datetime.now().strftime(self.format_string)
return ""
render 始终应返回字符串;仅设置变量时返回空字符串。用法:
{% current_time "%Y-%m-%d %I:%M %p" %}<p>The time is {{ current_time }}.</p>
上下文变量只在赋值所在的同一个 block 中可用,这是有意设计的作用域,避免与其他块冲突。
但 CurrentTimeNode2 把 current_time 名称写死,可能覆盖模板其他地方已有变量。更清晰的方案让调用者指定输出变量:
{% current_time "%Y-%m-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p>
重构编译函数和节点:
import re
class CurrentTimeNode3(template.Node):
def __init__(self, format_string, var_name):
self.format_string = format_string
self.var_name = var_name
def render(self, context):
context[self.var_name] = datetime.datetime.now().strftime(self.format_string)
return ""
def do_current_time(parser, token):
# This version uses a regular expression to parse tag contents.
try:
# Splitting by None == splitting by spaces.
tag_name, arg = token.contents.split(None, 1)
except ValueError:
raise template.TemplateSyntaxError(
"%r tag requires arguments" % token.contents.split()[0]
)
m = re.search(r"(.*?) as (\w+)", arg)
if not m:
raise template.TemplateSyntaxError("%r tag had invalid arguments" % tag_name)
format_string, var_name = m.groups()
if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
raise template.TemplateSyntaxError(
"%r tag's argument should be in quotes" % tag_name
)
return CurrentTimeNode3(format_string[1:-1], var_name)
区别是 do_current_time 同时提取格式字符串与变量名,传给 CurrentTimeNode3。只需要简单的“更新上下文”语法时,应考虑支持变量赋值的 simple_tag。
解析直到另一个块标签
标签可以配对。例如 comment 隐藏到 endcomment 之间的内容。编译函数中使用 parser.parse:
def do_comment(parser, token):
nodelist = parser.parse(("endcomment",))
parser.delete_first_token()
return CommentNode()
class CommentNode(template.Node):
def render(self, context):
return ""
真实 comment 略有不同:为允许内部存在无效模板标签,它使用 parser.skip_past('endcomment'),而非先 parse 再 delete_first_token,从而不创建节点列表。
parser.parse 接收终止标签名称元组,返回遇到终止标签前全部 Node 的 NodeList。上例列表包含 comment 和 endcomment 之间的节点,不含两端标签。parse 尚未消费 endcomment,因此需显式 delete_first_token。CommentNode.render 返回空字符串,内部内容被忽略。
解析并保存块内容
前例丢弃内部内容,也可以对其进行处理。upper 标签将自身与 endupper 之间的内容变为大写:
{% upper %}This will appear in uppercase, {{ your_name }}.{% endupper %}
仍使用 parse,但将 NodeList 交给节点:
def do_upper(parser, token):
nodelist = parser.parse(("endupper",))
parser.delete_first_token()
return UpperNode(nodelist)
class UpperNode(template.Node):
def __init__(self, nodelist):
self.nodelist = nodelist
def render(self, context):
output = self.nodelist.render(context)
return output.upper()
新增概念是 UpperNode.render 中的 self.nodelist.render(context):先渲染内部,再转换为大写。
更复杂渲染示例可查看 defaulttags.py 中 for 标签,以及 smartif.py 中 if 标签源码。
原文:How to create custom template tags and filters
来源:Django 6.1 文档。本文为中文译稿,保留该版本范围和全部原文示例;代码注释、变量、函数名与模板语法保留原文,未在本次环境运行 Django。© Django Software Foundation 和各贡献者;具体文档许可证仍需核验,转载依据用户对本批文章的明确授权。











暂无评论内容