一个虚拟主机做2个网站,wordpress域名重定义,discuz企业网站模板,企业所得税法前言#xff1a; 在上一章中#xff0c;我们对Django的Model层有了比较全面的认识#xff0c;本章就来配置Django自带的admin。这里需要认识到#xff0c;Django的Model层是很重要的一环#xff0c;无论是对于框架本身还是对于基于Django框架开发的大多数系统而言。因为一…前言 在上一章中我们对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 adminfrom blog.models import Tag, Categoryadmin.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 adminfrom blog.models import Tag, Categoryadmin.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.userreturn 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_htmlfrom blog.models import Tag, Category, Postadmin.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.userreturn 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 Trueactions_on_bottom False# 编辑页面save_on_top Truefields ((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.userreturn 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_htmlfrom 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 保存、编辑、编辑并新建按钮是否在顶部展示。 除了这些ModelAdminDjango还提供了很多其他配置具体可以通过文档查看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.nameclass Tag(models.Model):# 省略其他代码def __str__(self):return self.nameclass Post(models.Model):# 省略其他代码def __str__(self):return self.titleModelAdmin总结 好了到此为止一个简单的管理后台就配置好了我们稍稍总结一下。 你应该能够体会到“当你有了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 adminfrom .models import Commentadmin.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 adminfrom .models import Link, SideBaradmin.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.userreturn 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.userreturn 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_categorydef lookups(self, request, model_admin):# print(Category.objects.filter(ownerrequest.user).values_list(id, name))# QuerySet [(5, wdq), (6, wwww)] 打印下来的格式return Category.objects.filter(ownerrequest.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__idcategory_id)return queryset通过继承 Django admin 提供的SimpleListFilter 类来实现自定义过滤器之后只需要把自定义过滤器配置到ModelAdmin中即可。这里先解释一下上面的代码。 SimpleListFilter 类提供了两个属性和两个方法来供我们重写。两个属性的作用顾名思义title用于展示标题parameter_name 就是查询时 URL参数的名字比如查询分类id为1的内容时URL后面的Query部分是owner_category1此时就可以通过我们的过滤器拿到这个id从而进行过滤。 两个方法的作用如下。
lookups 返回要展示的内容和查询用的id就是上面Query用的。queryset 根据 URL Query 的内容返回列表页数据。比如如果 URL 最后的 Query是owner_category1那么这里拿到的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.userreturn super().save_model(request, obj, form, change)# 新增代码如下def get_queryset(self, request): qs super().get_queryset(request)return qs.filter(ownerrequest.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.pyfields ((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 formsclass PostAdminForm(forms.ModelForm):# 把摘要改为Textarea组件desc forms.CharField(widgetforms.Textarea, label摘要, requiredFalse)上面就是完整的代码接着将其配置到admin 定义中详见 blog/admin.py):
# blog/admin.py
from blog.adminforms import PostAdminFormadmin.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.pycustom_site.py中继承Adminsite来定义自己的site其代码如下
# typeidca/typeidea/custom_site.py
from django.contrib.admin import AdminSiteclass CustomSite(AdminSite):site_header Typeidea # 定义admin站点顶部导航栏中显示的标题site_title Typeidea 管理后台 # 定义了admin站点页面的title标签和页面头部通常是在h1标签中显示的标题。index_title 首页 # 定义了admin站点索引页面即通常所说的“首页”上显示的标题custom_site CustomSite(namecus_admin)接着就需要修改所有App下register部分的代码了。下面以修改PostAdmin为例来介绍。 把admin.register(Post)修改为admin.register(Postsitecustom_site)即可。
所有的app下的admin.py都要修改!!! 说明 我们需要在模块上面引入custom_sitefrom typeidea.custom_site import customsite 需要注意的是上面用reverse方式来获取后台地址时我们用到了admin这个名称因此需要调整blog/admin.py的代码。 原代码
# blog/admin.pydef operator(self, obj): 新增编辑按钮 return format_html(a href{}编辑/a,reverse(admin:blog_post_change, args[obj.id]))operator.short_description 操作修改为
# blog/admin.pydef 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 pathfrom typeidea.custom_site import custom_siteurlpatterns [path(super_admin/, admin.site.urls, namesuper-admin),path(admin/, custom_site.urls, nameadmin),
] 这样就有两套后台地址一套用来管理用户另外一套用来管理业务。需要理解的是这两套系统都是基于一套逻辑的用户系统只是我们在URL上进行了划分。 这么修改代码之后可以再次看看页面有什么不同。除了上面简单的文案配置外AdminSite中还提供了首页、登录页、密码修改等页面的重载接口。具体内容可以查看文档这里不做过多介绍。 3.2.6 admin的权限逻辑以及SSO登录 如果在写上面那部分代码时去看Django的文档或者源码的话应该能看到部分关于权限的代码。 在日常开发中权限管理是常规需求虽然在博客系统开发中没提到。但作为后台开发的重点这里还是要提一下。 在开发企业内部系统时往往需要集成已有的SSOSingle Sign-On单点登录系统进来。集成登录的逻辑只需要参考Django默认的Settings的配置 AUTHENTICATION_BACKENDS 是如何实现的即可并且Django也提供了详细的文档告诉你如何定制第三方认证系统。 这里讲一下如果已经集成了SSO那么权限部分的逻辑怎么处理。有两种方式一种是在自定义的AUTHENTICATION_BACKEND中来做另外一种就是在 Django admin中来做。先来看看Django admin 提供给我们的接口
has_add_permissionhas_change_permissionhas_delete_permissionhas_module_permission 一个ModelAdmin的配置就对应一个Model的数据管理页面列表页、新增页、编辑页、删除页)所以ModelAdmin的配置中包含了这些权限的方法。 如果需要自己实现不同Model对应管理功能上的权限逻辑可以通过重写上面的方法来实现。我们看一个简单的例子比如需要判断某个用户是否有添加文章的权限而权限的管理是在另外的系统上只提供了一个接口http://permission.sso.com/has perm?user用户标识perm code权限编码。如果有权限那么响应状态为200如果没有权限则为403。这里面的地sso.com是随便写的使用时需要替换为你们内部的SSO系统地址。 这里我们来简单实现一下 has_add_permission:
import requestsfrom django.contrib.auth import get_permission_codenamePERMISSION_API http://permission.sso.com/has_perm?user{}perm_code{}class PostAdmin(admin.ModelAdmin):def has_add_permission(self, request):opts self.optscodename 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 adminclass 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(ownerrequest.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_htmlfrom blog.adminforms import PostAdminForm
from blog.models import Tag, Category, Post
from typeidea.base_admin import BaseOwnerAdmin
from typeidea.custom_site import custom_siteclass PostInline(admin.TabularInline):# 在分类的增加页面中可以对文章进行编辑fields (title, desc) # 定义了在内联表单中要显示的字段对文章中的标题和摘要进行编辑extra 1 # 控制额外多几个model Post # 与Post关联admin.register(Category, sitecustom_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, sitecustom_site)
class TagAdmin(BaseOwnerAdmin):list_display (name, status, owner, created_time)fields (name, status)class CategoryOwnerFilter(admin.SimpleListFilter): 自定义过滤器只展示当前用户分类 title 分类过滤器parameter_name owner_categorydef lookups(self, request, model_admin):# print(Category.objects.filter(ownerrequest.user).values_list(id, name))# QuerySet [(5, wdq), (6, wwww)]return Category.objects.filter(ownerrequest.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__idcategory_id)return querysetadmin.register(Post, sitecustom_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 Trueactions_on_bottom False# 编辑页面save_on_top Trueexclude (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 adminfrom .models import Link, SideBar
from typeidea.custom_site import custom_site
from typeidea.base_admin import BaseOwnerAdminadmin.register(Link, sitecustom_site)
class LinkAdmin(admin.ModelAdmin):list_display (title, href, status, weight, created_time)fields (title, href, status, weight)admin.register(SideBar, sitecustom_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, LogEntryreturn LogEntry.objects.log_action(user_idrequest.user.pk,content_type_idget_content_type_for_model(obj).pk,object_idobj.pk,object_reprstr(obj),action_flagADDITION,change_messagemessage,)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, LogEntryreturn LogEntry.objects.log_action(user_idrequest.user.pk,content_type_idget_content_type_for_model(obj).pk,object_idobj.pk,object_reprstr(obj),action_flagCHANGE,change_messagemessage,)这是摘自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_modelpost Post.objects.get(id1)
log_entries LogEntry.objects.filter(content_type_idget_content_type_for_model (post).pk,object_idpost.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, sitecustom_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