简介
本教程从《添加样式表和图像》结束的地方开始。我们将继续Web投票应用程序,并专注于自定义Django自动生成的管理站点,我们在《设置数据库》中首次探讨了该站点。
本教程从《添加样式表和图像》结束的地方开始。我们将继续Web投票应用程序,并专注于自定义Django自动生成的管理站点,我们在《设置数据库》中首次探讨了该站点。
通过使用 admin.site.register(Question)
注册 Question
模型,Django 能够构建默认的表单表示形式。通常,你会希望自定义管理表单的外观和工作方式。你可以在注册对象时告诉 Django 你想要的选项来实现这一点。
让我们通过重新排列编辑表单上的字段来看看这是如何工作的。将 admin.site.register(Question)
这一行替换为:
编辑 ~/project/mysite/polls/admin.py
文件,使其如下所示:
from django.contrib import admin
from.models import Question
class QuestionAdmin(admin.ModelAdmin):
fields = ["pub_date", "question_text"]
admin.site.register(Question, QuestionAdmin)
每当你需要更改模型的管理选项时,你都将遵循此模式 —— 创建一个模型管理类,然后将其作为第二个参数传递给 admin.site.register()
。
运行 Django 开发服务器:
cd ~/project/mysite
python manage.py runserver
在桌面环境的 Firefox 中打开 http://127.0.0.1:8000/admin/
,然后点击 “Questions” 链接。你应该会看到一个如下所示的表单。
上面这个特定的更改使得 “发布日期” 字段出现在 “问题” 字段之前:
对于只有两个字段的情况,这可能并不明显,但对于有几十个字段的管理表单来说,选择一个直观的顺序是一个重要的可用性细节。
说到有几十个字段的表单,你可能希望将表单拆分为多个字段集:
from django.contrib import admin
from.models import Question
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["question_text"]}),
("日期信息", {"fields": ["pub_date"]}),
]
admin.site.register(Question, QuestionAdmin)
~django.contrib.admin.ModelAdmin.fieldsets
中每个元组的第一个元素是字段集的标题。这是我们现在的表单样子:
好的,我们已经有了问题管理页面,但是一个 Question
有多个 Choice
,而管理页面并没有显示选项。
不过,马上就会显示了。
有两种方法可以解决这个问题。第一种方法是像注册 Question
一样向管理站点注册 Choice
:
from django.contrib import admin
from.models import Choice, Question
#...
admin.site.register(Choice)
现在,“选项” 在 Django 管理站点中是一个可用选项。“添加选项” 表单如下所示:
在那个表单中,“问题” 字段是一个下拉框,包含数据库中的每个问题。Django 知道 ~django.db.models.ForeignKey
在管理站点中应该表示为一个 <select>
框。在我们的例子中,此时只有一个问题。
还要注意 “问题” 旁边的 “添加另一个问题” 链接。与另一个对象具有 ForeignKey
关系的每个对象都会自动获得这个链接。当你点击 “添加另一个问题” 时,会弹出一个带有 “添加问题” 表单的窗口。如果你在那个窗口中添加一个问题并点击 “保存”,Django 会将问题保存到数据库中,并在你正在查看的 “添加选项” 表单上动态地将其添加为所选选项。
但是,实际上,这是一种向系统中添加 Choice
对象的低效方式。如果能在创建 Question
对象时直接添加一堆选项,那就更好了。让我们来实现这一点。
删除对 Choice
模型的 register()
调用。然后,编辑 Question
的注册代码如下:
from django.contrib import admin
from.models import Choice, Question
class ChoiceInline(admin.StackedInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["question_text"]}),
("日期信息", {"fields": ["pub_date"], "classes": ["collapse"]}),
]
inlines = [ChoiceInline]
admin.site.register(Question, QuestionAdmin)
这告诉 Django:“Choice
对象在 Question
管理页面上进行编辑。默认情况下,提供足够的字段用于 3 个选项。”
加载 “添加问题” 页面,看看效果如何:
它的工作原理如下:有三个用于关联选项的插槽 —— 由 extra
指定 —— 并且每次你回到已创建对象的 “更改” 页面时,都会再得到另外三个额外的插槽。
在当前三个插槽的末尾,你会找到一个 “添加另一个选项” 链接。如果你点击它,会添加一个新的插槽。如果你想删除添加的插槽,可以点击添加插槽右上角的 X。这张图片展示了一个添加的插槽:
不过,有一个小问题。显示用于输入关联 Choice
对象的所有字段会占用大量屏幕空间。因此,Django 提供了一种以表格形式显示内联关联对象的方式。要使用它,将 ChoiceInline
声明改为:
class ChoiceInline(admin.TabularInline):
...
使用 TabularInline
(而不是 StackedInline
),关联对象将以更紧凑的基于表格的格式显示:
请注意,有一个额外的 “删除?” 列,允许删除使用 “添加另一个选项” 按钮添加的行以及已经保存的行。
既然问题管理页面看起来不错了,那我们来对“更改列表”页面做一些调整 —— 就是那个显示系统中所有问题的页面。
目前它是这样的:
默认情况下,Django 显示每个对象的 str()
表示。但有时如果我们能显示单个字段会更有帮助。要做到这一点,可以使用 ~django.contrib.admin.ModelAdmin.list_display
管理选项,它是一个字段名元组,用于在对象的更改列表页面上作为列显示:
class QuestionAdmin(admin.ModelAdmin):
#...
list_display = ["question_text", "pub_date"]
为了保险起见,我们还把《设置数据库》中的 was_published_recently()
方法也加上:
class QuestionAdmin(admin.ModelAdmin):
#...
list_display = ["question_text", "pub_date", "was_published_recently"]
现在问题更改列表页面是这样的:
你可以点击列标题按这些值进行排序 —— 除了 was_published_recently
标题,因为不支持按任意方法的输出进行排序。还要注意,was_published_recently
的列标题默认是方法名(下划线替换为空格),并且每行都包含输出的字符串表示。
你可以通过在该方法上使用 ~django.contrib.admin.display
装饰器(在 polls/models.py
中)来改进这一点,如下所示:
from django.contrib import admin
class Question(models.Model):
#...
@admin.display(
boolean=True,
ordering="pub_date",
description="最近发布?",
)
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
有关可通过装饰器配置的属性的更多信息,请参阅 ~django.contrib.admin.ModelAdmin.list_display
。
再次编辑你的 polls/admin.py
文件,并对 Question
更改列表页面进行改进:使用 ~django.contrib.admin.ModelAdmin.list_filter
进行过滤。在 QuestionAdmin
中添加以下行:
list_filter = ["pub_date"]
这会添加一个“过滤”侧边栏,让人们可以按 pub_date
字段过滤更改列表:
显示的过滤器类型取决于你正在过滤的字段类型。因为 pub_date
是一个 ~django.db.models.DateTimeField
,Django 知道要给出适当的过滤选项:“任何日期”、“今天”、“过去7天”、“本月”、“今年”。
这看起来很不错了。让我们添加一些搜索功能:
search_fields = ["question_text"]
这会在更改列表顶部添加一个搜索框。当有人输入搜索词时,Django 将在 question_text
字段中进行搜索。你可以使用任意多个字段 —— 不过因为它在幕后使用 LIKE
查询,将搜索字段数量限制在合理数量会让你的数据库更容易进行搜索。
现在也是时候注意一下更改列表会自动提供分页功能了。默认是每页显示100项。更改列表分页 <django.contrib.admin.ModelAdmin.list_per_page>
、搜索框 <django.contrib.admin.ModelAdmin.search_fields>
、过滤器 <django.contrib.admin.ModelAdmin.list_filter>
、日期层次结构 <django.contrib.admin.ModelAdmin.date_hierarchy>
和 列标题排序 <django.contrib.admin.ModelAdmin.list_display>
都能像你期望的那样协同工作。
显然,在每个管理页面顶部显示“Django 管理”是很荒谬的。这只是占位文本。
不过,你可以使用 Django 的模板系统来更改它。Django 管理界面由 Django 自身提供支持,其界面使用 Django 自己的模板系统。
在你的项目目录(即包含 manage.py
的目录)中创建一个 templates
目录。模板可以存放在 Django 能够访问的文件系统的任何位置。(Django 以服务器运行的用户身份运行。)然而,将模板保存在项目内部是一个值得遵循的良好惯例。
打开你的设置文件(记得是 mysite/settings.py
),并在 TEMPLATES
设置中添加一个 DIRS <TEMPLATES-DIRS>
选项:
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"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-DIRS>
是一个文件系统目录列表,在加载 Django 模板时会检查这些目录;它是一个搜索路径。
就像静态文件一样,我们本可以把所有模板都放在一个大的 templates
目录中,这样也能完美运行。然而,属于特定应用的模板应该放在该应用的模板目录(例如 polls/templates
)中,而不是项目的(templates
)。我们将在《可复用应用教程》(/intro/reusable-apps)中更详细地讨论这样做的原因。
现在在 templates
目录内创建一个名为 admin
的目录,并将 Django 自身源代码中默认的 Django 管理模板目录(django/contrib/admin/templates
)中的模板 admin/base_site.html
复制到该目录中。
如果你在系统上难以找到 Django 源文件的位置,可以运行以下命令:
python -c "import django; print(django.__path__)"
['/home/labex/.local/lib/python3.10/site-packages/django']
然后,编辑该文件,将 {{ site_header|default:_('Django administration') }}
(包括花括号)替换为你认为合适的你自己网站的名称。你最终应该得到一段类似这样的代码:
{% block branding %}
<div id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a><div>
{% endblock %}
我们使用这种方法来教你如何覆盖模板。在实际项目中,你可能会使用 django.contrib.admin.AdminSite.site_header
属性来更轻松地进行这种特定的定制。
这个模板文件包含很多像 {% block branding %}
和 {{ title }}
这样的文本。{%
和 {{
标签是 Django 模板语言的一部分。当 Django 渲染 admin/base_site.html
时,这种模板语言将被求值以生成最终的 HTML 页面,就像我们在《创建公共接口视图》中看到的那样。
请注意,Django 的任何默认管理模板都可以被覆盖。要覆盖一个模板,按照你对 base_site.html
所做的操作进行 —— 从默认目录复制到你的自定义目录,并进行更改。
敏锐的读者可能会问:但是如果 DIRS <TEMPLATES-DIRS>
默认是空的,Django 是如何找到默认的管理模板的呢?答案是,由于 APP_DIRS <TEMPLATES-APP_DIRS>
被设置为 True
,Django 会自动在每个应用包内查找一个 templates/
子目录,用作备用(别忘了 django.contrib.admin
是一个应用)。
我们的投票应用不是很复杂,不需要自定义管理模板。但是如果它变得更复杂,并且需要为其某些功能修改 Django 的标准管理模板,那么修改应用的模板而不是项目的模板会更明智。这样,你可以将投票应用包含在任何新项目中,并确保它能找到所需的自定义模板。
有关 Django 如何查找其模板的更多信息,请参阅《模板加载文档》(template-loading)。
同样,你可能想要自定义 Django 管理索引页面的外观和感觉。
默认情况下,它会按字母顺序显示 INSTALLED_APPS
中已向管理应用注册的所有应用。你可能想要对布局进行重大更改。毕竟,索引页面可能是管理站点中最重要的页面,它应该易于使用。
要自定义的模板是 admin/index.html
。(按照上一节中对 admin/base_site.html
所做的操作 —— 从默认目录复制到你的自定义模板目录)。编辑该文件,你会看到它使用了一个名为 app_list
的模板变量。该变量包含每个已安装的 Django 应用。你可以不使用它,而是以你认为最合适的方式硬编码到特定对象管理页面的链接。
恭喜你!你已经完成了“自定义 Django 管理站点”实验。你可以在 LabEx 中练习更多实验来提升你的技能。