前言:
在上一章中,我们对Django的Model层有了比较全面的认识,本章就来配置Django自带的admin。这里需要认识到,Django的Model层是很重要的一环,无论是对于框架本身还是对于基于Django框架开发的大多数系统而言。因为一个系统的根基就是数据,所有业务都是建立在这个根基之上的。如果从数据层开始就出了问题(偏差),那么其他层的开发也不会得到好结果。
本章中,我们主要使用Django自带的admin来完成管理后台的开发。admin 是Django的杀手锏。对于内容管理系统来说,当你有了数据表,有了Model,就相当于自动有了一套管理后台,还包括权限控制,这简直是太爽的操作了。当然,这得益于Django的诞生环境——“它最初用来开发新闻内容相关的网站”。从框架本身来讲,这完全依托于Django的Model层。
我们在上一章中说过,Django是一个重Model 的框架。Model定义好了字段类型,上层可以根据这些字段类型定义 Form 中需要呈现以及编辑的字段类型,这样就形成了表单。有了表单之后,基本上就有了增、删、改的页面。而基于QuerySet 这个数据集合以及它所提供的查询操作,就有了列表的数据以及列表页的操作。
其实可以想一下,对于一个内容管理系统来说,需要哪些页面来完成数据的增、删、改、查。这其实也就是上面说到的那些。有了Model层的支持,上面的业务逻辑很容易实现。当然,这也带来另外一个问题,那就是上层的实现跟Model层耦合得比较紧。这既是好事,也是坏事,不过对于刚开始用Django的你来说,不用考虑太多,直接用就行,等你熟悉之后,所有的特点都能为你所用。
有了大概的认识之后,我们来看admin的用法。
第三章:开发管理后台
3.1 配置admin页面
基于前面编写完成的Model代码,我们来配置 admin的页面。重复编写Model中的字段相对枯燥,但是编写admin的代码会比较有趣,因为它让我们能直接看到对应的页面,也能直接修改页面。
废话不多说,我们开始编写admin的代码。
3.1.1 创建blog的管理后台
首先是blog这个App,其中定义了3个Model,分别是Category、Post 和Tag。先来创建 admin页面,其代码需要写到blog/admin.py这个模块(文件)中。
- 编写 Tag和Category的管理后台
我们先来编写Tag和Category这两个 Model对应的admin配置:
# blog/admin.py
from django.contrib import admin
from blog.models import Tag, Category
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'status', 'is_nav', 'owner', 'created_time') # 页面上显示的字段
fields = ('name', 'status', 'is_nav') # 增加时显示的字段
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ('name', 'status', 'owner', 'created_time')
fields = ('name', 'status')
这点代码就完成了 Tag 和 Category的 admin 配置,我们可以尝试运行一下看看效果,然后再来解释其中代码的作用。
首先,激活虚拟环境。先来创建超级用户的用户名和密码,执行,python manage.py createsuperuser
,然后根据提示输入用户名和密码,如下图所示:
接着运行Django,打开http://127.0.0.1:8000/admin/,进行登录,登录后能看到如图下图所示的界面。
刚才编写的“标签”和“分类”管理后台就出现了,你可以尝试点进去操作一下,比如新建一条数据,不过你应该会遇到如下图所示的错误:
这个错误可以理解为数据不完整。根据提示的信息 NOT NULL constraint failed:blog_category.owner_id,我们知道具体问题是“非空约束错误:blog_category.owner_id”。再结合我们编写的Model,不难得到问题的原因是:我们给每个Model定义了一个owner字段来标记这个数据属于哪个作者,而页面上并没有填这一项。
对于新手来说,学会查看和分析错误信息至关重要,其重要性远高于掌握一个框架!
接着来解决这个问题。知道了原因,解决起来就简单了。新建数据时,页面上并没有让我们设置作者的地方。拿“分类”管理来说,其界面如下图所示:
再对比上一章中Category模型的定义,为什么只展示了三个字段的内容?其原因就在 admin 的定义上。我们再来看看CategoryAdmin的定义:
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'status', 'is_nav', 'owner', 'created_time') # 页面上显示的字段
fields = ('name', 'status', 'is_nav') # 增加时显示的字段
可以发现,这三个字段刚好是fields的内容。这时即便没看文档也知道,fields这个配置的作用就是控制页面上要展示的字段。
因此,解决方案很简单,把 owner 放进去就好了。此时我们就能在页面上选择用户了,如下图所示:
不过这也不是一个好方案。如果这么做,岂不是任何作者都可以随意把自己创建的内容改为作者的吗?这就是bug了。
此时可以考虑另外一个方案:保存数据之前,把owner这个字段设定为当前的登录用户。怎么做呢?这个时候就需要重写ModelAdmin的save_model方法,其作用是保存数据到数据库中。重写后的完整代码如下:
# blog/admin.py
from django.contrib import admin
from blog.models import Tag, Category
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'status', 'is_nav', 'owner', 'created_time') # 页面上显示的字段
fields = ('name', 'status', 'is_nav') # 增加时显示的字段
# 新增字段
def save_model(self, request, obj, form, change):
obj.owner = request.user # 当前已经登录的用户作为作者
return super().save_model(request, obj, form, change)
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ('name', 'status', 'owner', 'created_time')
fields = ('name', 'status')
# 新增字段
def save_model(self, request, obj, form, change):
obj.owner = request.user
return super().save_model(request, obj, form, change)
这里我们再解释一下save_model这个方法,从其参数的命名基本上能看到它们的作用。通过给obj.owner赋值,就能达到自动设置owner的目的。这里的request 就是当前请求,request.user 就是当前已经登录的用户。如果是未登录的情况下,通过request.user 拿到的是匿名用户对象。
obj 就是当前要保存的对象,而 form 是页面提交过来的表单之后的对象,后面会讲。change用于标志本次保存的数据是新增的还是更新的。
这么修改之后,重新运行一下代码。保存数据后,查看列表页,得到的结果如下图所示。
看到这个页面后,再对比一下上面定义的 list_display,就能够知道它的作用是什么了。你应该自己去修改 list_display的内容,然后看看页面上有什么变化。
在编程中始终需要记住的一点是,无论文档上说这么写代码会得到这样的结果,还是大神告诉你那么写就能得到那样的结果,都是仅供参考。只有代码运行之后产生的结果才是可信的。因为计算机是客观的,执行什么,得到什么,是很严谨的因果关系。而人类是会犯错的,这个错可能不是知识上的错误,可能是环境差异导致的。谨记!
- 编写Post的管理后台
Tag和Category的admin代码都编写好了,接着需要编写Post的admin代码。还是在刚才的文件 blog/admin.py中增加代码:
# blog/admin.py
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from blog.models import Tag, Category, Post
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'status', 'is_nav', 'owner', 'created_time') # 页面上显示的字段
fields = ('name', 'status', 'is_nav') # 增加时显示的字段
def save_model(self, request, obj, form, change):
obj.owner = request.user # 当前已经登录的用户作为作者
return super().save_model(request, obj, form, change)
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ('name', 'status', 'owner', 'created_time')
fields = ('name', 'status')
def save_model(self, request, obj, form, change):
obj.owner = request.user
return super().save_model(request, obj, form, change)
# 新增代码如下!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'category', 'status', 'created_time', 'owner', 'operator']
list_display_links = []
list_filter = ['category', ]
search_fields = ['title', 'category_name']
actions_on_top = True
actions_on_bottom = False
# 编辑页面
save_on_top = True
fields = (
('category', 'title'),
'desc',
'status',
'content',
'tag',
)
def operator(self, obj):
""" 新增编辑按钮 """
return format_html(
'<a href="{}">编辑</a>',
reverse('admin:blog_post_change', args=[obj.id])
)
operator.short_description = '操作'
def save_model(self, request, obj, form, change):
obj.owner = request.user
return super().save_model(request, obj, form, change)
在blog/admin.py最上面增加新的引用。到目前为止,所有的引用为:
# blog/admin.py
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from blog.models import Tag, Category, Post
你可以先把上面的代码写到项目中运行一下,尝试创建一些数据,看看结果,然后再回过头来解释各项内容。
这里先总结PostAdmin中的各项配置,然后再整体总结一下。
-
list_display: 上面已经介绍过,它用来配置列表页面展示哪些字段。
-
list_display_links: 用来配置哪些字段可以作为链接,点击它们,可以进入编辑页面。如果设置为 None,则表示不配置任何可点击的字段。
-
list_filter:配置页面过滤器,需要通过哪些字段来过滤列表页。上面我们配置了category,这意味着可以通过category 中的值来对数据进行过滤。
-
search_fields: 配置搜索字段。
-
actions_on_top: 动作相关的配置,是否展示在顶部。
-
actions_on_bottom: 动作相关的配置,是否展示在底部。
-
save_on_top: 保存、编辑、编辑并新建按钮是否在顶部展示。
除了这些ModelAdmin,Django还提供了很多其他配置,具体可以通过文档查看:https://docs.djangoproject.com/zh-hans/4.2/ref/contrib/admin/#modeladmin-options。不过文档上列的也并不完整,最好等你熟悉Django之后,去看对应部分的源码。
最后,还需介绍的是自定义方法。在 list_display 中,如果想要展示自定义字段,如何处理呢?上面的operator 就是一个示例。
自定义函数的参数是固定的,就是当前行的对象。列表页中的每一行数据都对应数据表中的一条数据,也对应Model的一个实例。
自定义函数可以返回HTML,但是需要通过 format_html函数处理,reverse 是根据名称解析出URL地址,这个后面会介绍到。最后的operator.short_description的作用就是指定表头的展示文案。
在日常开发中,自定义函数很常用,除了上面介绍的可以自定义HTML代码外,还可以定义需要展示的其他内容。比如说,在“分类”列表页,我们需要展示该分类下有多少篇文章,此时可以在CategoryAdmin中增加如下代码:
# blog/models.py
def post_count(self, obj):
""" 统计文章数量 """
return obj.post_set.count()
post_count.short_description = '文章数量'
然后修改 list_display,最后增加 post_count,刷新页面就能看到修改之后的结果(如下图所示)。
完整CategoryAdmin代码如下:
# blog/models.py
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'status', 'is_nav', 'owner', 'created_time', 'post_count') # 页面上显示的字段
fields = ('name', 'status', 'is_nav') # 增加时显示的字段
def save_model(self, request, obj, form, change):
obj.owner = request.user # 当前已经登录的用户作为作者
return super().save_model(request, obj, form, change)
def post_count(self, obj):
""" 统计文章数量 """
return obj.post_set.count()
post_count.short_description = '文章数量'
- Model的__str__方法
如果你尝试运行了上面的代码,可能会发现列表页上有这样的文案:Category object。这是因为我们没有配置类的__str__方法。因此,对于每个Model,都需要增加这个方法,类似对于这样:
# 在blog/models页面下新增代码
class Category (models.Model):
# 省略其他代码
def __str__(self):
return self.name
class Tag(models.Model):
# 省略其他代码
def __str__(self):
return self.name
class Post(models.Model):
# 省略其他代码
def __str__(self):
return self.title
- ModelAdmin总结
好了,到此为止,一个简单的管理后台就配置好了,我们稍稍总结一下。
你应该能够体会到“当你有了Model之后,就自动有了一个管理系统”的感觉。如上面所说,这一切都是基于Model来实现的。
通过继承admin.ModelAdmin,就能实现对这个Model的增、删、改、查页面的配置。这里的ModelAdmin 是很重要的一环,后面还会接触到 Mode1Form 的概念,这些都是跟 Model紧耦合的。在 Model之上可以实现更多的业务逻辑。而关于 admin 的部分,ModelAdmin 可能是你使用最频繁的类,如果后面的工作会持续用到 admin模块的话。
3.1.2 comment的admin配置
这一块不用多讲了,跟上面一样,我们需要做的就是照猫画虎。你可以自行来写,或者根据下面我的实现来写。
comment/admin.py的完整代码如下:
# comment/admin.py
from django.contrib import admin
from .models import Comment
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ('target', 'nickname', 'content', 'website', 'created_time')
3.1.3 config的admin配置
这一块同样不用多说,直接来编辑代码。config/admin.py的完整代码如下:
# config/admin.py
from django.contrib import admin
from .models import Link, SideBar
@admin.register(Link)
class LinkAdmin(admin.ModelAdmin):
list_display = ('title', 'href', 'status', 'weight', 'created_time')
fields = ('title', 'href', 'status', 'weight')
def save_model(self, request, obj, form, change):
obj.owner = request.user
return super().save_model(request, obj, form, change)
@admin.register(SideBar)
class SideBarAdmin(admin.ModelAdmin):
list_display = ('title', 'display_type', 'content', 'created_time')
fields = ('title', 'display_type', 'content')
def save_model(self, request, obj, form, change):
obj.owner = request.user
return super().save_model(request, obj, form, change)
3.1.4 详细配置
通过上面的配置,我们已经得到了一个完善的内容管理后台,其中包含分类、标签、文章评论、侧边栏、友链的管理。算下来没几行代码,但是这足以供你完成简单的内容管理了。比如,你只是想做一个联系人的管理后台,那么到这一步就够了。但是我们需要做的更多,所以还要继续。
现在的问题是页面展示还不够友好,因此我们需要进行更多配置。
我们先梳理一下需要哪些定制,但在梳理之前需要先扮演用户,去使用自己的产品。看看有哪些别扭的地方,然后再改进。在正式开发中,程序员往往喜欢开发完一个功能,赶紧拿给产品经理或者老大看。潜台词是:我的开发效率高吧,快夸我!这时往往会得到产品经理或者老大的一顿鄙视,仅仅实现程序逻辑远不是一个优秀程序员的追求,我们需要创建的是易用、易维护的项目/产品。因此,把自己当作用户先去体验一下产品的流程,然后确认没问题了再拿出去。
假设我们要发布一篇文章,需要怎么做呢?步骤如下:
- 新建文章;
- 填写标题和内容,选择分类,选择标签;
- 保存文章;
- 查看文章;
- 查看某个分类或者标签下有多少文章;
- 查看最新的文章。
根据页面来分的话,其实也就两个页面,一是文章列表页,二是文章编辑页面。
列表页需要展示哪些内容呢?如果你是用户,希望看到哪些内容呢?标题,内容?考虑好这些,然后进行配置。
编辑页面呢?如何布局?用户写文章时,需要先写标题,还是先选择分类或者标签?
你可能觉得烦琐,不就是写个代码吗,考虑这么多产品逻辑作甚。
这里我们需要意识到的一件事是,我们做出来的东西,别人看到的永远只是界面而已,用起来是否舒服完全是感官上的东西。架构设计得多么合理,代码质量写得多么高,用户不会关心,也不知道。产品经理的评价可能是,这哥们做东西太马虎,只图快,用起来一塌糊涂。或者这哥们做东西就是细致,产品体验很好。
因此,优秀的程序员除了追求编写优雅的代码之外,还需要做出优雅的产品。
好了,说了这么多,目的只有一个:开发出优秀的产品。就像我们正在学习的Django一样,它对开发人员足够友好(充足的文档以及丰富的生态)。
3.1.5 总结
经过这一节,想必你已经得到了一个基本可用的管理系统。但是这还不够。请记住我们的追求。下一节中,再来学习更多的模块来优化项目。
本小节的内容我已上传到GitHub上了,分支名为03-admin,大家可以去GitHub上查看代码,也可以跟我一样把每一节每一章的内容都部署到git上。
这里再给大家展示一下本节部署完成后的页面效果(这里篇幅有限,只展示文章界面,其他界面后期再一一展示):
3.2 根据需求定制admin
上一节中,我们完成了基础的admin代码编写,已经得到了一个基本可用的内容管理系统。在这一节中,我们来说一下常用的定制行的操作,让大家有一个初步的认识,后面在实现需求时还会做更多讲解。
框架在设计时为了达到更好的通用性,会抽象出通用的逻辑,把一些可配置项暴露给用户,让用户可以通过简单的配置完成自己的需求。比方说,上一节配置的list_display 以及其他选项,配置起来都很简单。除了简单的配置项之外,一个好的框架在保证自身通用性的前提下,还会提供给我们定制的能力(或者说接口)。从这方面来说,Django也是一个优秀的框架。
其实我们自己思考一下,对于数据管理或者内容管理来说,我们需要操作的页面基本上只有两种:一是数据批量展示和操作的列表页,二是数据增加或者修改的编辑(新增)页。
接下来,我们来详细说一下如何定制这两部分页面,也看看Django给我们提供了哪些接口。
3.2.1 定义list页面
第一个需要定制的就是列表页,如何定制?这个需求谁来提?这是个问题。
开发人员一定要自己吃自己的“狗粮”(即要自己使用自己开发出来的系统)。把自己作为一个真实用户,去体验一下系统使用的流程,你就知道应该如何定制列表页,哪些信息是有用的,哪些信息是要隐藏的,哪些信息是只能自己查看的。这些你都得知道,然后才能去开发。
这也是你要自己写一个博客系统的原因。因为这将是你能持续参与使用和维护的一个真实项目。只有不断维护和改写,才能获得足够多的经验。
下面来看具体的定制细节。
上一节中的一些配置项不再多说,这里只补充两点。
- 其实在search_fielas 中已经用到了,就是通过 __(双下划线)的方式指定拽索关联Model 的数据,这种用法可以用于 list_display和 list_filter。
- list_filter 可以进行更多的自定义。除了配置字符串之外,还可以自定义类过滤器。具体定义的逻辑下面拆开来说。
- 自定义list_filter
自定义list_filter比较简单,看一下文档上的代码大概就知道怎么用了。我们需要做的是跟现在需要开发的系统结合起来。
运行系统之后,可以尝试在用户管理部分新添加几个用户,每个用户添加一下数据,然后在文章列表页就能看到如下图所示的结果。
从上图中能发现,当前登录用户是max,但是我却可以看到其他用户的文章。这是权限问题。作者应该只能看到自己的文章才对。
另外,上图右侧的过滤器中展示了非当前用户创建的分类——guest用户创建的分类1,这显然也是权限问题。
这就是需要解决的问题,可以通过定制Django提供给我们的接口来完成。
首先,完成右侧过滤器的功能,这时需要自定义过滤器。其使用方式很简单,文档上有很详细的说明和示例,这里直接编写代码,在 PostAdmin定义的上方定义如下代码:
# blog/admin.py
class CategoryOwnerFilter(admin.SimpleListFilter):
""" 自定义过滤器只展示当前用户分类 """
title = '分类过滤器'
parameter_name = 'owner_category'
def lookups(self, request, model_admin):
# print(Category.objects.filter(owner=request.user).values_list('id', 'name'))
# <QuerySet [(5, 'wdq'), (6, 'wwww')]> 打印下来的格式
return Category.objects.filter(owner=request.user).values_list('id', 'name')
def queryset(self, request, queryset):
# print(self.value())
# 6 (显示id)
category_id = self.value()
if category_id:
return queryset.filter(category__id=category_id)
return queryset
通过继承 Django admin 提供的SimpleListFilter 类来实现自定义过滤器,之后只需要把自定义过滤器配置到ModelAdmin中即可。这里先解释一下上面的代码。
SimpleListFilter 类提供了两个属性和两个方法来供我们重写。两个属性的作用顾名思义,title用于展示标题,parameter_name 就是查询时 URL参数的名字,比如查询分类id为1的内容时,URL后面的Query部分是?owner_category=1,此时就可以通过我们的过滤器拿到这个id,从而进行过滤。
两个方法的作用如下。
- lookups: 返回要展示的内容和查询用的id(就是上面Query用的)。
- queryset: 根据 URL Query 的内容返回列表页数据。比如如果 URL 最后的 Query是?owner_category=1,那么这里拿到的self.value()就是1,此时就会根据1来过滤QuerySet(这部分在第2章中已经介绍过)。这里的QuerySet 是列表页所有展示数据的合集,即post的数据集。
编写完之后,只需要把PostAdmin中的 list_filter 修改为:
# blog/admin.py
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'category', 'status', 'created_time', 'owner', 'operator']
list_display_links = []
list_filter = [CategoryOwnerFilter] # 修改部分!!!!!!!!!!!!!!!!
# 其余代码省略
就能让用户在侧边栏的过滤器中只看到自己创建的分类了。如下图所示:
- 自定义列表页数据
完成了list_filter 的定制,我们还需要继续定制,让当前登录的用户在列表页中只能看到自己创建的文章。怎么操作呢?
PostAdmin 继承自 admin.ModelAdmin,显然我们需要看ModelAdmin 提供了哪些方法,可以让我们来重写。
有两个地方可以查。一个是官方文档,从中可以查看ModelAdmin 提供的方法及其具体作用。另外一个就是Django源代码。不必对Django庞大的源码感到恐惧,只需要从你熟悉的地方开始就行。比如说 django/contrib/admin/options.py 这个模块,ModelAdmin 的定义就在其中,你可以看看它是怎么实现的,定义了哪些方法供开发者使用,定义了哪些属性可以让开发者配置。
我们来实现一下blog/admin.py,其中省略了部分代码:
# blog/admin.py
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'category', 'status', 'created_time', 'owner', 'operator']
list_display_links = []
list_filter = [CategoryOwnerFilter]
# 省略部分代码
def save_model(self, request, obj, form, change):
obj.owner = request.user
return super().save_model(request, obj, form, change)
# 新增代码如下!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.filter(owner=request.user)
这么写完之后,重启进程后(如果你用 python manage.py runserver方式启动的进程,那么进程会自动重启)刷新页面,观察结果。结果如下:
从这两个定制可以看出来,关于数据过滤的部分,只需要找到数据源在哪儿,也就是QuerySet 最终在哪儿生成,然后对其进行过滤即可。
列表页的处理就先介绍到这,再来看看编辑页面的定制。
3.2.2 编辑页面的配置
在上一节中,我们看到了部分展示,这里再重新梳理一遍。首先,需要明确在编辑页面中有哪些东西可以被定制,比如:
- 按钮位置;
- 哪些字段需要被用户填写,哪些不用填写甚至不用展示;
- 页面的字段展示顺序是不是能调整,展示位置是否能调整;
- 输入框的样式。
根据这些可能的需求,我们来一一处理。首先是按钮位置,在编辑页面中主要的按钮也就是“保存”,不过Django提供给我们另外两个便于操作的按钮:“保存并继续”以及“保存并新增另一个。
关于按钮的位置,有一个配置项可以完成:save_on_top 用来控制是否在页面顶部展示上述的三个按钮。
对于字段是否展示以及展示顺序的需求,可以通过fields 或者 fieldset 来配置。通过exclude可以指定哪些字段是不展示的,比如下面的owner,我们是在程序中自动赋值当前用户的。
代码如下:
# blog/admin.py
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
# 省略其他代码
# 新增代码如下!!!!
exclude = ('owner',) # 必须这么写,因为The value of 'exclude' must be a list or tuple.
fields = (
('category', 'title'),
'desc',
'status',
'content',
'tag',
)
# 省略其他代码
fields 配置有两个作用,一个是限定要展示的字段,另外一个就是配置展示字段的顺序。你可以将上面的代码放到项目中,看看页面展示效果。
接着,来看另外一项配置fieldsets,它用来控制页面布局。先来看代码示例,你可以把代码放到自己项目上看看效果,然后再看解释。我们用它来替预上述代码中的 fields:
# blog/admin.py
"""
fields = (
('category', 'title'),
'desc',
'status',
'content',
'tag',
)
上面和下面两种方法效果类似, 变动的地方都是在新增页面中显示,下面信息更全
"""
fieldsets = (
('基础配置', {
'description': '基础配置描述', # 这实际上不是Django admin标准fieldsets的一部分,但可以用作注释
'fields': (
('title', 'category'), # 这是一个字段对,通常用于将两个字段显示在同一行
'status', # 这是另一个字段,默认会单独显示在一行
),
}),
('内容', {
'fields': (
'desc', # 字段名,单独显示在一行
'content', # 另一个字段名,也单独显示在一行
),
}),
('额外信息', {
'classes': ('collapse',), # 这是一个元组,包含一个CSS类名,用于折叠该部分
'fields': (
'tag', # 字段名,单独显示在一行(在折叠部分)
),
}),
)
fieldsets 用来控制布局,要求的格式是有两个元素的tuple的 list,如:
fielasets = (
(名称,{内容}),
(名称,{内容}),
)
其中包含两个元素的tuple内容,第一个元素是当前版块的名称,第二个元素是当前版块的描述,字段和样式配置。也就是说,第一个元素是string,第二个元素是dict,而dict的key可以是’fields’、‘description’和’classes’。
fields 的配置效果同上面一样,可以控制展示哪些元素,也可以给元素排序并组合元素的位置。
classes 的作用就是给要配置的版块加上一些CSS属性,Django admin 默认支持的是collapse 和wide。当然,你也可以写其他属性,然后自己来处理样式。
最后,关于编辑页的配置,还有针对多对多字段展示的配置 filter_horizontal和filter_vertical,它们用来控制多对多字段的展示效果,你可以自行尝试,后面我们会通过其他插件来处理这种功能。
这两种配置方式也简单,同其他的一样,只需要设置哪些字段是横向展示的,哪些字段是纵向展示的即可:
# blog/admin.py
filter_horizontal = ('tag', ) # 横向展示
# 或者下面(二选一)
filter_vertical = ('tag', ) # 纵向展示
可以运行代码看看效果。
3.2.3 自定义Form
上面的所有配置都是基于ModelAdmin的。如果有更多的定制需求,应该怎么处理呢?比如说,我们希望文章描述字段能够以textarea(也就是多行多列的方式)展示,怎么处理呢?这其实属于展示层的定义。
这就需要用到ModelForm了,它的用法前面也介绍过。这里需要知道的是,我们目前看到以及用到的admin的页面,就是通过这些组件生成的,ModelForm就是其中一环。只是用到的是Django admn 默认的 Form 而已。
先在 blog的目录下新增一个文件adminforms.py。因为这是用作后台管理的Form,所以这里要命名为adminforms而不是forms。这只是为了跟前台针对用户输入进行处理的Form区分开来。接着,需要在里面编写代码,定义Form。关于Form的作用,之前有讲到:Form 跟 Model其实是耦合在一起的,或者说Form 跟Model的逻辑是一致的,Model是对数据库中字段的抽象,Form 是对用户输入以及Model中要展示数据的抽象。
在adminforms.py中,我们通过Form来定制status这个字段的展示:
# blog/adminforms.py(新增py文件)
from django import forms
class PostAdminForm(forms.ModelForm):
# 把摘要改为Textarea组件
desc = forms.CharField(widget=forms.Textarea, label='摘要', required=False)
上面就是完整的代码,接着将其配置到admin 定义中(详见 blog/admin.py):
# blog/admin.py
from blog.adminforms import PostAdminForm
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
form = PostAdminForm # 显示摘要改为Textarea组件
# 省略其他代码
好了,编写完上述代码后,刷新一下页面,就能看到文章描述字段已经改为Textarea组件了。
3.2.4 在同一页面编辑关联数据
了解了上面的定制,大部分需求应该都可以满足了。不过对于关联内容的管理,偶尔也需要考虑,比如下面的需求。
产品经理说:我们需要在分类页面直接编辑文章。
当然,这是一个伪需求。因为这种内置(inline)的编辑相关内容的操作更适合字段较少的Model。这里只是演示一下它的用法。在 blog/admin.py 文件中增加:
# blog/admin.py
# 新增!!!!!!!!!!!!!!!!
class PostInline(admin.TabularInline):
# 在分类的增加页面中可以对文章进行编辑
fields = ('title', 'desc') # 定义了在内联表单中要显示的字段(对文章中的标题和摘要进行编辑)
extra = 1 # 控制额外多几个
model = Post # 与Post关联
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
# 新增!!!!!!!!!!!
inlines = [PostInline] # 这个属性是一个列表,包含了要在Category编辑页面上显示的内联模型类。
编写完成后,启动程序,进入分类编辑页面,此时就能看到页面下方多了一个新增/编辑文章的组件。对于需要在一个页面内完成两个关联模型编辑的需求,使用inline admin方式非常合适。
3.2.5 定制site
大部分情况下,只需要一个site 就够了,一个site对应一个站点,这就像上面所有操作最终都反应在一个后台。当然,我们也可以通过定制 site 来实现一个系统对外提供多套admin 后台的逻辑。
如何区分不同的site 呢?一个 URL后面对应的就是一个site。看一下urls.py文件中的这段代码:path('admin/', admin.site.urls)
,这就对应了一个site。
那么,我们得到一个新需求:用户模块的管理应该跟文章分类等数据的管理分开。另外,我们也需要修改后台的默认展示。现在后台名称是“Django管理”,这看起来有点奇怪。
接下来,我们来实现这个需求。
从上面的代码也能看到,我们用的是Django提供的admin.site 模块,这里面的site其实是django.contrib.admin.AdminSite的一个实例。
因此,我们在typeidea/typeidea下新增py文件custom_site.py,custom_site.py中继承Adminsite来定义自己的site,其代码如下:
# typeidca/typeidea/custom_site.py
from django.contrib.admin import AdminSite
class CustomSite(AdminSite):
site_header = 'Typeidea' # 定义admin站点顶部导航栏中显示的标题
site_title = 'Typeidea 管理后台' # 定义了admin站点页面的<title>标签和页面头部(通常是在<h1>标签中)显示的标题。
index_title = '首页' # 定义了admin站点索引页面(即通常所说的“首页”)上显示的标题
custom_site = CustomSite(name='cus_admin')
接着,就需要修改所有App下register部分的代码了。下面以修改PostAdmin为例来介绍。
把@admin.register(Post)
修改为@admin.register(Post,site=custom_site)
即可。
所有的app下的admin.py都要修改!!!
说明: 我们需要在模块上面引入custom_site:from typeidea.custom_site import customsite
需要注意的是,上面用reverse方式来获取后台地址时,我们用到了admin这个名称,因此需要调整blog/admin.py的代码。
原代码:
# blog/admin.py
def operator(self, obj):
""" 新增编辑按钮 """
return format_html(
'<a href="{}">编辑</a>',
reverse('admin:blog_post_change', args=[obj.id])
)
operator.short_description = '操作'
修改为:
# blog/admin.py
def operator(self, obj):
""" 新增编辑按钮 """
return format_html(
'<a href="{}">编辑</a>',
reverse('cus_admin:blog_post_change', args=[obj.id]) # 修改地方!!!!!
)
operator.short_description = '操作'
接着,需要在urls.py文件中进行修改,其完整代码如下:
# typeidea/urls.py
from django.contrib import admin
from django.urls import path
from typeidea.custom_site import custom_site
urlpatterns = [
path('super_admin/', admin.site.urls, name='super-admin'),
path('admin/', custom_site.urls, name='admin'),
]
这样就有两套后台地址,一套用来管理用户,另外一套用来管理业务。需要理解的是,这两套系统都是基于一套逻辑的用户系统,只是我们在URL上进行了划分。
这么修改代码之后,可以再次看看页面有什么不同。除了上面简单的文案配置外,AdminSite中还提供了首页、登录页、密码修改等页面的重载接口。具体内容可以查看文档,这里不做过多介绍。
3.2.6 admin的权限逻辑以及SSO登录
如果在写上面那部分代码时,去看Django的文档或者源码的话,应该能看到部分关于权限的代码。
在日常开发中,权限管理是常规需求,虽然在博客系统开发中没提到。但作为后台开发的重点,这里还是要提一下。
在开发企业内部系统时,往往需要集成已有的SSO(Single Sign-On,单点登录)系统进来。集成登录的逻辑只需要参考Django默认的Settings的配置 AUTHENTICATION_BACKENDS 是如何实现的即可,并且Django也提供了详细的文档,告诉你如何定制第三方认证系统。
这里讲一下如果已经集成了SSO,那么权限部分的逻辑怎么处理。有两种方式,一种是在自定义的AUTHENTICATION_BACKEND中来做,另外一种就是在 Django admin中来做。先来看看Django admin 提供给我们的接口:
- has_add_permission
- has_change_permission
- has_delete_permission
- has_module_permission
一个ModelAdmin的配置就对应一个Model的数据管理页面(列表页、新增页、编辑页、删除页),所以ModelAdmin的配置中包含了这些权限的方法。
如果需要自己实现不同Model对应管理功能上的权限逻辑,可以通过重写上面的方法来实现。我们看一个简单的例子,比如需要判断某个用户是否有添加文章的权限,而权限的管理是在另外的系统上,只提供了一个接口:http://permission.sso.com/has perm?user=<用户标识>&perm code=<权限编码>。如果有权限,那么响应状态为200;如果没有权限,则为403。(这里面的地sso.com是随便写的,使用时需要替换为你们内部的SSO系统地址。)
这里我们来简单实现一下 has_add_permission:
import requests
from django.contrib.auth import get_permission_codename
PERMISSION_API = "http://permission.sso.com/has_perm?user={}&perm_code={}"
class PostAdmin(admin.ModelAdmin):
def has_add_permission(self, request):
opts = self.opts
codename = get_permission_codename('add', opts)
perm_code = "%s.%s" % (opts.app_label, codename)
resp = requests.get(PERMISSION_API.format(request.user.username, perm_code))
if resp.status_code == 200:
return True
else:
return False
这就是一个简单的实现,实际情况会稍微复杂些,不过大概流程是一样的。在实际中,需要双方统一用户标识以及权限编码,当然还有接口规范。
这种每次都通过接口去查询是否有权限的效率比较低。我们可以在用户登录之后把所有的权限从数据库中读取出来,保存到session或者缓存中,从而避免每次都去API查询是否有权限。但是需要注意的问题是,如果发生了权限变更,那么当前系统中的用户需要登出或者系统主动清理缓存后才会使新的权限生效。
3.2.7 总结
正如前面所说,当你有了Model之后,就有了一套CRUD的管理后台,这一节是一个直观的体验。现在你可能对admin还有点陌生,但是当你上手之后,会觉得这确实能减少很多后台开发的工作量。
随着使用的深入或者需求的变化,定制开发不可避免,因此我们需要做的是先熟读 Django
admin 部分的文档,理解其中模块之间的关系后,根据需要去查看源代码。
3.2.8 参考资料
自定义 admin site:
https://docs.djangoproject.com/zh-hans/4.2/ref/contrib/admin/#customizing-the-adminsite-class。
format_html的用法:
https://docs.djangoproject.com/zh-hans/4.2/ref/utils/#django.utils.html.format html。
admin list_filter定制:
https://docs.djangoproject.com/zh-hans/4.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter。
admin get_queryset接口:
https://docs.djangoproject.com/zh-hans/4.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_queryset。
3.3 抽取Admin基类
这一节中,我们整理一下admin的所有代码,来保证代码整洁。
之前我们在PostAdmin 中重写了save_model方法和get_gueryset方法,目的是设置文章作者以及当前用户只能看到自己的文章。除了文章管理之外,还有其他模块也需要这么处理。
最简单的方法就是,复制一下,然后粘贴过去。这种方式虽然快,但会导致同样的代码出现在各个地方,提高了代码的维护成本。这对于编写代码来说也是冗余的。因此,我们需要“懒”一点,让我们的维护变得简单。
因此,需要做一定程度的抽象。
3.3.1 抽象 author 基类
在日常开发中,经常会遇到这样的问题,同样的代码抑或是类似的逻辑遍布在项目各处,可能是之前有人通过复制完成的业务功能,也可能是自己编写的,恰巧逻辑一样。不管怎么说,这样的代码会导致维护成本提高。试想一下,如果有一天这样的逻辑需要修改,那你要修改多少个地方?就算你是维护这个项目很久的“老人”,也可能会疏忽、遗漏一两处,进而导致线上故障.
因此,在开发时,我们要时刻保持这样的理念——尽量降低后期的维护成本。如何降低呢?自然是降低修改代码时的负担,降低我们在修改一个需求时要修改的代码量,让后来的程序员在修改代码时不会被之前凌乱的代码“绊倒”。
话不多说,先来抽象出一个基类 BaseOwnerAdmin,这个类帮我们完成两件事:一是重写save 方法,此时需要设置对象的owner;二是重写get_queryset方法,让列表页在展示文章或者分类时只能展示当前用户的数据。
在typeidea目录下新增base_admin.py文件,下面来看具体代码:
# typeidea/base_admin.py文件
from django.contrib import admin
class BaseOwnerAdmin(admin.ModelAdmin):
"""
1. 用来自动补充文章、分类、标签、侧边栏、友链这些 Model的 owner字段
2. 用来针对 queryset过滤当前用户的数据
"""
exclude = ('owner', )
def get_queryset(self, request):
""" 用户只能看到自己创建的文章 """
qs = super().get_queryset(request)
# <QuerySet [<Post: Post object (6)>, <Post: Post object (5)>, ...]>
# 可以取qs.values('id', 'title')
# print(qs)
return qs.filter(owner=request.user)
def save_model(self, request, obj, form, change):
obj.owner = request.user # 当前已经登录的用户作为作者
return super().save_model(request, obj, form, change)
我们把这段代码放到base_admin.py文件中,跟custom_site.py同目录即可。之所以放这里,是因为所有的App都需要用到。
有了这个基类,接下来需要做的就是改造那些需要隔离不同用户数据的管理页面,只需要让对应的Admin类继承这个基类即可。这里我们列一下完整的blog/admin.py代码:
# blog/admin.py
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from blog.adminforms import PostAdminForm
from blog.models import Tag, Category, Post
from typeidea.base_admin import BaseOwnerAdmin
from typeidea.custom_site import custom_site
class PostInline(admin.TabularInline):
# 在分类的增加页面中可以对文章进行编辑
fields = ('title', 'desc') # 定义了在内联表单中要显示的字段(对文章中的标题和摘要进行编辑)
extra = 1 # 控制额外多几个
model = Post # 与Post关联
@admin.register(Category, site=custom_site)
class CategoryAdmin(BaseOwnerAdmin):
inlines = [PostInline] # 这个属性是一个列表,包含了要在Category编辑页面上显示的内联模型类。
list_display = ('name', 'status', 'is_nav', 'owner', 'created_time', 'post_count') # 页面上显示的字段
fields = ('name', 'status', 'is_nav') # 增加时显示的字段
def post_count(self, obj):
""" 统计文章数量 """
return obj.post_set.count()
post_count.short_description = '文章数量'
@admin.register(Tag, site=custom_site)
class TagAdmin(BaseOwnerAdmin):
list_display = ('name', 'status', 'owner', 'created_time')
fields = ('name', 'status')
class CategoryOwnerFilter(admin.SimpleListFilter):
""" 自定义过滤器只展示当前用户分类 """
title = '分类过滤器'
parameter_name = 'owner_category'
def lookups(self, request, model_admin):
# print(Category.objects.filter(owner=request.user).values_list('id', 'name'))
# <QuerySet [(5, 'wdq'), (6, 'wwww')]>
return Category.objects.filter(owner=request.user).values_list('id', 'name')
def queryset(self, request, queryset):
# print(self.value())
# 6 (显示id)
category_id = self.value()
if category_id:
return queryset.filter(category__id=category_id)
return queryset
@admin.register(Post, site=custom_site)
class PostAdmin(BaseOwnerAdmin):
form = PostAdminForm # 显示摘要改为Textarea组件
list_display = ['title', 'category', 'status', 'created_time', 'owner', 'operator']
list_display_links = []
list_filter = [CategoryOwnerFilter]
search_fields = ['title', 'category_name']
actions_on_top = True
actions_on_bottom = False
# 编辑页面
save_on_top = True
exclude = ('owner',) # 必须这么写,因为The value of 'exclude' must be a list or tuple.
"""
fields = (
('category', 'title'),
'desc',
'status',
'content',
'tag',
)
上面和下面两种方法效果类似, 变动的地方都是在新增页面中显示,下面信息更全
"""
fieldsets = (
('基础配置', {
'description': '基础配置描述', # 这实际上不是Django admin标准fieldsets的一部分,但可以用作注释
'fields': (
('title', 'category'), # 这是一个字段对,通常用于将两个字段显示在同一行
'status', # 这是另一个字段,默认会单独显示在一行
),
}),
('内容', {
'fields': (
'desc', # 字段名,单独显示在一行
'content', # 另一个字段名,也单独显示在一行
),
}),
('额外信息', {
'classes': ('collapse',), # 这是一个元组,包含一个CSS类名,用于折叠该部分
'fields': (
'tag', # 字段名,单独显示在一行(在折叠部分)
),
}),
)
# filter_horizontal = ('tag', ) # 横向展示
filter_vertical = ('tag',) # 纵向展示
def operator(self, obj):
""" 新增编辑按钮 """
return format_html(
'<a href="{}">编辑</a>',
reverse('cus_admin:blog_post_change', args=[obj.id])
)
operator.short_description = '操作'
config/admin.py也类似,下面给出完整代码:
# config/admin.py
from django.contrib import admin
from .models import Link, SideBar
from typeidea.custom_site import custom_site
from typeidea.base_admin import BaseOwnerAdmin
@admin.register(Link, site=custom_site)
class LinkAdmin(admin.ModelAdmin):
list_display = ('title', 'href', 'status', 'weight', 'created_time')
fields = ('title', 'href', 'status', 'weight')
@admin.register(SideBar, site=custom_site)
class SideBarAdmin(admin.ModelAdmin):
list_display = ('title', 'display_type', 'content', 'created_time')
fields = ('title', 'display_type', 'content')
大家也可以到本项目的GitHub 地址https://github.com/1273055646/typeidea/blob/03-admin/blog/admin.py 查看。但我建议先自己参考blog的admin 配置完成,最后手动编写代码,然后进行对比。学习的过程就是模仿-吸收-创造的过程,急于求成反而导致学习进度比别人慢。
3.3.2 总结
我们回顾一下这一节的内容,这里主要完成了后台的配置,可以进行基础的增、删、改、查操作。对后台代码进行一定程度的抽象,便于之后的代码编写。这一节看起来比较容易,没做太多处理。不过不要心急,后面我们会在admin层进行更多定制,以完成需求。
现在开始把其他App的代码完善一下。
3.4 记录操作日志
LogEntry 也是在后台开发中经常用到的模块,它在admin后台是默认开启的,如下图所示。
图中展示的就是变更记录的功能,每次修改文章时,都会记录下来。
在日常开发中,这也是非常常用的功能。比如,对于新闻类系统,我们需要知道这篇新闻是谁创建的,谁编辑的,谁发布的。因此,需要在后台记录所有用户(编辑)的操作记录。一方面是用来监督,另一方面可以用来做回滚。在Django中实现这个功能很简单,直接使用LogEntry模块即可。
3.4.1 使用LogEntry
前面我们学习了ModelAdmin的定制,其中日志记录的功能ModelAdmin本身就有。当我们新建一个实体(Post、Category、Tag等)时,它就会帮我们创建一条变更日志记录。当我们修改一条内容时,ModelAdmin 又会帮忙我们调用LogEntry来创建一条日志,记录一下这个变更。
ModelAdmin内部提供了两个方法,分别是 log_addition 和 log_change。在官方文档上是看不到这个介绍的,因为它们是内部使用的函数。其功能如命名一样,一个是记录新增日志,一个是记录变更日志。我们可以看一下它们的定义来学习LogEntry的用法:
# 代码位置:django/contrib/admin/options.py
# django内置代码
def log_addition(self, request, obj, message):
"""
Log that an object has been successfully added.
The default implementation creates an admin LogEntry object.
"""
from django.contrib.admin.models import ADDITION, LogEntry
return LogEntry.objects.log_action(
user_id=request.user.pk,
content_type_id=get_content_type_for_model(obj).pk,
object_id=obj.pk,
object_repr=str(obj),
action_flag=ADDITION,
change_message=message,
)
def log_change(self, request, obj, message):
"""
Log that an object has been successfully changed.
The default implementation creates an admin LogEntry object.
"""
from django.contrib.admin.models import CHANGE, LogEntry
return LogEntry.objects.log_action(
user_id=request.user.pk,
content_type_id=get_content_type_for_model(obj).pk,
object_id=obj.pk,
object_repr=str(obj),
action_flag=CHANGE,
change_message=message,
)
这是摘自Django4.2 版本的代码,如果你有兴趣看代码的话,会发现相邻位置还有log_deletion的定义。不过内容大同小异,从上面的代码也能看出来。
这两个方法均调用了LogEntry.objects.log_action方法,只是参数略有不同。可以看到,如果需要自定义变更记录的话,只需要传递对应的参数即可。这里简要介绍一下这些参数。
-
user_id:当前用户id。
-
content_type_id: 要保存内容的类型,上面的代码中使用的是get_content_type_for_model 方法拿到对应 Model的类型 id。这可以简单理解为ContentType 为每个Model定义了一个类型id。
-
object_id: 记录变更实例的id,比如PostAdmin中它就是post.id。
-
object_repr:实例的展示名称,可以简单理解为我们定义的__str__所返回的内容。
-
action_flag: 操作标记。admin的Model里面定义了几种基础的标记:ADDITION、CHANGE 和DELETION。它用来标记当前参数是数据变更、新增,还是删除。
-
change_message: 这是记录的消息,可以自行定义。我们可以把新添加的内容放进去(必要时可以通过这里来恢复),也可以把新旧内容的区别放进去。
理解了这几个参数,如果遇到类似的需求,你就能直接使用Django现成的工具来完成了。
3.4.2 查询某个对象的变更
上面我们知道如何记录某个对象的变更日志了,那么问题来了,如何查询已经记录的变更呢?
其实这是简单的Model查询问题。假设我们记录的对象是Post的操作,现在来获取Post中id为1的所有变更日志,大概代码如下:
from django.contrib.admin.models import LogEntry, CHANGE
from django.contrib.admin.options import get_content_type_for_model
post = Post.objects.get(id=1)
log_entries = LogEntry.objects.filter(
content_type_id=get_content_type_for_model (post).pk,
object_id=post.id,
)
这样我们就拿到了文章id为1的所有变更记录了。
3.4.3 在admin页面上查看操作日志
我们既知道如何记录变更日志,也知道如何获取变更日志,那么如何才能够在admin后台方便地查看操作日志呢?
这其实就是简单配置admin的事儿了。我们可以在blog/admin.py中新增这个页面。虽然更合适的位置应该是在typeidea对应的/admin.py下面,不过我们将其暂放到blog/admin.py中。
新增如下配置:
# blog/admin.py
# 最上面增加import
from django.contrib.admin.models import LogEntry
#文件最下方增加
@admin.register(LogEntry, site=custom_site)
class LogEntryAdmin(admin.ModelAdmin):
list_display = ['object_repr', 'object_id', 'action_flag', 'user', 'change_message']
这样就可以看到所有的变更记录了。当然,这个管理的权限应该只有超级用户才有,因为这里可以看到所有用户的操作记录。当然,如果需要配置其他用户可见,但是又不想设置他为管理员的话,可以通过我们拆出来的super_admin后台对某个用户进行权限配置。
3.5 本章总结
admin的配置其实比较简单,我们需要做的是了解Django admin提供了哪些功能,然后直接使用即可。
另外,需要意识到的一件事就是,admin 本身也是基于Django的内置功能开发的,结合了Template、Form、Model和 View 这些模块。这是一个典型的Django的MTV模式的用法,其中我们配置的admin部分就是View层。因此,在开发其他页面时,可以参考admin的逻辑。
在所有代码中我会尽量带上注释,这些注释也是我在学习过程中不太懂并且有疑惑的地方,大家在学习过程中如果遇到不懂的点(比如该接口或函数的用法是什么?有什么意义?),可以去官网查看文档或者直接问AI。
本项目的GitHub 地址:https://github.com/1273055646/typeidea/tree/03-admin