9. Django Admin后台系统

发布于:2024-05-08 ⋅ 阅读:(52) ⋅ 点赞:(0)

image-20240506224332773

9. Admin后台系统

Admin后台系统也称为网站后台管理系统, 主要对网站的信息进行管理, 
如文字, 图片, 影音和其他日常使用的文件的发布, 更新, 删除等操作,
也包括功能信息的统计和管理, 如用户信息, 订单信息和访客信息等.
简单来说, 它是对网站数据库和文件进行快速操作和管理的系统, 以使网页内容能够及时得到更新和调整.

9.1 走进Admin

当一个网站上线之后, 网站管理员通过网站后台系统对网站进行管理和维护.
Django已内置Admin后台系统, 在创建Django项目的时候, 
可以从配置文件settings.py中看到项目已默认启用Admin后台系统, 如图9-1所示.

image-20240501234340999

9-1 Admin配置信息
从图9-1中看到, 在INSTALLED_APPS中已配置了Admin后台系统, 
如果网站不需要Admin后台系统, 就可以将配置信息删除, 这样可以减少程序对系统资源的占用.
此外, 在MyDjango的urls.py中也可以看到Admin后台系统的路由信息, 
只要运行MyDjango并在浏览器上输入: 127.0.0.1:8000/admin , 就能访问Admin后台系统, 如图9-2所示.

image-20240501234541537

9-2 Admin登录页面
在访问Admin后台系统时, 需要用户的账号和密码才能登录后台管理页面.
创建用户的账号和密码之前, 必须确保项目已执行数据迁移, 在数据库中已创建相应的数据表.
以MyDjango项目为例, 项目的数据表如图9-3所示.
# 执行数据迁移命令:
python manage.py makemigrations
python manage.py migrate

image-20240501235114018

image-20240501235231000

9-3 数据表信息
如果Admin后台系统以英文的形式显示, 那么我们还需要在项目的settings.py中设置中间件MIDDLEWARE, 将后台内容以中文形式显示.
添加的中间件是有先后顺序的, 具体可回顾2.5, 如图9-4所示.
# MyDjango 的 settings.py
# 添加中间件LocaleMiddleware
'django.middleware.locale.LocaleMiddleware',

image-20240502000241174

9-4 设置中文显示
完成上述设置后, 下一步创建超级管理员的账号和密码, 创建方法由Django的内置指令createsuperuser完成.
在PyCharm的Terminal模式下输入创建指令, 代码如下:
D:\MyDjango> python manage.py createsuperuser
Username (leave blank to use 'blue'): admin
Email address:
Password: 123456
Password (again): 123456
This password is too short. It must contain at least 8 characters.
This password is too common.
This password is entirely numeric.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

image-20240502000827671

在创建用户时, 用户名和邮箱地址可以为空, 如果用户名为空, 就默认使用计算机的用户名, 
而设置用户密码时, 输入的密码不会显示在屏幕上.
如果密码过短, Django就会提示密码过短并提示是否继续创建.
若输入'Y', 则强制创建用户; 若输入'N', 则重新输入密码.
完成用户创建后, 打开数据表auth_user可以看到新增了一条用户信息, 如图9-5所示.

image-20240502000953176

9-5 数据表auth_user
在浏览器上再次访问Admin的路由地址, 在登录页面上使用刚刚创建的账号和密码登录, 即可进入Admin后台系统, 如图9-6所示.

image-20240502001201840

image-20240502001225913

9-6 Admin后台系统
在Admin后台系统中可以看到, 网页布局分为站点管理, 认证和授权, 用户和组, 分别说明如下:
(1) 站点管理是整个Admin后台的主体页面, 整个项目的App所定义的模型都会在此页面显示.
(2) 认证和授权是Django内置的用户认证系统, 包括用户信息, 权限管理和用户组设置等功能.
(3) 用户和组是认证和授权所定义的模型, 分别对应数据表auth_user和auth_user_groups.
在MyDjango中, 项目应用index定义模型PersonInfo和Vocation, 分别对应数据表index_personinfo和index_vocation.
# index 的 models.py
from django.db import models


# 定义人员信息类
class PersonInfo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20)
    age = models.IntegerField()

    # 打印记录时展示用户
    def __str__(self):
        return str(self.name)

    # 定义模型对象的元数据
    class Meta:
        # admin中展示的表名称
        verbose_name = '人员信息'


# 定义职业信息表
class Vocation(models.Model):
    id = models.AutoField(primary_key=True)
    job = models.CharField(max_length=20)
    title = models.CharField(max_length=20)
    salary = models.DecimalField(max_digits=10, decimal_places=2)
    person_info = models.ForeignKey(PersonInfo, on_delete=models.CASCADE)

    # 打印记录时展示id
    def __str__(self):
        return str(self.id)

    class Meta:
        verbose_name = '职业信息'

image-20240502190435664

# 执行数据迁移命令:
python manage.py makemigrations
python manage.py migrate

image-20240502001539314

若想将index定义的模型展示在Admin后台系统中, 则需要在index的admin.py中编写相关代码, 以模型PersonInfo为例, 代码如下:
# index的admin.py
from django.contrib import admin
from .models import *


# 方法一:
# 将模型直接注册到admin后台
# admin.site.register(PersonInfo)

# 方法二:
# 自定义PersonInfoAdmin类并继承ModelAdmin
# 注册方法一, 使用装饰器将PersonInfoAdmin和Product绑定
@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin):
    # 设置显示的字段
    list_display = ['id', 'name', 'age']
# 注册方法二
# admin.site.register(PersonInfo, PersonInfoAdmin)

image-20240502190342084

上述代码使用两种方法将数据表index_personinfo注册到Admin后台系统: 方法一是基本的注册方式; 方法二是通过类的继承方式实现注册.
日常开发普遍采用第二种方法实现, 实现过程如下:
(1) 自定义PersonInfoAdmin类, 使其继承ModelAdmin. ModelAdmin用于设置模型如何展现在Admin后台系统里.
(2) 将PersonInfoAdmin类注册到Admin后台系统有两种方法, 两者是将模型PersonInfo和PersonInfoAdmin类绑定并注册到Admin后台系统.

当刷新Admin后台系统页面, 看到站点管理出现INDEX, 就代表项目应用index;
INDEX下的'人员信息s'代表模型PersonInfo, 它对应数据表index_personinfo, 如图9-7所示.

image-20240502125754096

9-7 Admin后台系统
单击'人员信息s', 浏览器将访问模型PersonInfo的数据列表页, 模型PersonInfo的所有数据以分页的形式显示, 每页显示100行数据;
数据列表页还设置了新增数据, 修改数据和删除数据的功能,如图9-8所示。

2024-05-02_130326

9-8 数据列表项
若想在模型PersonInfo里新增数据, 则可单击模型PersonInfo的数据列表页'增加人员信息'或表格信息栏的'增加'按钮, 
浏览器就会进入数据新增页面, 用户在此页面添加数据并保存即可, 如图9-9所示.

2024-05-02_130511

 9-9数据新增页面
在模型PersonInfo的数据列表页里, 每行数据的ID字段都设有路由地址, 单击某行数据的ID字段,
浏览器就会进入当前数据的修改页面, 用户在此页面修改数据并保存即可, 如图9-10所示.

image-20240502131719078

9-10 数据修改页面
若想在模型PersonInfo里删除数据, 则可在数据列表页勾选需要删除的记录, 
然后选择执行的动作为'删除所有勾选的人员信息s', 再点击执行, 这是会进入确认删除页面, 如图9-11所示.

image-20240502132831589

image-20240502132923662

9-11 数据删除页面

9.2 源码分析ModelAdmin

简单了解Admin后台系统的网页布局后, 接下来深入了解ModelAdmin的定义过程, 在PyCharm里打开ModelAdmin的源码文件, 如图9-12所示.

image-20240502134623353

9-12 ModelAdmin的源码文件
从图9-12看到, ModelAdmin继承BaseModelAdmin, 而父类BaseModelAdmin的元类为MediaDefiningClass,
因此Admin系统的属性和方法来自ModelAdmin和BaseModelAdmin.

由于定义的属性和方法较多, 因此这里只说明日常开发中常用的属性和方法.
 fields: 由BaseModelAdmin定义, 格式为列表或元组, 在新增或修改模型数据时, 设置可编辑的字段.
 exclude: 由BaseModelAdmin定义, 格式为列表或元组, 在新增或修改模型数据时, 隐藏字段, 使字段不可编辑,
  同一个字段不能与fields共同使用, 否则提示异常.
 fieldsets: 由BaseModelAdmin定义, 格式为两元的列表或元组(列表或元组的嵌套使用), 改变新增或修改页面的网页布局,
  不能与fields和exclude共同使用, 否则提示异常.
 radio_fields: 由BaseModelAdmin定义, 格式为字典, 如果新增或修改的字段数据以下拉框的形式展示,
  那么该属性可将下拉框改为单选按钮.
 readonly_fields: 由BaseModelAdmin定义, 格式为列表或元组, 在数据新增或修改的页面设置只读的字段, 使字段不可编辑.
 ordering: 由BaseModelAdmin定义, 格式为列表或元组, 设置排序方式, 比如以字段id排序, ['id']为升序, ['-id']为降序.
 sortable_by: 由BaseModelAdmin定义, 格式为列表或元组, 设置数据列表页的字段是否可排序显示,
  比如数据列表页显示模型字段id, name和age, 如果单击字段name, 数据就以字段name进行升序(降序)排列,
  该属性可以设置某些字段是否具有排序功能.
 formfield_for_choice_field(): 由BaseModelAdmin定义, 如果模型字段设置choices属性,
  那么重写此方法可以更改或过滤模型字段的属性choices的值.
 formfield_for_foreignkey(): 由BaseModelAdmin定义, 如果模型字段为外键字段(一对一关系或一对多关系),
  那么重写此方法可以更改或过滤模型字段的可选值(下拉框的数据).
 formfield_for_manytomany(): 由BaseModelAdmin定义, 如果模型字段为外键字段(多对多关系),
  那么重写此方法可以更改或过滤模型字段的可选值.
 get_queryset(): 由BaseModelAdmin定义, 重写此方法可自定义数据的查询方式
 get_readonly_fields(): 由BaseModelAdmin定义, 重写此方法可自定义模型字段的只读属性,
  比如根据不同的用户角色来设置模型字段的只读属性.
 list_display: 由ModelAdmin定义, 格式为列表或元组, 在数据列表页设置显示在页面的模型字段.
 list_display_links: 由ModelAdmin定义, 格式为列表或元组, 为模型字段设置路由地址, 由该路由地址进入数据修改页.
 list_filter: 由ModelAdmin定义, 格式为列表或元组, 在数据列表页的右侧添加过滤器, 用于筛选和查找数据.
 list_per_page: 由ModelAdmin定义, 格式为整数类型, 默认值为100, 在数据列表页设置每一页显示的数据量.
 list_max_show_all: 由ModelAdmin定义, 格式为整数类型, 默认值为200, 在数据列表页设置每一页显示最大上限的数据量.
 list_editable: 由ModelAdmin定义, 格式为列表或元组, 在数据列表页设置字段的编辑状态,
  可以在数据列表页直接修改某行数据的字段内容并保存, 该属性不能与list_display_links共存, 否则提示异常信息.
 search_fields: 由ModelAdmin定义, 格式为列表或元组, 在数据列表页的搜索框设置搜索字段, 根据搜索字段可快速查找相应的数据.
 date_hierarchy: 由ModelAdmin定义, 格式为字符类型, 在数据列表页设置日期选择器, 只能设置日期类型的模型字段.
 save_as: 由ModelAdmin定义, 格式为布尔型, 默认为False, 若改为True, 则在数据修改页添加'另存为'功能按钮.
 actions: 由ModelAdmin定义, 格式为列表或元组, 列表或元组的元素为自定义函数, 函数在'动作'栏生成操作列表.
 actions_on_top和actions_on_bottom: 由ModelAdmin定义, 格式为布尔型, 设置'动作'栏的位置.
 save_model(): 由ModelAdmin定义, 重写此方法可自定义数据的保存方式.
 delete_model(): 由ModelAdmin定义, 重写此方法可自定义数据的删除方式.
为了更好地说明ModelAdmin的属性功能, 以MyDjango为例, 在index的admin.py里定义VocationAdmin.
在定义VocationAdmin之前, 我们需要将模型Vocation进行重新定义, 代码如下:
# index 的 models.py
from django.db import models


# 定义人员信息类
class PersonInfo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20)
    age = models.IntegerField()

    # 打印记录时展示用户
    def __str__(self):
        return str(self.name)

    # 定义模型对象的元数据
    class Meta:
        # admin中展示的表名称
        verbose_name = '人员信息'


# 定义职业信息表
class Vocation(models.Model):
    JOB = (
        ('软件开发', '软件开发'),
        ('软件测试', '软件测试'),
        ('需求分析', '需求分析'),
        ('项目管理', '项目管理'),
    )

    id = models.AutoField(primary_key=True)
    job = models.CharField(max_length=20, choices=JOB)
    title = models.CharField(max_length=20)
    salary = models.IntegerField(null=True, blank=True)
    person_info = models.ForeignKey(PersonInfo, on_delete=models.CASCADE)
    record_time = models.DateField(auto_now=True, null=True, blank=True)

    # 打印记录时展示id
    def __str__(self):
        return str(self.id)

    class Meta:
        verbose_name = '职业信息'

JOB表量的内部元组中的第一个值是标签的value值(提交的数据), 第二个值是被html标签包的值(页面展示).

image-20240502173545229

模型Vocation重新定义后, 在PyCharm的Terminal窗口下执行数据迁移, 
并在数据表index_personinfo和index_vocation中添加数据, 如图9-13所示.
(前面使用了index_personinfo表, 如果有数据自己清空, 后续添加数据注意自增id...)
-- 写入人员信息:
INSERT INTO index_personinfo VALUES
    (1, '张三', 26),
    (2, '李四', 23),
    (3, '王五', 28),
    (4, '赵六', 30);

-- 写入职业信息(遇到字段顺序不按定义的顺序排列的, 以后插入数据尽量写上字段名称):
INSERT INTO index_vocation VALUES
	( 1, '软件开发', 'Python开发',  2, '2019-01-02', 10000),
	( 2, '软件测试', '自动化测试', 3, '2019-03-20', 8000),
	( 3, '需求分析', '需求分析', 1, '2019-02-02', 6000),
	( 4, '项目管理', '项目经理', 4, '2019-04-04', 12000);

image-20240502143324281

9-12 数据表index_personinfo和index_vocation
完成模型Vocation的定义与数据迁移后, 下一步在admin.py里定义VocationAdmin, 使模型Vocation的数据显示在Admin后台系统.
VocationAdmin的定义如下:
# index 的 models.py
from django.contrib import admin
from .models import *


@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin):
    # 设置显示的字段
    list_display = ['id', 'name', 'age']


@admin.register(Vocation)
class VocationAdmin(admin.ModelAdmin):
    # 设置显示的字段
    # fields = ['job', 'title', 'salary', 'person_info']

    # 在数据新增或修改的页面设置不可编辑的字段
    # exclude = []

    # 改变新增或修改页面的网页布局
    fieldsets = (
        ('职业信息', {
             'fields': ('job', 'title', 'salary')
        }),

        ('人员信息', {
            # 设置隐藏与显示
            'classes': ('collapse',),
            'fields': ('person_info',),
        }),
    )

    # 将下拉框改为单选按钮
    # admin.HORIZONTAL 是水平排列
    # admin.VERTICAL 是垂直排列
    radio_fields = {'person_info': admin.HORIZONTAL}

    # 在数据新增或修改的页面设置可读的字段, 不可编辑
    readonly_fields = ['job', ]

    # 设置排序方式, ['id']为升序, ['-id']为降序
    ordering = ['id']

    # 设置数据列表页的每列数据是否可排序显示
    sortable_by = ['job', 'title']

    # 在数据列表页设置显示的模型字段
    list_display = ['id', 'job', 'title', 'salary', 'person_info']

    # 为数据列表页的字段id和job设置路由地址, 改路由可进入数据修改页
    # list_display_links = ['id', 'job']

    # 设置过滤器, 若有外键, 则应使用双下划线连接两个模型的字段(现在连接表的name字段信息)
    list_filter = ['job', 'title', 'person_info__name']

    # 实在数据列表页设置每一页显示的数据量
    list_per_page = 100

    # 在数据列表页设置每一页显示最大上限的数据量
    list_max_show_all = 200

    # 为数据列表页的字段job和title设置可编辑状态
    list_editable = ['job', 'title']

    # 设置可搜索的字段
    search_fields = ['job', 'title']

    # 在数据列表页设置日期筛选器
    date_hierarchy = 'record_time'

    # 在数据修改页面添加'另存为'功能
    save_as = True

    # 设置'动作栏'的设置
    actions_on_top = False
    actions_on_bottom = True
    

image-20240502173423175

VocationAdmin演示了如何使用ModelAdmin的常用属性.
运行MyDjango项目, 在浏览器上访问模型Vocation的数据列表页, 页面的样式和布局变化如图9-14所示.

2024-05-02_165929

9-14 模型Vocation的数据列表页
模型Vocation的数据列表页里单击某行数据的ID字段, 由ID字段的链接进入模型Vocation的数据修改页,
该页面的样式和布局的变化情况与数据新增页有相同之处, 如图9-15所示.

2024-05-02_172159

9-15 模型Vocation的数据修改页
最后在模型Vocation的数据列表页的右上方找到并单击'增加职业信息', 
浏览器将访问模型Vocation的数据新增页, 该页面的样式和布局的变化情况如图9-16所示.

image-20240502174639499

9-16 模型Vocation的数据新增页
对比模型PersonInfo与模型Vocation的Admin后台页面发现, 
ModelAdmin的属性主要设置Admin后台页面的样式和布局, 使模型数据以特定的形式展示在Admin后台系统.
而在9.4, 我们将会讲述如何重写ModelAdmin的方法, 实现Admin后台系统的二次开发.

9.3 Admin首页设置

我们将模型PersonInfo和模型Vocation成功展现在Admin后台系统, 其中Admin首页的INDEX代表项目应用的名称,
但对一个不会网站开发的使用者来说, 可能无法理解INDEX的含义, 而且使用英文表示会影响整个网页的美观.
若想将Admin首页的INDEX改为中文内容, 则在项目应用的初始化文件__init__.py中设置即可, 
以MyDjango的index为例, 在index的__init__.py中编写以下代码:
# index的__init__.py
from django.apps import AppConfig  # 导入apps的应用配置
import os

# 修改App在Admin后台显示的名称
# default_app_config用于指定默认的AppConfig子类
default_app_config = 'index.IndexConfig'


# 获取当前App的命名
def get_current_app_name(_file):
    return os.path.split(os.path.dirname(_file))[-1]


# 重写应用配置类IndexConfig
class IndexConfig(AppConfig):  	
    # 用于指定该配置类所对应的应用程序的名称(告诉Django这个AppConfig是关联到哪个Django应用的)
    name = get_current_app_name(__file__)  
    # 设置admin后台显示的名称
    verbose_name = '网站首页'  

image-20240502204008307

上述代码中, 变量default_app_config指向自定义的IndexConfig类,
该类的属性verbose_name用于设置当前项目应用在Admin后台的名称, 如图9-17所示.

2024-05-02_185735

9-17 设置App的后台名称
在Django中, 当将应用程序添加到INSTALLED_APPS设置时, 有几种方式来指定该应用程序.
如果你只想通过应用名称来注册应用, 并且该应用有一个默认的AppConfig子类(其名称遵循apps.AppConfig的命名模式), 
那么Django会自动加载它
假设有一个名为index的Django应用, 并且该应用在index/apps.py文件中定义了一个名为IndexConfig的AppConfig子类:
# index 的 apps.py
from django.apps import AppConfig  
  
class IndexConfig(AppConfig):  
    name = 'index'  
    
在这种情况下, 只需在INSTALLED_APPS中添加应用的名称index, 而不需要指定IndexConfig:
# settings.py
INSTALLED_APPS = [  
    # ...  
    'myapp',  
]

Django会自动查找myapp/apps.py中的IndexConfig(或任何遵循命名模式的AppConfig)并将其作为该应用的配置类.
如果想要明确地指定使用IndexConfig作为配置类, 可以在INSTALLED_APPS中这样添加:  
# settings.py
INSTALLED_APPS = [  
    # ...  
    'index.apps.IndexConfig',  
]

在这种情况下, Django将直接加载myapp.apps.IndexConfig作为myapp应用的配置类.
Django项目的settings.py文件的INSTALLED_APPS中, 通常只需要列出应用的名称(: 'index'),
但如果你想要使用自定义的AppConfig类, 可以通过点号路径来指定它.
通常不在 INSTALLED_APPS 中直接指定 AppConfig 的点号路径, 而是在应用的__init__.py 文件中设置default_app_config变量.
想要default_app_config变量生效, 确保在INSTALLED_APPS设置中, 应用是这样添加的不带".apps.IndexConfig"后缀!!!
否则他会使用apps中的IndexConfig, 这个类中直接添加: verbose_name = '网站首页' 也是可行的.

default_app_config是一个特殊的变量, 
用于告诉Django当该应用程序被添加到INSTALLED_APPS时, 应该使用哪个AppConfig子类作为默认配置.
在这里, 它被设置为'index.IndexConfig', 意味着Django会使用index应用程序中的IndexConfig类作为默认配置.

__file__ 它表示当前模块(文件)的完整路径, 目前为: D:\MyDjango\index\__init__.py
os.path.dirname(): 它返回指定文件或目录路径的目录名, D:\MyDjango\index\__init__.py --> D:\MyDjango\index .
os.path.split(): 返回一个包含两个元素的元组, 第一个是路径的目录部分(即最后一个目录分隔符之前的所有内容),
第二个是文件名或子目录名(即最后一个目录分隔符之后的内容).
".apps.IndexConfig"后缀, __init__.py的default_app_config变量是不生效的!!!

2024-05-02_184803

当Django加载INSTALLED_APPS列表中的应用程序时, 
它会查看每个应用程序的__init__.py文件, 检查是否存在default_app_config设置.
如果存在, Django就会使用指定的AppConfig子类来加载和配置该应用程序.
如果不存在default_app_config设置, Django会使用默认的AppConfig
(如果应用程序遵循Django的命名约定, 即apps.py文件中有一个名为AppConfig的类)
从图9-16看到, 模型PersonInfo和模型Vocation在Admin后台显示为'人员信息s''职业信息s',
这是由模型属性Meta的verbose_name设置, 若想将中文内容的字母s去掉, 则可以在模型的Meta属性中设置verbose_name_plural,
以模型PersonInfo为例, 代码如下:
# index 的 models.py
from django.db import models


# 定义人员信息类
class PersonInfo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20)
    age = models.IntegerField()

    # 打印记录时展示用户
    def __str__(self):
        return str(self.name)

    # 定义模型对象的元数据
    class Meta:
        # admin中展示的表名称
        verbose_name = '人员信息'
        verbose_name_plural = '人员信息'


# 定义职业信息表
class Vocation(models.Model):
    JOB = (
        ('软件开发', '软件开发'),
        ('软件测试', '软件测试'),
        ('需求分析', '需求分析'),
        ('项目管理', '项目管理'),
    )

    id = models.AutoField(primary_key=True)
    job = models.CharField(max_length=20, choices=JOB)
    title = models.CharField(max_length=20)
    salary = models.IntegerField(null=True, blank=True)
    person_info = models.ForeignKey(PersonInfo, on_delete=models.CASCADE)
    record_time = models.DateField(auto_now=True, null=True, blank=True)

    # 打印记录时展示id
    def __str__(self):
        return str(self.id)

    class Meta:
        verbose_name = '职业信息'
        verbose_name_plural = '职业信息'

image-20240502190641489

如果在模型的Meta属性中分别设置verbose_name和verbose_name_plural,
Django就优先显示verbose_name_plural的值.
重新运行MyDjango, 运行结果如图9-18所示.

image-20240502190739666

9-18 设置模型的后台名称
除了在Admin首页设置项目应用和模型的名称之外, 还可以设置Admin首页的网页标题, 
实现方法是在项目应用的admin.py中设置Admin的site_title和site_header属性, 
如果项目有多个项目应用, 那么只需在某个项目应用的admin.py中设置一次即可.
以index的admin.py为例, 设置如下:
# index 的 admin.py
# 在末尾面添加...
from django.contrib import admin
# 修改title的hender
admin.site.site_title = 'MyDjango后台管理'
admin.site.site_header = 'MyDjango'

image-20240502191425660

运行MyDjango并访问Admin首页, 观察网页的标题变化情况, 如图9-18所示.

2024-05-02_192116

9-18 Admin的网页标题
综上所述, Admin后台系统的首页设置包括: 项目应用的显示名称, 模型的显示名称和网页标题, 三者的设置方式说明如下:
 项目应用的显示名称: 在项目应用的__init__.py中设置变量default_app_config, 
  该变量指向自定义的IndexConfig类, 由IndexConfig类的verbose_name属性设置项目应用的显示名称.
 模型的显示名称: 在模型属性Meta中设置verbose_name和verbose_name_plural, 
  两者的区别在于verbose_name是以复数的形式表示的, 若在模型中同时设置这两个属性, 则优先显示verbose_name_plural的值. 
 网页标题: 在项目应用的admin.py中设置Admin的site_title和site_header属性, 
  如果项目有多个项目应用, 那么只需在某个项目应用的admin.py中设置一次即可.

9.4 Admin的二次开发

我们已经掌握了ModelAdmin的属性设置和Admin的首页设置, 但是每个网站的功能和需求并不相同, 这导致Admin后台的功能有所差异.
因此, 本节将重写ModelAdmin的方法, 实现Admin的二次开发, 从而满足多方面的开发需求.

为了更好地演示Admin的二次开发所实现的功能, 9.3节的MyDjango为例, 在Admin后台系统里创建非超级管理员账号.
在Admin首页的'认证和授权'下单击用户的新增链接, 设置用户名为root, 密码为mydjango123,
用户密码的长度和内容有一定的规范要求, 如果不符合要求就无法创建用户, 如图9-20所示.
用户创建后, 浏览器将访问用户修改页面, 我们需勾选当前用户的职员状态, 否则新建的用户无法登录Admin后台系统, 如图9-21所示.

2024-05-02_204413

9-20 创建用户

2024-05-02_204613

9-21 设置职员状态
除了设置职员状态之外, 还需要为当前用户设置相应的访问权限, 我们将Admin的所有功能的权限都给予root用户.
如图9-21所示, 最后单击'保存'按钮, 完成用户设置.

2024-05-02_204929

9-22 设置用户权限

9.4.1 函数get_readonly_fields()

已知get_readonly_fields()是由BaseModelAdmin定义的, 它获取readonly_fields的属性值, 
从而将模型字段设为只读属性, 通过重写此函数可以自定义模型字段的只读属性, 比如根据不同的用户角色来设置模型字段的只读属性.
(根据用户动态为模式设置readonly_fields只读属性的值.)
以MyDjango为例, 在VocationAdmin里重写get_readonly_fields()函数, 根据当前访问的用户角色设置模型字段的只读属性, 代码如下:
# index 的 admin.py
from django.contrib import admin
from .models import *

# 修改title的header
admin.site.site_title = 'MyDjango后台管理'
admin.site.site_header = 'MyDjango'


@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin):
    # 设置显示的字段
    list_display = ['id', 'name', 'age']


@admin.register(Vocation)
class VocationAdmin(admin.ModelAdmin):
    # 在数据列表页设置显示的模型字段
    list_display = ['id', 'job', 'title', 'salary']

    # 重写get_readonly_fields函数
    # 设置超级管理员和普通用户的权限
    def get_readonly_fields(self, request, obj=None):
        # 判断用户是否为超级管理员, 来设置只读字段
        if request.user.is_superuser:
            self.readonly_fields = []
        else:
            self.readonly_fields = ['salary']
        
        return self.readonly_fields

image-20240503123438882

request.user是一个常用的方式来获取当前请求的用户对象.
这个对象通常是User模型的一个实例, 它代表了登录到Django网站的用户.

request.user.is_superuser是一个布尔值(True  False), 它表示该用户是否是一个超级用户.
超级用户通常具有网站上的所有权限, 可以访问和修改所有内容.
在用户信息设置的权限中勾选了'超级用户状态'的用户都是超级用户.
函数get_readonly_fields首先判断当前发送请求的用户是否为超级管理员, 
如果符合判断条件, 就将属性readonly_fields设为空列表, 使当前用户具有全部字段的编辑权限;
如果不符合判断条件, 就将模型字段salary设为只读状态, 使当前用户无法编辑模型字段salary(只有只读权限).
函数参数request是当前用户的请求对象, 参数obj是模型对象, 默认值为None, 代表当前网页为数据新增页, 否则为数据修改页.
函数必须设置返回值, 并且返回值为属性readonly_fields, 否则提示异常信息.
运行MyDjango, 使用不同的用户角色登录Admin后台系统, 在模型Vocation的数据新增页或数据修改页看到,
不同的用户角色对模型字段salary的操作权限有所不同, 比如分别切换用户admin和root进行登录, 查看是否对模型字段salary具有编辑权限.
现在登入的用户是admin, 是超级用户, 可以在数据修改中对salary字段进行修改.

image-20240502210238626

登入root用户(虽然叫root, 可没有勾选超级用户状态), 不可以在数据修改中对salary字段进行修改.

2024-05-02_210543

目前普通用户是可以设置用户权限的...

9.4.2 设置字段样式

在Admin后台系统预览模型Vocation的数据信息时, 数据列表页所显示的模型字段是由属性list_display设置的,
每个字段的数据都来自于数据表, 并且数据以固定的字体格式显示在网页上.

若要对某些字段的数据进行特殊处理, 如设置数据的字体颜色,
则以模型Vocation的外键字段person_info为例, 将该字段的数据设置为不同的颜色, 实现代码如下:
# index 的 models.py
from django.db import models
from django.utils.html import format_html  # 格式化HTML字符串模块


# 定义人员信息类
class PersonInfo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20)
    age = models.IntegerField()

    # 打印记录时展示用户
    def __str__(self):
        return str(self.name)

    # 定义模型对象的元数据
    class Meta:
        # admin中展示的表名称
        verbose_name = '人员信息'
        verbose_name_plural = '人员信息'


# 定义职业信息表
class Vocation(models.Model):
    JOB = (
        ('软件开发', '软件开发'),
        ('软件测试', '软件测试'),
        ('需求分析', '需求分析'),
        ('项目管理', '项目管理'),
    )

    id = models.AutoField(primary_key=True)
    job = models.CharField(max_length=20, choices=JOB)
    title = models.CharField(max_length=20)
    salary = models.IntegerField(null=True, blank=True)
    person_info = models.ForeignKey(PersonInfo, on_delete=models.CASCADE)
    record_time = models.DateField(auto_now=True, null=True, blank=True)

    # 打印记录时展示id
    def __str__(self):
        return str(self.id)

    class Meta:
        verbose_name = '职业信息'
        verbose_name_plural = '职业信息'

    # 自定义函数(虚拟字段), 设置字体颜色
    def colored_name(self):
        if '张三' in self.person_info.name:
            color_code = 'red'
        else:
            color_code = 'blue'

        return format_html(
            '<span style="color: {}">{}</span>',
            color_code,
            self.person_info
        )

    # 设置虚拟字段colored_name在Admin中显示名称
    colored_name.short_description = '带颜色的姓名'

image-20240502220627626

short_description并不是一个通用的Python属性或方法,
而是Django为ModelAdmin类或其内联(inline)的字段定义提供的一个特殊属性.
这个属性用于自定义在admin页面上显示字段时的简短描述或标题.

Python的动态性: Python是一种动态类型的语言, 它允许你在运行时向对象添加属性.
函数是Python中的一等公民, 它们也是对象, 因此你可以给它们添加属性.
在模型Vocation的定义过程中, 我们自定义函数colored_name, 函数实现的功能说明如下: 
(1) 由于模型的外键字段person指向模型PersonInfo, 因此self.person_info.name可以获取模型PersonInfo的字段name.
(2) 通过判断模型字段name的值来设置变量color_code, 如果字段name的值为'张三',那么变量color_code等于red, 否则为blue.
(3) 将变量color_code和模型字段name的值以HTML表示, 这是设置模型字段name的数据颜色,
    函数返回值使用Django内置的format_html方法执行HTML转义处理.
(4) 为函数colored_name设置short_description属性, 使该函数以字段的形式显示在模型Vocation的数据列表页.
模型Vocation自定义函数colored_name是作为模型的虚拟字段, 它在数据表里没有对应的表字段, 数据由外键字段name提供.
若将自定义函数colored_name显示在Admin后台系统, 则可以在VocationAdmin的list_display属性中添加函数colored_name, 代码如下:
# 在属性list_display中添加自定义字段colored_name
# colored_name来自于模型Vocation
list_display.append('colored_name')
# 完整代码 index 的 admin.py
from django.contrib import admin
from .models import *

# 修改title的header
admin.site.site_title = 'MyDjango后台管理'
admin.site.site_header = 'MyDjango'


@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin):
    # 设置显示的字段
    list_display = ['id', 'name', 'age']


@admin.register(Vocation)
class VocationAdmin(admin.ModelAdmin):
    # 在数据列表页设置显示的模型字段
    list_display = ['id', 'job', 'title', 'salary']
    list_display.append('colored_name')

    # 重写get_readonly_fields函数
    # 设置超级管理员和普通用户的权限
    def get_readonly_fields(self, request, obj=None):
        # 判断用户是否为超级管理员, 来设置只读字段
        if request.user.is_superuser:
            self.readonly_fields = []
        else:
            self.readonly_fields = ['salary']

        return self.readonly_fields

image-20240502220513643

运行MyDjango, 在浏览器上访问模型Vocation的数据列表页, 发现该页面新增'带颜色的姓名'字段, 如图9-23所示.

image-20240502220759043

9-23 新增'带颜色的姓名'字段
虚拟字段可以直接定义在admin.py文件中, 示例如下:
from django.contrib import admin  
from django.utils.html import format_html  
from .models import MyModel  
  
class MyModelAdmin(admin.ModelAdmin):  
    list_display = ('name', 'colored_name')  
  
    def colored_name(self, obj):  
        # ... 逻辑代码 ...  
        return format_html('<span style="color: {}">{}</span>', color_code, obj.name)  
  
    # 设置Admin的字段名称  
    colored_name.short_description = '带颜色的姓名'  
  
admin.site.register(MyModel, MyModelAdmin)

9.4.3 函数get_queryset()

函数get_queryset()用于查询模型的数据信息, 然后在Admin的数据列表页展示.
默认情况下, 该函数执行全表数据查询, 若要改变数据的查询方式, 则可重新定义该函数,
比如根据不同的用户角色执行不同的数据查询, 以VocationAdmin为例, 实现代码如下:
# index 的 admin.py
from django.contrib import admin
from .models import *

# 修改title的header
admin.site.site_title = 'MyDjango后台管理'
admin.site.site_header = 'MyDjango'


@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin):
    # 设置显示的字段
    list_display = ['id', 'name', 'age']


@admin.register(Vocation)
class VocationAdmin(admin.ModelAdmin):
    # 在数据列表页设置显示的模型字段
    list_display = ['id', 'job', 'title', 'salary', 'colored_name']

    # 根据当前用户名设置数据访问权限
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs  # 返回全部数据
        else:
            return qs.filter(id__lt=2)  # 返回id<2的数据

image-20240502221915160

分析上述代码可知, 自定义函数get_queryset的代码说明如下:
(1) 通过super方法获取父类ModelAdmin的函数get_queryset所生成的模型查询对象, 该对象用于查询模型Vocation的全部数据.
(2) 判断当前用户角色, 如果为超级管理员, 函数就返回模型Vocation的全部数据, 否则返回模型字段id小于2的数据.
运行MyDjango, 使用普通用户(9.4节创建的root用户)登录Admin后台,
打开模型Vocation的数据列表页, 页面上只显示id等于1的数据信息, 如图9-24所示.

(定义 VocationAdmin类并覆盖get_queryset方法时, 告诉Django admin, 当渲染这个模型的列表页面时, 应该如何获取数据.
super().get_queryset(request) 调用父类( admin.ModelAdmin)中的get_queryset方法.
默认情况下, 这个方法会返回模型对应的QuerySet, 该QuerySet包含了模型在数据库中的所有对象.)

image-20240502222311521

9-24 模型Vocation的数据列表页

9.4.4 函数formfield_for_foreignkey()

在新增或修改数据的时候, 如果某个模型字段为外键字段, 该字段就显示为下拉框控件, 并且下拉框的数据来自于该字段所指向的另一个模型.
以模型Vocation的数据新增页为例, 该模型的外键字段person_info呈现方式如图9-25所示.

image-20240502231003159

9-25 模型Vocation的外键字段person info (不会显示下划线)
如果想要对下拉框的数据实现过滤筛选, 那么可以对函数formfield_for_foreignkey()进行重写,
如根据用户角色实现数据的过滤筛选, 以VocationAdmin为例, 实现代码如下:
# index 的 admin.py
from django.contrib import admin
from .models import *

# 修改title的header
admin.site.site_title = 'MyDjango后台管理'
admin.site.site_header = 'MyDjango'


@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin):
    # 设置显示的字段
    list_display = ['id', 'name', 'age']


@admin.register(Vocation)
class VocationAdmin(admin.ModelAdmin):
    # 在数据列表页设置显示的模型字段
    list_display = ['id', 'job', 'title', 'salary', 'colored_name']

    # 新增或修改数据时, 外键设置可选值(在新增或修改数据时执行这个函数)
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        # db_field 不是一个普通的Python对象, 而是一个Django模型字段对象,
        # 它包含了这个字段的类型, 名称, 相关模型, 关联字段等信息.
        # 通过db_field.name可以获取该外键字段在模型中的名称.
        # 判断是否为外键字段
        if db_field.name == 'person_info':
            # 判断是否为超级管理员
            if not request.user.is_superuser:
                # 过滤下拉框的数据
                v = Vocation.objects.filter(id__lt=2)
                kwargs['queryset'] = PersonInfo.objects.filter(id__in=v)

        return super().formfield_for_foreignkey(db_field, request, **kwargs)

当formfield_for_foreignkey方法被调用时, Django期望通过kwargs字典中的queryset键来获取QuerySet, 来设置外键值.

image-20240503084231250

上述代码根据不同的用户角色过滤筛选下拉框的数据内容, 实现过程如下:
(1) 参数db_field是模型Vocation的字段对象, 因为一个模型可以定义多个外键字段, 所以需要对特定的外键字段进行判断处理.
(2) 判断当前用户是否为超级管理员, 参数request是当前用户的请求对象.
    如果当前用户为普通用户, 就在模型Vocation中查询字段id小于2的数据v, 再将数据v作为模型PersonInfo的查询条件,
    将模型PersonInfo的查询结果传递给参数queryset, 该参数用于设置下拉框的数据. (查询职业id为{x1, x2..}的人员信息).
    因为外键字段person的数据主要来自模型PersonInfo, 所以参数queryset的值应以模型PersonInfo的查询结果为准.
(3) 将形参kwargs传递给父类的函数formfield_for_foreignkey(), 
    由父类的函数从形参kwargs里获取参数queryset的值, 从而实现数据的过滤筛选.
运行MyDjango, 使用普通用户(9.4节创建的root用户)登录Admin后台, 
打开模型Vocation的数据新增页或数据修改页, 外键字段person的数据如图9-26所示.
(修改职业表id为4的数据页面中, persin info字段绑定的是赵六, 可赵六现在被过滤了, 就显示为----空值, 
查看页面受formfield_for_foreignkey()函数的影响, 能正常显示外键.)

image-20240503085017161

9-26 外键字段person的数据列表
函数formfield_for_foreignkey()只适用于一对一或一对多的数据关系, 如果是多对多的数据关系, 
就可重写函数formfield_for_manytomany(), 两者的重写过程非常相似, 这里不再重复讲述.
def formfield_for_manytomany(self, db_field, request=None, **kwargs):  
    # 检查是否是特定的多对多字段  
    if db_field.name == 'my_m2m_field':  
        # 修改kwargs来改变表单字段的行为  
        kwargs['queryset'] = db_field.queryset.filter(active=True)  # 仅显示活动的对象  

    return super().formfield_for_manytomany(db_field, request, **kwargs)

9.4.5 函数formfield_for_choice_field()

如果模型字段设置了参数choices, 并且字段类型为CharField, 比如模型Vocation的job字段,
在Admin后台系统为模型Vocation新增或修改某行数据的时候, 模型字段job就以下拉框的形式表示,
它根据模型字段的参数choices生成下拉框的数据列表.
若想改变非外键字段的下拉框数据, 则可以重写函数formfield_for_choice_field().
以模型Vocation的字段job为例, 在Admin后台系统为字段job过滤下拉框数据, 实现代码如下:
# index 的 admin.py
from django.contrib import admin
from .models import *

# 修改title的header
admin.site.site_title = 'MyDjango后台管理'
admin.site.site_header = 'MyDjango'


@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin):
    # 设置显示的字段
    list_display = ['id', 'name', 'age']


@admin.register(Vocation)
class VocationAdmin(admin.ModelAdmin):
    # 在数据列表页设置显示的模型字段
    list_display = ['id', 'job', 'title', 'salary', 'colored_name']

    # db_field.choices获取模型字段的属性choices的值
    def formfield_for_choice_field(self, db_field, request, **kwargs):
        if db_field.name == 'job':
            # 减少字段job可选的选项
            kwargs['choices'] = (('软件开发', '软件开发'),
                                 ('软件测试', '软件测试'),)

        return super().formfield_for_choice_field(db_field, request, **kwargs)

image-20240503122501568

formfield_for_choice_field()函数设有3个参数, 每个参数说明如下:
 参数db_field代表当前模型的字段对象, 由于一个模型可定义多个字段, 因此需要对特定的字段进行判断处理.
 参数request是当前用户的请求对象, 可以从该参数获取当前用户的所有信息.
 形参**kwargs为空字典, 它可以设置参数widget和choices.
  widget是表单字段的小部件(表单字段的参数widget), 能够设置字段的CSS样式;
  choices是模型字段的参数choices, 可以设置字段的下拉框数据.
自定义函数formfield_for_choice_field()判断当前模型字段是否为job, 若判断结果为True, 则重新设置形参**kwargs的参数choices,
并且参数choices有固定的数据格式, 最后调用super方法使函数继承并执行父类的函数formfield_for_choice_field(),
这样能为模型字段job过滤下拉框数据.
运行MyDjango, 在Admin后台系统打开模型Vocation的数据新增页或数据修改页, 单击打开字段job的下拉框数据, 如图9-27所示.

image-20240503123140619

9-26 字段job的下拉框数据
formfield_for_choice_field()只能过滤已存在的下拉框数据, 
如果要对字段的下拉框新增数据内容, 只能自定义内置函数formfield_for_dbfield(), 
如果在admin.py都重写了formfield_for_dbfield()和formfield_for_choice_field(),
Django优先执行函数formfield_for_dbfield(), 然后再执行函数formfield_for_choice_field(),
所以字段的下拉框数据最终应以formfield_for_choice_field()为准.

9.4.6 函数save_model()

函数save_model()是在新增或修改数据的时候, 单击'保存'按钮所触发的功能, 该函数主要对输入的数据进行入库或修改处理.
若想在这个功能中加入一些特殊功能, 则可对函数save_model()进行重写.
比如对数据的修改实现日志记录, 以VocationAdmin为例, 函数save_model()的实现代码如下:
from django.contrib import admin
from .models import *

# 修改title的header
admin.site.site_title = 'MyDjango后台管理'
admin.site.site_header = 'MyDjango'


@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin):
    # 设置显示的字段
    list_display = ['id', 'name', 'age']


@admin.register(Vocation)
class VocationAdmin(admin.ModelAdmin):
    # 在数据列表页设置显示的模型字段
    list_display = ['id', 'job', 'title', 'salary']

    
    def save_model(self, request, obj, form, change):
        # 判断是否为修改操作
        if change:
            # 获取当前用户名
            user = request.user.username
            # 使用模型获取数据(Vocation模型的数据), pk代表具主键属性的字段
            job = self.model.objects.get(pk=obj.pk).job  # 获取旧数据中的job信息
            # 从from表单的清洗数据中获取外键表的name信息
            person_info = form.cleaned_data['person_info'].name
            # 写入日志文件
            f = open('d://log.tat', 'a')
            f.write(person_info + '职位:' + job + ', 被' + user + '修改' + '\r\n')
            f.close()

        else:
            pass

        # 使用super在继承父类已有功能的情况下新增自定义功能
        super().save_model(request, obj, form, change)

image-20240503164807670

form.cleaned_data['person_info'] 在Django表单处理中不会是一个外键值(比如一个整数ID)而是一个模型对象实例.
这是 Django 表单系统如何处理外键字段的一个关键特性.
普通表单只是保存外键对象的ID, 而Django Admin则会自动查询数据库, 将ID转换为完整的模型对象实例.

obj.person_info.age 能获取到外键表的对象.
通常不需要直接从form.cleaned_data中获取数据, 
因为obj已经包含了表单中的所有数据(这些数据在表单验证后已经设置到了obj的属性上).
save_model()函数设有4个参数, 每个参数说明如下: 
 参数request代表当前用户的请求对象.
 参数obj是模型的数据对象, 比如修改模型Vocation的某行数据(称为数据A),
  参数ojb代表数据A的数据对象, 如果为模型Vocation新增数据, 参数ojb就为None.
 参数form代表模型表单, 它是Django自动创建的模型表单,
  比如在模型Vocation里新增或修改数据, Django自动为模型Vocation创建表单VocationForm.
 参数change判断当前请求是来自数据修改页还是来自数据新增页, 
  如果来自数据修改页, 就代表用户执行数据修改, 参数change为True, 否则为False.
无论是修改数据还是新增数据, 都会调用函数save_model()实现数据保存, 因此函数会对当前操作进行判断, 
如果参数change为True, 就说明当前操作为数据修改, 否则为新增数据.

如果当前操作是修改数据, 就从函数参数request, obj和form里获取当前数据的修改内容, 然后将修改内容写入D盘的log.txt文件,
最后调用super方法使函数继承并执行父类的函数save_model(), 实现数据的入库或修改处理.
若不调用super方法, 则当执行数据保存时, 程序只执行日志记录功能, 并不执行数据入库或修改处理.
运行MyDjango, 使用超级管理员登录Admin后台并打开模型Vocation的数据修改页,
单击'保存'按钮实现数据修改, 在D盘下打开并查看日志文件log.txt, 如图9-28所示.

image-20240503162804667

9-28 日志文件log.txt
如果执行数据删除操作, Django就调用函数delete_model()实现, 
该函数设有参数request和obj, 参数的数据类型与函数save_model()的参数相同.
若要重新定义函数delete_model(), 则定义过程可参考函数save_model(), 在此就简单讲述.
from django.contrib import admin  
from .models import MyModel  
  
class MyModelAdmin(admin.ModelAdmin):  
    # ... 其他配置 ...  
  
    def delete_model(self, request, obj):  
        # 在这里添加删除前的自定义逻辑  
        # 例如, 可能想要记录日志, 或者执行一些清理工作  
  
        # 调用Django的默认删除逻辑  
        super().delete_model(request, obj)  
  
        # 可以在这里添加删除后的自定义逻辑  
        # 例如, 可能想要发送通知或者执行其他依赖于删除操作完成的任务  
  
admin.site.register(MyModel, MyModelAdmin)

在上面的例子中, delete_model方法首先执行一些自定义的逻辑(如果有的话),
然后调用父类(admin.ModelAdmin)的delete_model方法来执行实际的删除操作.
之后, 可以再添加一些删除后的逻辑.

注意, 虽然可以阻止默认的删除逻辑(即不调用 super().delete_model(request, obj)),
但这通常不是个好主意, 因为这样做会绕过Django的ORM系统, 可能会导致数据不一致或其他问题.

此外, 如果在 delete_model 中抛出了异常, Django Admin的删除操作将会失败, 并显示一个错误消息给用户.
这可以用于实现一些自定义的验证逻辑, 确保在删除之前满足某些条件.

9.4.7 数据批量操作

模型Vocation的数据列表页设有'动作', 单击'动作'栏右侧的下拉框可以看到数据删除操作.
只要选中某行数据前面的复选框, '动作'栏右侧的下拉框选择'删除所选的职业信息'并单击'执行'按钮, 即可实现数据删除, 如图9-29所示.

image-20240503164329692

9-29 删除数据
从上述的数据删除方式来看, 这种操作属于数据批量处理, 因为每次可以删除一行或多行数据, 
若想对数据执行批量操作, 则可在'动作'栏里自定义函数, 实现数据批量操作.
比如实现数据的批量导出功能, 以模型Vocation为例, 在VocationAdmin中定义数据批量导出函数, 代码如下:
# index 的 admin
from django.contrib import admin
from .models import *

# 修改title的header
admin.site.site_title = 'MyDjango后台管理'
admin.site.site_header = 'MyDjango'


@admin.register(PersonInfo)
class PersonInfoAdmin(admin.ModelAdmin):
    # 设置显示的字段
    list_display = ['id', 'name', 'age']


@admin.register(Vocation)
class VocationAdmin(admin.ModelAdmin):
    # 在数据列表页设置显示的模型字段
    list_display = ['id', 'job', 'title', 'salary']

    # 数据批量操作
    def get_datas(self, request, queryset):
        temp = []
        for d in queryset:  # 遍历被勾选的数据对象
            t = [d.job, d.title, str(d.salary), d.person_info.name]
            temp.append(t)

        f = open('d:/data.txt', 'a')
        for t in temp:
            f.write(','.join(t) + '\r\n')
        f.close()

        # 设置提示信息
        self.message_user(request, '数据导出成功!')

    # 设置函数的显示名称
    get_datas.short_description = '导出数据'

    # 添加到'动作'栏
    actions = ['get_datas']

数据批量操作函数get_datas可自行命名函数名, 参数request代表当前用户的请求对象, 参数queryset代表已被勾选的数据对象.
函数实现的功能说明如下:
(1) 遍历参数queryset, 从已被勾选的数据对象里获取模型字段的数据内容, 每行数据以列表t表示, 并且将列表t写入列表temp.
(2) 在D盘下创建data.txt文件, 并遍历列表temp, 将每次遍历的数据写入data.txt文件,
    最后调用内置方法message_user提示数据导出成功.
(3) 为函数get_datas设置short_description属性, 该属性用于设置'动作'栏右侧的下拉框的数据内容.
(4) 将函数get_datas绑定到ModelAdmin的内置属性actions, '动作'栏生成数据批量处理功能.
运行MyDjango, 在模型Vocation的数据列表页全选当前数据, 打开'动作'栏右侧的下拉框,
选择'导出所选数据', 单击'执行'按钮执行数据导出操作, 如图9-30所示.

2024-05-03_170532

9-30 数据批量导出
在D盘下打开并查看导出的数据文件data.txt.

image-20240503170657867

9.4.8 自定义Admin模板

Admin后台系统的模板文件是由Django提供的, 
在Django的源码目录下可以找到Admin模板文件所在的路径(django\contrib\admin\templates\admin).
如果想对Admin模板文件进行自定义更改, 那么可以直接修改Django内置的Admin模板文件, 但不提倡这种方法.

image-20240503175310798

如果一台计算机同时开发多个Django项目, 就会影响其他项目的使用.
除了这种方法之外, 还可以利用模板继承的方法实现自定义模板开发.
我们对MyDjango的目录架构进行调整, 如图9-31所示.

image-20240503182845755

9-31 MyDjango的目录架构
在模板文件夹templates下依次创建文件夹admin和index, 文件夹的作用说明如下:
 文件夹admin代表该文件夹里的模板文件用于Admin后台系统, 而且文件夹必须命名为admin.
 文件夹index代表项目应用index, 文件夹的命名必须与项目应用的命名一致.
  文件夹存放模板文件change_form.html, 所有在项目应用index中定义的模型都会使用该模板文件生成网页信息.
 如果将模板文件change_form.html放在admin文件夹下, 那么整个Admin后台系统都会使用该模板文件生成网页信息.

MyDjango的模板文件change_form.html来自Django内置模板文件, 我们根据内置模板文件的代码进行重写,
MyDjango的change_form.html代码如下:
{% extends "admin/change_form.html" %}
{% load i18n admin_urls static admin_modify %}
{% block object-tools-items %}
    {#  判断当前用户角色  #}
    {% if request.user.is_superuser %}
        <li>
            {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
            <a href="{% add_preserved_filters history_url %}" class="historylink">
                {% trans "History" %}
            </a>
        </li>
    {% endif %}

    {% if has_absolute_url %}
        <li>
            <a href="{{ absolute_url }}" class="viewsitelink">
                {% trans "View in site" %}
            </a>
        </li>
    {% endif %}
{% endblock %}

image-20240503182822496

从上述代码可以看到, 自定义模板文件change_form.html的代码说明如下: 
(1) 自定义模板文件change_form.html继承内置模板文件change_form.html, 并且自定义模板文件名必须与内置模板文件名一致.
(2) 由于内置模板文件admin/change_form.html导入了标签{% load i18n admin_urlsstatic admin_modify %},
    因此自定义模板文件change_form.html也要导入该模板标签, 否则提示异常信息.
(3) 使用block标签实现内置模板文件的代码重写.
    查看内置模板文件的代码发现, 模板代码以{% block xxx %}形式分块处理, 将网页上不同的功能以块的形式划分.
    因此, 在自定义模板中使用block标签对某个功能进行自定义开发.
下面是对这段代码的详细解释:
* 1. 模板继承: {% extends "admin/change_form.html" %}
     这一行表示这个模板继承了admin/change_form.html.
     在Django admin中, change_form.html是用于显示对象编辑表单的模板.
     通过继承, 这个新模板可以覆盖或添加一些块(block)到基础模板中.
    
* 2. 加载标签库: {% load i18n admin_urls static admin_modify %}
     这一行加载了几个Django模板标签库:
     i18n: 用于国际化(Internationalization)的支持.  
     admin_urls: 用于生成Django admin中的URL.  
     static: 用于加载静态文件(CSS, JavaScript, 图片等).  
     admin_modify: 可能是一个自定义的或第三方提供的标签库, 用于在Django admin的修改视图中执行某些操作.

* 3. 覆盖对象工具块:
     {% block object-tools-items %}  
     ...  
     {% endblock %}
     这个块覆盖了admin/change_form.html中的object-tools-items块, 
     该块通常用于显示对象上方的工具(如历史记录, 在网站上查看等).

* 4. 添加工具项:
     {% if request.user.is_superuser %}  
     ...  
     {% endif %}  
     这个条件判断当前用户是否是超级用户. 如果是, 它会添加一个指向对象历史记录的链接.
     这个链接是通过admin_urls标签库中的admin_urlname和admin_urlquote过滤器以及Django的url模板标签生成的.
     
     {% if has_absolute_url %}  
     ...  
     {% endif %}  
     这个条件判断当前对象是否有一个绝对URL(即是否可以在网站上直接查看该对象).
     如果有, 它会显示一个链接和一个文本(通过trans标签进行国际化), 告诉用户可以在网站上查看该对象.

这个模板片段为Django admin的修改表单页面添加了额外的对象工具,
这些工具根据当前用户的角色和对象是否具有绝对URL来决定是否显示.
运行MyDjango, 当访问Admin后台系统的时候,
Django优先查找admin文件夹的模板文件, 找不到相应的模板文件时,再从Django的内置Admin模板文件中查找.
我们使用超级管理员和普通用户分别访问职业信息的数据修改页, 不同的用户角色所返回的页面会有所差异, 如图9-32所示.

2024-05-03_182608

9-32 自定义模板文件
要在Django admin中显示一个链接, 允许用户直接查看某个对象在网站上的表示形式, 需要确保以下几点:
* 0. 在admin模板中添加链接: 
     在Django admin模板中(admin/index/change_form.html), 添加类似以下的代码来显示链接(上面已经完成).
{% if has_absolute_url %}
<li>
    <a href="{{ absolute_url }}" class="viewsitelink">
        {% trans "View in site" %}
    </a>
</li>
{% endif %}
* 1. 模型应该有一个get_absolute_url方法, 它返回一个指向该对象在网站上表示形式的URL.
     定义了get_absolute_url方法后在数据修改页面就会出现"View in site"按钮.
# index 的 models.py
from django.db import models
from django.urls import reverse


# 定义人员信息类
class PersonInfo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20)
    age = models.IntegerField()

    # 打印记录时展示用户
    def __str__(self):
        return str(self.name)

    # 定义模型对象的元数据
    class Meta:
        # admin中展示的表名称
        verbose_name = '人员信息'
        verbose_name_plural = '人员信息'


# 定义职业信息表
class Vocation(models.Model):
    JOB = (
        ('软件开发', '软件开发'),
        ('软件测试', '软件测试'),
        ('需求分析', '需求分析'),
        ('项目管理', '项目管理'),
    )

    id = models.AutoField(primary_key=True)
    job = models.CharField(max_length=20, choices=JOB)
    title = models.CharField(max_length=20)
    salary = models.IntegerField(null=True, blank=True)
    person_info = models.ForeignKey(PersonInfo, on_delete=models.CASCADE)
    record_time = models.DateField(auto_now=True, null=True, blank=True)

    # 打印记录时展示id
    def __str__(self):
        return str(self.id)

    class Meta:
        verbose_name = '职业信息'
        verbose_name_plural = '职业信息'

    def get_absolute_url(self):
        # 反向解析, 生成一个url地址
        return reverse('index:detail', args=[self.id])

image-20240503195556080

* 2. 确保URL配置正确在urls.py文件中, 需要有一个URL模式与get_absolute_url方法返回的URL相匹配.
# MyDjango 的 urls.py
from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include(('index.urls', 'index'), namespace='index'))
]

# index 的 urls.py
from django.urls import path
from . import views
urlpatterns = [
    path('vacation/<int:pk>/', views.vacation_detail, name='detail'),
]

image-20240503200117099

* 3. 编写视图处理请求: vacation_detail视图函数接受一个pk参数, 这个参数的值就是从URL中捕获的整数.
     然后, 使用get_object_or_404函数来根据这个pk值从Vacation模型中获取相应的对象.
     如果对象存在, 我们就继续处理; 如果对象不存在, 函数会自动返回一个404错误.
# index 的 views.py
from django.shortcuts import render
from django.shortcuts import get_object_or_404
from .models import *


def vacation_detail(request, pk):
    # 使用get_object_or_404来获取对象, 如果对象不存在则返回404页面
    vacation = get_object_or_404(Vocation, pk=pk)

    # 获取模型的所有字段(仅用于展示, 可能需要排除某些字段)
    field_list = [(field.name, field.value_from_object(vacation)) for field in Vocation._meta.fields]

    # 渲染模板并传递字段列表
    return render(request, 'vacation_detail.html', {'vacation': vacation, 'field_list': field_list})

image-20240503201150258

* 4. 在模板中显示用户信息.
<!-- templates 的 vacation_detail.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>字段列表</title>
    <style>
        /* 添加CSS样式来格式化表格 */
        table {
            width: 100%; /* 设置表格宽度为100% */
            border-collapse: collapse; /* 合并边框 */
        }

        th, td {
            border: 1px solid black; /* 设置单元格边框 */
            padding: 8px; /* 设置单元格内边距 */
            text-align: left; /* 文本左对齐 */
        }

        th {
            background-color: #f2f2f2; /* 设置表头背景色 */
        }
    </style>
</head>
<body>
<table>
    <thead>
    <tr>
        <th>字段名</th>
        <th>字段值</th>
    </tr>
    </thead>
    <tbody>
    {% for field_name, field_value in field_list %}
        <tr>
            <td>{{ field_name }}</td>
            <!-- 你可能需要处理不同类型的字段值,例如日期、时间等 -->
            <td>{{ field_value }}</td>
        </tr>
    {% endfor %}
    </tbody>
</table>
</body>
</html>

image-20240503201229818

启动程序访问数据修改页面, 会显示'VIEW IN SET'标签.

image-20240503201353654

点击会跳转到数据详情页面.

image-20240503201553674

9.4.9 自定义Admin后台系统

Admin后台系统为每个网页设置了具体的路由地址, 每个路由的响应内容是调用内置模板文件生成的.
若想改变整个Admin后台系统的网页布局和功能, 则可重新定义Admin后台系统, 比如常见的第三方插件Xadmin和Django Suit,
这些插件都是在Admin后台系统的基础上进行重新定义的.

重新定义Admin后台系统需要对源码结构有一定的了解, 我们可以从路由信息进行分析.
以MyDjango为例, 在MyDjango的urls.py中查看Admin后台系统的路由信息, 如图9-33所示.

image-20240503201945712

9-33 Admin后台系统的路由信息
长按键盘上的Ctrl键, 在PyCharm里单击图9-32中的site即可打开源码文件sites.py, 
将该文件的代码注释进行翻译得知, Admin后台系统是由类AdminSite实例化创建而成的, 
换句话说, 只要重新定义类AdminSite即可实现Admin后台系统的自定义开发, 如图9-34所示.

image-20240506150114189

9-34 源码文件sites.py
Admin后台系统还有一个系统注册过程, 将Admin后台系统绑定到Django, 当运行Django时, Admin后台系统会随之运行.
Admin的系统注册过程在源码文件apps.py里定义, 如图9-35所示.

image-20240506150335474

9-35 源码文件apps.py
综上所述, 如果要实现Admin后台系统的自定义开发, 就需要重新定义类AdminSite和改变Admin的系统注册过程.
下一步通过简单的实例来讲述如何自定义开发Admin后台系统, 我们将会更换Admin后台系统的登录页面.
以MyDjango为例, 在项目的根目录创建static文件并放置登录页面所需的JavaScript脚本文件和CSS样式文件;
然后在模板文件夹templates中放置登录页面login.html; 最后在MyDjango文件夹创建myadmin.py和myapps.py文件.
项目的目录结构如图9-36所示.

image-20240506220806581

9-36 目录结构(static的文件在配套资源中)
下一步在MyDjango的myadmin.py中定义类MyAdminSite, 它继承父类AdminSite并重写方法admin_view()和get_urls(),
从而更改Admin后台系统的用户登录地址, 实现代码如下:
# MyDjango 的 myadmin.py
from django.contrib import admin
from functools import update_wrapper
from django.views.generic import RedirectView
from django.urls import reverse
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.http import HttpResponseRedirect
from django.contrib.auth.views import redirect_to_login
from django.urls import include, path, re_path
from django.contrib.contenttypes import views as contenttype_views


class MyAdminSite(admin.AdminSite):
    # 管理员视图装饰器(默认关闭缓存)
    def admin_view(self, view, cacheable=False):
        def inner(request, *args, **kwargs):
            if not self.has_permission(request):
                if request.path == reverse('admin:logout', current_app=self.name):
                    index_path = reverse('admin:index', current_app=self.name)
                    return HttpResponseRedirect(index_path)
                # 修改注销后重新登录的路由地址
                return redirect_to_login(
                    request.get_full_path(),
                    '/login.html'
                )
            return view(request, *args, **kwargs)

        if not cacheable:
            inner = never_cache(inner)
        if not getattr(view, 'csrf_exempt', False):
            inner = csrf_protect(inner)
        return update_wrapper(inner, view)

    def get_urls(self):
        def wrap(view, cacheable=False):
            def wrapper(*args, **kwargs):
                return self.admin_view(view, cacheable)(*args, **kwargs)

            wrapper.admin_site = self
            return update_wrapper(wrapper, view)

        urlpatterns = [
            path('', wrap(self.index), name='index'),
            # 修改登录界面的路由地址
            path('login/', RedirectView.as_view(url='/login.html')),
            path('logout/', wrap(self.logout), name='logout'),
            path('password_change/', wrap(self.password_change, cacheable=True), name='password_change'),
            path(
                'password_change/done/',
                wrap(self.password_change_done, cacheable=True),
                name='password_change_done',
            ),
            path('jsi18n/', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'),
            path(
                'r/<int:content_type_id>/<path:object_id>/',
                wrap(contenttype_views.shortcut),
                name='view_on_site',
            ),
        ]
        valid_app_labels = []
        for model, model_admin in self._registry.items():
            urlpatterns += [
                path('%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)),
            ]
            if model._meta.app_label not in valid_app_labels:
                valid_app_labels.append(model._meta.app_label)
        if valid_app_labels:
            regex = r'^(?P<app_label>' + '|'.join(valid_app_labels) + ')/$'
            urlpatterns += [
                re_path(regex, wrap(self.app_index), name='app_list'),
            ]
        return urlpatterns

image-20240506222516635

这段代码定义了一个名为MyAdminSite的类, 该类继承了Django的admin.AdminSite.
admin.AdminSite是Django管理后台(admin site)的主要入口点, 用于注册模型, 处理URL配置等.
通过继承并修改admin_view方法, 可以自定义管理员视图的权限检查和缓存策略.

下面是对代码的详细解释:
* 1. 类定义: class MyAdminSite(admin.AdminSite):
     这定义了一个名为MyAdminSite的类, 该类继承自Django的admin.AdminSite.

* 2. admin_view 方法: def admin_view(self, view, cacheable=False):
     这是MyAdminSite类中的一个方法, 用于装饰管理员视图.
     这个方法接受两个参数: 一个是要装饰的视图函数view和一个布尔值cacheable(默认为False), 表示这个视图是否可以被缓存.

     内部函数 inner: def inner(request, *args, **kwargs):
     这是一个内部函数, 它包装了原始的管理员视图函数view.
     它首先检查用户是否有权限访问管理员界面(self.has_permission(request)).
     
     权限检查: 如果用户没有权限, 并且请求的URL是注销页面的URL(reverse('admin:logout', current_app=self.name)),
     则重定向到管理员首页*reverse('admin:index', current_app=self.name)).
     如果用户没有权限且请求的URL不是注销页面的URL, 则使用redirect_to_login函数将用户重定向到登录页面.

     缓存和CSRF保护: 如果cacheable参数为False(即视图不应该被缓存), 则使用never_cache装饰器来装饰inner函数.
     如果view函数没有设置为csrf_exempt(即它应该受到CSRF保护), 则使用csrf_protect装饰器来装饰inner函数.
    
     更新包装器: return update_wrapper(inner, view)
     使用update_wrapper函数来更新inner函数的__name__, __module__, __doc__等属性, 使其与原始的view函数保持一致.
     这样, 当在Django的URL配置或模板中使用这个视图时, 它仍然会表现得像原始的view函数一样.
     
     不同的URL输入如何影响代码的执行:
     1. 用户访问 /admin/ (尝试访问管理后台):
        self.has_permission(request)被调用, 检查用户是否有权限访问管理后台.
        如果用户有权限, view(request, *args, **kwargs)被调用, 执行admin_view视图函数.
        如果用户没有权限, 并且用户不是在尝试注销(因为URL不是/admin/logout/), 用户将被重定向到管理后台的首页(/admin/).
        但由于已经在首页, 实际上可能不会发生重定向, 或者根据具体实现, 可能会显示一个权限不足的提示.
     2. 用户访问 /admin/logout/ (尝试注销):
        self.has_permission(request)被调用, 检查用户是否有权限访问注销页面(尽管这通常不需要特定权限, 但假设代码如此).
        无论用户是否有权限, 由于用户正在尝试注销, 代码会执行redirect_to_login(request.get_full_path(), '/login.html').


* 3. wrap 函数: wrap函数是一个闭包函数, 它接受一个视图函数view和一个布尔值cacheable作为参数.
     在wrap函数内部, 定义了另一个函数wrapper, 该函数会调用self.admin_view来处理view函数, 并将cacheable参数传递给它.
     wrapper函数还设置了admin_site属性为self, 以便在需要时可以从内部访问AdminSite的实例.
     最后使用update_wrapper函数来更新wrapper的元信息(如__name__, __doc__ ), 使其与原始 view 函数保持一致.

     urlpatterns列表: 这是一个用于存储URL模式的列表.
     列表中的每个元素都是一个path或re_path对象, 它们定义了URL的模式和对应的视图函数.
     这里, 使用前面定义的wrap函数来包装(即装饰)视图函数, 以确保它们具有适当的权限检查, 缓存和CSRF保护.

     注册模型的URL模式: 通过遍历self._registry(这是AdminSite类用于存储已注册模型和它们的管理类的字典),
     代码为每个已注册的模型添加了一个URL模式.
     这些URL模式的路径由模型的app_label和model_name组成, 并包含该模型的管理类提供的URL配置(通过include(model_admin.urls)).
    
     应用列表URL: 如果存在有效的应用标签(即至少有一个模型已注册), 则生成一个正则表达式模式来匹配这些应用标签.
     使用re_path和wrap(self.app_index)创建一个URL模式, 当用户访问某个应用的根路径时, 会调用app_index视图函数.

     返回值: 最后, get_urls方法返回urlpatterns列表, 这个列表包含了Django管理界面的所有URL模式.
上述代码将父类AdminSite的方法admin_view()和get_urls()进行局部的修改, 修改的代码已标有注释说明, 其他代码无须修改.
从修改的代码看到, Admin后台系统的用户登录页面的路由地址设为/login.html, 因此还要定义路由地址/login.html.
分别在MyDjango的urls.py, index的urls.py和views.py中定义路由login及其视图函数loginView, 代码如下:
# MyDjango 的 urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include(('index.urls', 'index'), namespace='index'))
]

image-20240506221319085

# index 的 urls.py
from django.urls import path
from .views import login_view


urlpatterns = [
    # 定义路由
    path('login.html', login_view, name='login'),
]

image-20240506221401605

# index 的 views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login, authenticate
from django.contrib.auth.models import User
from django.urls import reverse


def login_view(request):
    if request.method == 'POST':
        u = request.POST.get('username', '')
        p = request.POST.get('password', '')
        if User.objects.filter(username=u):  # 检查用户名是否存在, 如果成功则返回一个用户对象, 否则会None
            user = authenticate(username=u, password=p)  # 使用authenticate函数验证用户
            if user:
                if user.is_active:  # 检查用户是否'活跃', is_active属性为True, 用户才能被登录
                    login(request, user)
                return redirect(reverse('index:login'))
            else:
                pass_error = '账号密码错误, 请重新输入!'
        else:
            user_error = '用户不存在, 请注册!'

    else:
        if request.user.username:
            return redirect(reverse('admin:index'))

    return render(request, 'login.html', locals())

image-20240506221613207

视图函数loginView用于实现用户登录, 它由Django内置的Auth认证系统实现登录过程.
用户登录页面由模板文件夹templates的login.html生成.
模板文件login.html的代码如下:
<!-- templates 的 login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>后台登录</title>
    {% load static %}
    <link rel="stylesheet" href="{% static "css/reset.css" %}">
    <link rel="stylesheet" href="{% static "css/user.css" %}">
    <script src="{% static "js/jquery.min.js" %}"></script>
    <script src="{% static "js/user.js" %}"></script>
</head>
<body>
<div class="page">
    <div class="loginwarrp">
        <div class="logo">用户登录:</div>
        <div class="login_form">
            <form action="" id="login" name="login" method="post">
                {% csrf_token %}
                <li class="login-item">
                    <label for="id_username">用户名称:</label>
                    <input id="id_username" type="text" name="username" class="login_input">
                    <p id="count-msg" class="error plugin-error">{{ user_error }}</p>
                </li>
                <li class="login-item">
                    <label for="id_password">用户密码:</label>
                    <input id="id_password" type="password" name="password" class="login_input">
                    <p id="password-msg" class="error">{{ pass_error }}</p>
                </li>
                <li class="login-sub">
                    <input type="submit" name="Submit" value="登录">
                </li>
            </form>
        </div>
    </div>
</div>
{# 画布粒子 #}
<script type="text/javascript">
    window.onload = function () {
        var config = {
            vx: 4,
            vy: 4,
            height: 2,
            width: 2,
            count: 100,
            color: "121, 162, 185",
            stroke: '100, 200, 180',
            dist: 6000,
            e_dist: 20000,
            max_conn: 100
        };
        CanvasParticle(config);
    }
</script>
{# 画布粒子文件 #}
<script src="{% static 'js/canvas-particle.js' %}"></script>
</body>
</html>

image-20240506221731245

完成MyAdminSite和路由login的定义后, 将自定义的MyAdminSite进行系统注册过程, 由MyAdminSite实例化创建Admin后台系统.
在MyDjango文件夹的myapps.py中定义系统注册类MyAdminConfig, 代码如下:
# MyDjango 的 myapps.py
from django.contrib.admin.apps import AdminConfig


# 继承父类AdminConfig
# 重新设置属性default_site的值,使它指向MyAdminSite类
class MyAdminConfig(AdminConfig):
    default_site = 'MyDjango.myadmin.MyAdminSite'

image-20240506222108412

系统注册类MyAdminConfig继承父类AdminConfig并设置父类属性default_site, 使它指向MyAdminSite,
从而由MyAdminSite实例化创建Admin后台系统.
最后在配置文件settings.py中配置系统注册类MyAdminConfig, 此外还需配置静态资源文件夹static, 代码如下:
# Django 的 settings.py
# 配置系统注册类MyAdminConfig
INSTALLED_APPS = [
    # 注释原有的admin
    # 'django.contrib.admin',
    # 指向myapps的MyAdminConfig
    'MyDjango.myapps.MyAdminConfig',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'index'
]

# 配置静态资源文件夹static
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']

image-20240506221936660

完成上述开发后, 运行MyDjango, 在浏览器上清除Cookie信息, 确保Admin后台系统处于未登录状态, 
访问: 127.0.0.1:8000/admin 就能自动跳转到我们定义的用户登录页面, 如图9-37所示.

image-20240506222356057

9.5 本章小结

Admin后台系统也称为网站后台管理系统, 主要对网站的信息进行管理, 
如文字, 图片, 影音和其他日常使用文件的发布, 更新, 删除等操作, 也包括功能信息的统计和管理, 如用户信息, 订单信息和访客信息等.
简单来说, 它是对网站数据库和文件进行快速操作和管理的系统, 以使网页内容能够及时得到更新和调整.
ModelAdmin继承BaseModelAdmin, BaseModelAdmin的元类为MediaDefiningClass, 
因此Admin系统的属性和方法来自ModelAdmin和BaseModelAdmin.

由于定义的属性和方法较多, 因此这里只说明日常开发中常用的属性和方法.
 fields: 由BaseModelAdmin定义, 格式为列表或元组, 在新增或修改模型数据时, 设置可编辑的字段.
 exclude: 由BaseModelAdmin定义, 格式为列表或元组, 在新增或修改模型数据时, 
  隐藏字段, 使字段不可编辑, 同一个字段不能与fields共同使用, 否则提示异常.
 fieldsets: 由BaseModelAdmin定义, 格式为两元的列表或元组(列表或元组的嵌套使用),
  改变新增或修改页面的网页布局, 不能与fields和exclude共同使用, 否则提示异常.
 radio_fields: 由BaseModelAdmin定义, 格式为字典, 如果新增或修改的字段数据以下拉框的形式展示,
  那么该属性可将下拉框改为单选按钮.
 readonly_fields: 由BaseModelAdmin定义, 格式为列表或元组, 在数据新增或修改的页面设置只读的字段, 使字段不可编辑.
 ordering: 由BaseModelAdmin定义, 格式为列表或元组, 设置排序方式, 比如以字段id排序, ['id']为升序, ['-id']为降序.
 sortable_by: 由BaseModelAdmin定义, 格式为列表或元组, 设置数据列表页的字段是否可排序显示,
  比如数据列表页显示模型字段id, name和age, 如果单击字段name, 数据就以字段name进行升序(降序)排列,
  该属性可以设置某些字段是否具有排序功能.
 formfield_for_choice_field(): 由BaseModelAdmin定义, 如果模型字段设置choices属性,
  那么重写此方法可以更改或过滤模型字段的属性choices的值.
 formfield_for_foreignkey(): 由BaseModelAdmin定义, 如果模型字段为外键字段(一对一关系或一对多关系),
  那么重写此方法可以更改或过滤模型字段的可选值(下拉框的数据).
 formfield_for_manytomany(): 由BaseModelAdmin定义, 如果模型字段为外键字段(多对多关系),
  那么重写此方法可以更改或过滤模型字段的可选值.
 get_queryset(): 由BaseModelAdmin定义, 重写此方法可自定义数据的查询方式.
 get_readonly_fields(): 由BaseModelAdmin定义, 重写此方法可自定义模型字段的只读属性,
  比如根据不同的用户角色来设置模型字段的只读属性.
 list_display: 由ModelAdmin定义, 格式为列表或元组, 在数据列表页设置显示的模型字段.
 list_display_links: 由ModelAdmin定义, 格式为列表或元组, 为模型字段设置路由地址, 由该路由地址进入数据修改页.
 list_filter: 由ModelAdmin定义, 格式为列表或元组, 在数据列表页的右侧添加过滤器, 用于筛选和查找数据.
 list_per_page: 由ModelAdmin定义, 格式为整数类型, 默认值为100, 在数据列表页设置每一页显示的数据量.
 list_max_show_all: 由ModelAdmin定义, 格式为整数类型, 默认值为200, 在数据列表页设置每一页显示最大上限的数据量.
 list_editable: 由ModelAdmin定义, 格式为列表或元组,
  在数据列表页设置字段的编辑状态, 可以在数据列表页直接修改某行数据的字段内容并保存,
  该属性不能与list_display_links共存, 否则提示异常信息.
 search_fields: 由ModelAdmin定义, 格式为列表或元组, 在数据列表页的搜索框设置搜索字段, 根据搜索字段可快速查找相应的数据.
 date_hierarchy: 由ModelAdmin定义, 格式为字符类型, 在数据列表页设置日期选择器, 只能设置日期类型的模型字段.
 save_as: 由ModelAdmin定义, 格式为布尔型, 默认为False, 若改为True, 则在数据修改页添加'另存为'功能按钮.
 actions: 由ModelAdmin定义, 格式为列表或元组, 列表或元组的元素为自定义函数, 函数在'动作'栏生成操作列表.
 actions_on_top和actions_on_bottom: 由ModelAdmin定义, 格式为布尔型, 设置'动作'栏的位置.
 save_model(): 由ModelAdmin定义, 重写此方法可自定义数据的保存方式.
 delete_model(): 由ModelAdmin定义, 重写此方法可自定义数据的删除方式.
Admin后台系统的首页设置包括: 项目应用的显示名称, 模型的显示名称和网页标题, 三者的设置方式说明如下:
 项目应用的显示名称: 在项目应用的__init__.py中设置变量default_app_config,
  该变量指向自定义的IndexConfig类, 由IndexConfig类的verbose_name属性设置项目应用的显示名称.
 模型的显示名称: 在模型属性Meta中设置verbose_name和verbose_name_plural,
  两者的区别在于verbose_name是以复数的形式表示的, 若在模型中同时设置这两个属性, 则优先显示verbose_name_plural的值.
 网页标题: 在项目应用的admin.py中设置Admin的site_title和site_header属性,
  如果项目有多个项目应用, 那么只需在某个项目应用的admin.py中设置一次即可.