本文介绍Django的管理站点的配置. 通过配置, 可以打造一个功能完善的管理网站. 包括模型的注册、自定义列表页面、添加计算列、加载关联模型、自定义并覆盖基本查询集、添加搜索框、添加过滤栏、链接到其他页面、添加自定义操作、自定义表单、显示子项、添加数据验证、添加通用的关联, 以及介绍如何扩展可插拨应用. 内容丰富基本函盖Django管理站点的所有知识点.
设置Django管理站点
Django管理站点需要加后缀admin
http://127.0.0.1:8000/admin
为了能管理网站还要创建超用户, 以及设置密码
python manage.py createsuperuser
python manage.py changepassword admin
修改管理站点标题, 进入 storefront–urls.py, 添加:
admin.site.site_header = ‘Storefront Admin’
admin.site.index_title = ‘Admin’
注册模型
每个应用都有一个admin模块: admin.py, 这是自定义应用程序管理面板的地方.
首先注册模型 以使在管理站点可以管理. 先注册 Collection
store–admin.py, 添加:
from . improt models
…
admin.site.register(models.Collection)
再修改对象的显示字符串,避免显示 object(n) 这样的难懂的.
store–models.py, 修改 Collectiong类:
…
def __str__(self):
return self.title
class Meta:
ordering = [‘title’]
同样的, Product模型也作同样的注册和显示. 这里不重复了.
自定义列表页面
前面的列表页只有一列, 显示对象的方法 __str__ 返回的字符串, 很多时候我们希望能显示多列, 并且有些列可以在列表页面进行修改.
store–admin.py, 添加:
…
class ProductAdmin(admin.ModelAdmin):
list_display = [‘title’, ‘unit_price’]
…
我们添加一个新管理类ProductAdmin, 按照约定一般命名为 “类名+Admin”, 我们设置要显示的列为 title 和 unit_price.
再修改注册:
admin.site.register(models.Product, ProductAdmin)
将管理类也注册进去.
还有一种更简短的表示方法.
…
@admin.register(models.Product)
class ProductAdmin(admin.ModelAdmin):
list_display = [‘title’, ‘unit_price’]
除了list_display属性, 还有其他
list_editable = [‘unit_price’]
list_per_page = 10
list_editable 表示可编辑的列, list_per_page表示每页显示的数量.
可以搜索google关键字 “Django ModelAdmin”查看完整选项.
类似地, 对Customer的列表页自定义:
- 有三列显示,为 first_name, last_name, membership
- 可以更改会员资格 membership
- 每页显示10个, 默认排序是 first_name, 第二列排序是 last_name.
很简单, Customer管理页面的修改代码就不列了.
添加计算列
在Product列表页面, 我们添加一列显示库存状态, 当小于10时, 显示 Low.
…
class ProductAdmin(admin.ModelAdmin):
list_display = [‘title’, ‘unit_price’, ‘inventory_status’]
…
@admin.display(ordering=’inventory’)
def inventory_status(self, product):
if product.inventory<10:
return ‘Low’
return ‘OK’
我们添加了一列 inventory_status, 该列是类的方法名称, 根据该方法计算返回结果, 注意该方法的第2个参数为product.
另外, 由于该列是计算列, 加上修饰符 @admin.display(ordering=’inventory’) 表示在列表页面可以点该列标题通过 inventory 进行排序
加载关联对象
我们希望在Product列表页面添加一列显示Collection, 直接添加 collection 就好.
class ProductAdmin(admin.ModelAdmin):
list_display = [‘title’, ‘unit_price’, ‘inventory_status’, ‘collection’]
…
Django自动关联collection, 并用collection的title 显示, 因为在前面,Collection已重写了 __str__ 方法.
如果要显示关联对象的其他字段的话, 以前文章讲的加双划线方法如 …,’collection__title’ 是不可以的. 可以通过上一节添加计算列的方法:
…
list_display = […,’collection_title’]
list_select_related = [‘collection’]
def collection_title(self, product):
return product.collection.title
…
这里的 list_select_related_related属性不能省略, 否则每一行都会向数据库发起请求, 大大降低性能.
我们再来做自定义 order 列表, 可以在列表页面看到订单的客户.
store–admin.py添加:
class OrderAdmin(admin.ModelAdmin):
list_display = [‘id’, ‘placed_at’, ‘customer’]
…
store–models.py 修改 Customer类:
class Customer(models.Model):
def __str__(self):
return f'{self.first_name} {self.last_name}’
class Meta:
ordering = [‘first_name’, ‘last_name’]
覆盖基本查询集
我们在Collection列表页面添加产品数量的列, 修改CollectionAdmin类:
@admin.register(models.Collection)
class CollectionAdmin(admin.ModelAdmin):
list_display = [‘title’, ‘products_count’]
添加的 products_count 列用计算列的方式:
…
def products_count(self, collection):
return collection.products_count
…
显然, collection没有products_count列, 我们可以在基本查询集上增加一列. 覆盖原先的查询集.
…
def get_queryset(self, request):
return super().get_queryset(request).annotate(products_count=Count(‘product’))
…
每个模型都有一个 get_queryset 方法, 可以覆盖修改, 这里我们增加了products_count列. 注意要导入Count类:
from django.db.models.aggregates import Count
另外增加的这列不能在列表页面点标头排序, 可以在计算列上增加排序字段:
…
@admin.display(ordering=’products_count’)
def products_count(self, collection):
…
链接到其他页面
如果我们希望在点击Collection列表页面的products_count列可以链接到具体的产品, 这也是很常见的操作, 该如何做呢?
这个需要格式化 html ,先导入 format_html:
from django.utils.html improt format_html
再修改Collection页面的计算列 products_count:
…
def products_count(self, collection):
return format_html(‘<a>{}</a>’, collection.products_count)
format_html函数的第一个参数里的 {} 是占位符, 后面的参数代入.
我们先让他链接到百度看看是否正常.
…
return format_html(‘<a href=”https://www.baidu.com”>{}</a>’, collection.products_count)
…
刷新页面, 点击products_count列的某一值已能链接到百度, 下面我们修改使之能链到Product列表页面, 需要用到reverse 函数. 先导入:
from django.urls import reverse
然后, 修改计算列 products_count:
…
def products_count(self, collection):
return format_html(‘<a href=”{}”>{}</a>’, reverse(‘admin:store_product_changelist’), collection.products_count)
这是Django的格式, admin:应用_模型_changelist.
刷新页面, 点击products_count列的某一值已能链接Product列表页, 但它没有筛选, 我们要加上. url查询筛选是“ ?key=value”, 所以先加个问号 ? ,然后再加筛选参数, 构造筛选参数需要用到 urlencode, 先导入:
from django.utils.html import urlencode
然后修改成这样:
…
def products_count(self, collection):
url = reverse(‘admin:store_product_changelist’)+’?’ + \
urlencode({‘collection__id’: str(collection.id)})
return format_html(‘<a href=”{}”>{}</a>’, url, collection.products_count)
练习:
在Customer列表页面添加 orders_count, 以查看每个客户的订单.
添加搜索到列表页面
添加搜索到Customer列表页面.
…
search_fields = [‘first_name’, ‘last_name’]
列表页面出现搜索框, 键入的搜索内容在这两个字段中查找是否有匹配. 也可以查询以什么开头.
search_fields = [‘first_name__startswith’, ‘last_name__istartswith’]
XXX__startswith 表示以什么开头, 而 XXX___istartswith 加上 i 表示不区分大小写.
添加过滤列表页面
我们在Product列表页面添加过滤器, 很简单, 只需在管理类加上list_filter属性即可.
…
list_filter = [‘collection’, ‘last_update’]
也可以创建自定义的过滤器.
class InventoryFilter(admin.SimpleListFilter):
title = ‘inventory’ # 过滤标题
parameter_name = ‘inventory’ # 在地址栏上显示的查询参数,也可以设成其他任何词
def lookups(self, request, model_admin):
return [(‘<10’, ‘Low’)]
# 显示的过滤条目, 每个列表项是一个元组,对应filter条目的值和显示项
def queryset(self, request, queryset):
# 实际过滤的逻辑
if self.value() == ‘<10’:
return queryset.filter(inventory__lt=10)
然后, 在 ProductAdmin类的 list_filter属性中加入 InventoryFilter
…
list_filter = [‘collection’, ‘last_update’, InventoryFilter]
创建定制的操作
我们可以创建定制的操作, 比如创建一个批量设置所有Product 库存为 0 的操作. 在 ProductAdmin类中添加方法:
…
@admin.action(description=’clear inventory’)
def clear_inventory(self, request, queryset):
update_count = queryset.update(inventory=0)
self.message_user(
request, f'{update_count} products were successfully updated.’)
再在ProductAdmin类中添加actions属性:
…
actions = [‘clear_inventory’]
刷新网页, 就可以在顶部操作区看到 clear_inventory 操作了.
每一个管理类都有message_user方法, 用于显示提示消息. 前面我们用了两个参数, 还可以设置第三个参数, 表示消息类型.
先导入 message:
from django.contrib import admin, messages
然后添加第3个参数:messages.ERROR
…
self.message_user(
request, f'{update_count} products were successfully updated.’, messages.ERROR)
这样提示消息就变为红叉的错误警示.
自定义表单
添加一条记录, 需要用到表单. 我们是可以自定义表单的.
以Product为例, 我们在ProductAdmin类中添加:
…
fields = [‘title’, ‘slug’]
# 设定显示的列
exclude = [‘promotions’]
# 排除某些列
# readonly_fields = [‘title’]
# 只读字段
prepopulated_fields = {
# 表示是预填,这里是用 title预填slug
‘slug’: [‘title’],
}
下拉列表框太长的话, 是不好选的. 有些时候改成自动完成字段比较好. 对Product表单, collection是默认用下拉列表框的, 我们在ProductAdmin添加一行:
…
autocomplete_fields = [‘collection’]
另外, 还需要在CollectionAdmin类里添加 search_fields:
…
search_fields = [‘title’]
刷新网页, 添加一个Product记录, 在collection框里输入会自动完成.
详细查看, 可以谷歌搜索关键字 “Django ModelAdmin”—Admin actions
练习:
修改Order表单, 只显示 payment_status 和 customer,不显示 placed_at, 因为Django会自动添加的. 将 customer 改成自动完成.
添加数据验证
我们在定义模型时, 已有基本的验证, 比如不能为空等. 我们在新增一条记录时, 比如Product时, 页面会提示哪些不能为空等信息. 我们将Product模型的description修改下,添加null=True.
…
description = models.TextField(null=True)
然后再migrate到数据库:
python manage.py makemigrations
python manage.py migrate
再进行新增一条 Product记录, 在 description框为空时, Django仍然提示不能为空. 为什么?
null=true 只是数据库的验证允许为空, 页面还是会验证不能为空, 所以还得加上 blank=True , 这样页面才允许description为空.
description = models.TextField(null=True, blank=True)
我们添加Product时, unit_price 设置为0 甚至负数 -1 时, 页面也不会提示错误, 我们需要添加数据验证. 这里用到的是MinValueValidator.
from django.core.validators import MinValueValidator
…
unit_price = models.DecimalField(
max_digits=6,
decimal_places=2,
validators=[MinValueValidator(1)]
)
MinValueValidator还有第2个参数message, 用于提示信息,这里暂不用.
搜索谷歌关键字 “django validators”, 可以查到很多实用的验证模块方法.
练习:
Product的 inventory 作同样设置, promotions 加上 blank=True, 页面编辑时可选.
编辑子项用Inlines
目前OrderItem未管理, 它属于Order的子项. 可以在OrderAdmin里添加 inlines属性进行管理.
具体做法是:
先在admin.py里创建TabularInline或StackedInline的子类.
…
class OrderItemInline(admin.TabularInline):
model = models.OrderItem
…
TabularInline表示表格内联, 然后在 OrderAdmin类添加属性inlines.
…
inlines = [OrderItemInline]
…
OrderItemInline 间接继承于 ModelAdmin, 所以ModelAdmin的属性也可以用. 所以我们可以添加给OrderItemInline添加一个属性:
autocomplete_fields = [‘product’]
默认的内联表格位占符是3个, 有点丑, 我们可以取消掉, 设为0 即可. 另外也可以设置 子项的数量. 如下:
class OrderItemInline(admin.TabularInline):
model = models.OrderItem
autocomplete_fields = [‘product’]
extra = 0 # 占位符
min_num = 1 # 界面上子项最小数量
max_num = 10 # 界面上子项最大数量
顺便可以试一下让OrderItemInline 继承 admin.StackedInline 看看效果, 说实话, 这个界面本人接受不了: ) 一般就用TabularInline就好.
使用通用关联
storefront 项目有通用的 tags应用. 我们要在产品中增加一个 tag 管理, 该如何做?
先在 tags–admin.py里注册 Tag
…
from .models import Tag
…
admin.site.register(Tag)
在tags—models.py里添加 Tag 的 __str__ 方法:
…
def __str__(self):
return self.label
回到store应用, admin.py 添加 TagInline类:
…
from django.contrib.contenttypes.admin import GenericTabularInline
from tags.models import TaggedItem
…
class TagInline(GenericTabularInline):
model = TaggedItem
在ProductAdmin类中添加 inlines 属性:
…
inlines = [TagInline]
类似地, 在TagInline类中, 添加自动完成列
…
autocomplete_fields = [‘tag’]
…
再回到 tags 应用创建TagAdmin 类, 设置 search_fields
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
search_fields = [‘label’]
扩展可插拨应用
上节 store 应用需要导入TaggedItem, 对 tags应用产生了依赖. 而store 与 tags 是互相独立的应用, 不应该有此依赖. 我们需要进行解耦.
可以创建一个独立的应用, 比如命名为 store_custom, 它知道 store 和 TaggedItem, 也仅用于 store ,其他应用不会用.
python manage.py startapp store_custom
再进入 store_custom的 admin.py, 将原在 store–admin.py 的 TagInline剪切过来.
class TagInline(GenericTabularInline):
model = TaggedItem
autocomplete_fields = [‘tag’]
创建类 CustomProductAdmin, 继承于ProductAdmin
class CustomProductAdmin(ProductAdmin):
inlines = [TagInline]
再重新注册到 CustomProductAdmin
admin.site.unregister(Product)
admin.site.register(Product, CustomProductAdmin)
最后一步:
在项目的 storefront — settings.py的 INSTALL_APPS 添加 ‘store_custom’
当然别忘在 ProductAdmin 中删除 inlines = [TagInline], 同时删除 import TaggedItem ,使之不再依赖于 tags 应用.
最重要的一点是, 如果我们在 INSTALL_APPS 删除 store_custom, 则 Product 表单就不显示 TagInline 表单, 加上则显示. 完全解耦, 非常漂亮!!
小结
本文介绍Django的管理站点的配置.. 内容基本函盖Django管理站点的所有知识点.
下一篇开始介绍Django中创建RESTful API的知识.