Django admin 配置

Django 自带的 Admin Site 管理页面可以方便用户快速构建一个简单的后台管理系统,少量代码即可快速实现对数据库中的数据进行展示、修改、保存的可视化页面和功能。当需要对后台展示的数据进行配置时,只需要在 app 的代码文件 admin.py 中进行相应配置即可。

环境信息

  • centos 7
  • python 3.10
  • django 4.0

为 model 配置 admin 管理页面

要为 model 启用 admin 管理接口,参考配置

常用配置说明

配置登录页面和 web 页面中显示的 title

修改 APP 对应的 admin.py

admin.py
from django.contrib import admin

admin.site.site_header = "My Admin Site"
admin.site.site_title = "My Admin Site"

常用配置示例

admin 中的配置大多来自 ModelAdmin 对象的选项 [2]

admin.py
from django.contrib import admin

# 假如 app 为 servers,导入 models
from servers import models

@admin.register(models.Servers)
class ServersAdmin(admin.ModelAdmin):
# list_display 定义model 中要显示的列
# list_display_links 定义了哪些列可以点击(链接)跳转到对象的修改页面
list_display = ('inVender', 'contacter', 'name', 'ip', 'type', 'zone', 'diskSize', 'diskType', 'dataTransfer')
list_display_links = ('inVender', 'name', 'ip')

# fields 定义修改页面中显示哪些修改项, 未出现在此的列,添加或修改对象时不显示.
# fieldsets 对要编辑的部分进行分组显示, 'classes': ('collapse',) 为 CSS 格式定义隐藏和显示
fields = ('inVender', )

fieldsets = (
['Main',{
'fields':('name','ip'),
}],
['Advance',{
'classes': ('collapse',), # CSS
'fields': ('inVender',),
}]
)

# search_fields 定义哪些列可以被搜索,假如搜索的列是外键或其他关连列,要使用关联列中的字符类型或数字类型为搜索对象,如 'inVender__name'
# 要自定义搜索多个内容,可参考本文后续内容
search_fields = ('name', 'inVender__name')

# 显示默认的搜索内容, Djanog 4 不可用
placeholder = '输入搜索内容'



# actions 要在变更列表页上提供的动作列表
actions = ['startCheck']

# actions_on_top actions_on_bottom 定义控制动作栏在页面的哪个位置出现,默认在顶部
actions_on_top = True
actions_on_bottom = False

# readonly_fields 定义只读列,此处里面的项不可编辑,不在此列表中的项可以编辑
# 同时存在于 readonly_fields 和 fields/fieldsets 中的列,不可编辑
readonly_fields = ('name',)

# 控制每个分页的管理变更列表页面上出现多少个项目。默认情况下,设置为 100
# 可以将 list_per_page 属性设置为一个列表,以便在Admin页面上提供不同的页面大小选项。
list_per_page = 100


# 配置 filter, 可以在列表页的右侧显示筛选选项。当筛选的列只包含一个对象时,此筛选列会隐藏不显示
list_filter = ('name',)

# 允许在列表上直接编辑字段
list_editable = ['account']

list_display 中自定义列

以下示例中,project_series 不属于 Info 模型中已有的字段,属于 Info 模型中的外键 project 中的字段,要在后台对应模型中添加显示此列,可以参考以下配置

admin.py
@admin.register(models.Info)
class InfoAdmin(admin.ModelAdmin):
list_display = ('id', 'project_series')

def project_series(self, obj):
return obj.project.project_series

project_series.short_description = '项目系列'

自定义 actions

actions 定义要在变更列表页上提供的动作列表 [1]

以下示例代码中,添加了 3 个动作: export_as_excelchange_payStatusToPaychange_payStatusToUnPay,分别完成导出选中数据到 Excel、修改支付状态为支付/未支付

其中,每个方法函数的 short_description 属性定义了显示在 admin 页面上的功能名称,allowed_permissions 定义了执行此操作需要的权限。

admin.py
from django.http import HttpResponse
import openpyxl

@admin.register(models.Servers)
class ServersAdmin(admin.ModelAdmin):

actions = ["export_as_excel",'change_payStatusToPay','change_payStatusToUnPay']
def export_as_excel(self,request,queryset):
meta = self.model._meta
field_names = ['id', 'inDeparMent', 'inVender', 'inOwner', 'usedFor', 'financeCode', 'ip', 'price', 'validDateTo','status']
response = HttpResponse(content_type='application/msexcel')
filename = "servers" + str(time.time()).replace('.','') + '.xlsx'
response['Content-Disposition'] = 'attachment; filename=%s' %(filename)
try:
wb = openpyxl.load_workbook(filename)
except FileNotFoundError:
wb = openpyxl.Workbook()
ws = wb.active
ws.append(field_names)
for obj in queryset:
data = []
data.append(getattr(obj,'id'))
data.append(getattr(obj,'inDeparMent').shortName)
data.append(getattr(obj, 'inVender').vender_shortName)
data.append(getattr(obj, 'inOwner').name)
data.append(getattr(obj, 'usedFor'))
data.append(getattr(obj, 'financeCode'))
data.append(getattr(obj, 'ip'))
data.append(getattr(obj, 'price'))
data.append(getattr(obj, 'validDateTo'))
data.append(getattr(obj,'status'))
ws.append(data)
wb.save(response)
return response
export_as_excel.short_description = "导出到Excel"

def change_payStatusToPay(self,request,queryset):
queryset.update(payStatus=1)
# 执行完成后向 admin web 返回相应消息
self.message_user(request, _('刷新完成'))

change_payStatusToPay.short_description = "更改支付状态-->已支付"
change_payStatusToPay.allowed_permissions = ('change',)

def change_payStatusToUnPay(self,request,queryset):
queryset.update(payStatus=0)

change_payStatusToUnPay.short_description = "更改支付状态-->未支付"
change_payStatusToUnPay.allowed_permissions = ('change',)

自定义 filter

要自定义 filter,可以通过继承 django.contrib.admin.SimpleListFilter 类来实现 [3]

以下代码示例创建自定义的 filter,用来筛选域名过期时间

admin.py
from django.contrib import admin
from domains_collect import models
from django.utils.translation import gettext_lazy as _
import datetime
import calendar

@admin.register(models.RawDomains)
class RawDomainsAdmin(admin.ModelAdmin):
list_display = ('domain', 'status', 'domain_created_time', 'expire')
search_fields = ('domain',)



class DomainExpireTimeFilter(admin.SimpleListFilter):
title = _('域名过期时间')
parameter_name = 'expire'

def lookups(self, request, model_admin):
return (
('already_expired', _('已到期')),
('today_expired', _('今天到期')),
('m_expired', _('本月到期')),
('nm_expired', _('下月到期')),
)
def queryset(self, request, queryset):
if self.value() == 'already_expired':
return queryset.filter(expire__lt=datetime.datetime.now(tz=datetime.timezone.utc))
if self.value() == 'today_expired':
today_date = datetime.date.today()
y, m, d = today_date.year, today_date.month, today_date.day
today_start = datetime.datetime(y, m, d, 0, 0, 0)
today_end = datetime.datetime(y, m, d, 23, 59, 59)
return queryset.filter(expire__lt=today_end, expire__gt=today_start)
if self.value() == 'm_expired':
today_data = datetime.date.today()
y, m = today_data.year, today_data.month
last_day_this_month = calendar.monthrange(y, m)[1]

day_start = datetime.datetime(y, m, 1, 0, 0, 0)
day_end = datetime.datetime(y, m, last_day_this_month, 23, 59, 59)
return queryset.filter(expire__lt=day_end, expire__gt=day_start)
if self.value() == 'nm_expired':
today_data = datetime.date.today()
y, m = today_data.year, today_data.month
last_day_this_month = calendar.monthrange(y, m)[1]
next_m_1d = datetime.date(y, m, 1) + datetime.timedelta(last_day_this_month)
y, m = next_m_1d.year, next_m_1d.month
last_day_next_month = calendar.monthrange(y, m)[1]

day_start = datetime.datetime(y, m, 1, 0, 0, 0)
day_end = datetime.datetime(y, m, last_day_next_month, 23, 59, 59)
return queryset.filter(expire__lt=day_end, expire__gt=day_start)

list_filter = ('status', DomainExpireTimeFilter)

自定义搜索功能

Admin 后台默认只能搜索一个目标,本实例配置允许搜索 以空格分割的 多个目标内容。此功能主要是通过重写方法 get_search_results 实现。

admin.py
from django.contrib import admin
from django.db.models import Q
from domains_collect import models

class RawDomainsAdmin(admin.ModelAdmin):
search_fields = ('domain',)

def get_search_results(self, request, queryset, search_term):
# 如果没有查询字符串,则返回所有内容
if not search_term:
return queryset, False
# 获取搜索参数并分割为多个搜索项
search_terms = search_term.split()

# 构建查询表达式
q_objects = Q()
for term in search_terms:
q_objects |= Q(domain__icontains=term)

# 执行搜索操作
queryset = queryset.filter(q_objects)

# 返回结果
return queryset, True

内联

管理界面可以在同一页面上与父模型编辑模型。这些被称为内联 [4]

以下示例中,RawDomainsAdmin 存放域名相关信息

models.py
class RawDomains(models.Model):
domain = models.CharField(max_length=64, unique=True, blank=False, help_text="域名", verbose_name='域名')
expire = models.DateTimeField(help_text="域名过期时间", verbose_name='域名过期时间')
domain_created_time = models.DateTimeField(help_text="域名注册时间", verbose_name='域名注册时间')

DomainProjectInfo 存放项目和域名的关联信息,其中 domain 是和 RawDomainsOneToOneField 的关系。

models.py
class DomainProjectInfo(models.Model):
domain = models.OneToOneField(RawDomains, on_delete=models.CASCADE, help_text="域名", verbose_name="域名")
project = models.ForeignKey('Project', on_delete=models.CASCADE, help_text="项目", verbose_name="项目")

在 admin 页面中配置项目信息内联到域名信息中

admin.py
@admin.register(models.RawDomains)
class RawDomainsAdmin(admin.ModelAdmin):
list_display = ('domain', 'status', 'domain_created_time', 'expire')

class DomainProjectDetails(admin.StackedInline):
model = models.DomainProjectInfo

inlines = [DomainProjectDetails]

实现效果如下

其他示例参考

如果要在 RawDomains 列表中展示和筛选 DomainProjectInfo 中的信息,可以使用以下方法实现

admin.py
@admin.register(models.RawDomains)
class RawDomainsAdmin(admin.ModelAdmin):
list_display = ('domain', 'status', 'domain_created_time', 'expire', 'use_status')

def use_status(self, obj):
return obj.domainprojectinfo.status

list_filter = ('domain', 'status', 'domainprojectinfo__status')

list_display 中的 use_status 属于自定义字段,其值来自 OneToOneField 表的 status 字段。

list_filter 中的值通过 model__属性 的方式引用 OneToOneField 中的字段。

要查看 OneToOneField 对应的表,可以查看 model 的属性值:

>>> RawDomains.domainprojectinfo
<django.db.models.fields.related_descriptors.ReverseOneToOneDescriptor object at 0x7f920ced2810>

修改后台页面中显示的 APP 名称

APP 是通过 python manage.py startapp 创建的,创建后 APP 项目所在目录下包含 apps.py,其中有 APP 相关的配置。默认 APP 在后台页面显示的名称为创建 APP 时指定的 APP 名称,要修改在 admin 页面上面显示的名称,可以在 apps.py 中添加 verbose_name = 'Myname''

apps.py
from django.apps import AppConfig


class DomainsCollectConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'domains_collect'
verbose_name = '域名统计'

admin 显示操作日志

ModelAdmin 本身就有日志记录功能,LogEntry 类可以跟踪通过管理界面完成的对象的添加、更改和删除。 [5]

在项目 APP 的 admin.py 文件中增加以下内容,可以展示后台操作日志

admin.py
from django.contrib.admin.models import LogEntry


@admin.register(LogEntry)
class LogEntryAdmin(admin.ModelAdmin):
list_display = ['action_time', 'user', 'object_repr', 'object_id', 'action_flag', 'content_type','change_message']

其中

  • object_id, object_repr 分别指被操作对象的 ID 和字符串表示。
  • content_type - 表示被操作对象的所属 model

根据不同的登陆用户显示不同的列

在需要权限控制的场景中,不同用户拥有的权限或者安全级别可能不一样,被允许看到的信息也会不一样。以下示例可以实现超级管理员用户和其他用户登陆后展示不同的列

主要的实现思路是重写 get_list_display 方法,根据不同用户返回不同列 [6]

admin.py
@admin.register(models.Servers)
class ServersAdmin(admin.ModelAdmin):
list_display = ('id','inDeparMent','inVender','inOwner','get_app','usedFor',
'financeCode','ip','price','validDateTo','status','payStatus','hwInfo','comment','createTime')


date_hierarchy = "createTime"
list_per_page = 200

def get_list_display(self,request):
user = request.user
if user.is_superuser:
return ['id', 'inDeparMent', 'inVender', 'inOwner','get_app', 'usedFor',
'financeCode', 'ip', 'price', 'validDateTo', 'status', 'payStatus', 'hwInfo', 'comment', 'createTime']
try:
group = Group.objects.get(user=user)
print("group = %s" %(group))
print(dir(group))
if group.name == "ops":
print("group mached")
return ['id', 'inDeparMent', 'inVender', 'inOwner', 'get_app', 'usedFor',
'financeCode', 'ip', 'price', 'validDateTo', 'status', 'payStatus', 'hwInfo', 'comment', 'createTime']

except ObjectDoesNotExist:
pass

return ['id', 'inDeparMent', 'inVender', 'inOwner','get_app',
'ip', 'validDateTo', 'status',
'hwInfo', 'comment', 'createTime']

admin 后台实现批量修改带外键的字段

本示例演示实现在 admin 后台批量修改 model 状态(通过自定义 action 实现),其中 model 的状态是外键到了其他 model,示例 models 如下

models.py

class Status(models.Model):
status = models.CharField(max_length=32, unique=True)

def __str__(self):
return self.status

class Project(models.Model):
status = models.ForeignKey('Status', on_delete=models.CASCADE)
project = models.CharField(max_length=128)

def __str__(self):
return self.project

为了实现可以在后台批量修改 status,需要在 Project 的后台注册类中编写自定义的 action,用户选择多个对象, 点击执行后, 会跳转到中间页面, 在中间页面中下拉选择要更改的状态, 主要代码如下

admin.py
@admin.register(models.Project)
class ProjectAdmin(admin.ModelAdmin):

def update_status(self, request, queryset):
# 用户可选择的状态
status_choices = [(status.id, status.status) for status in Status.objects.all()]
context = {
'queryset': queryset,
'status_choices': status_choices,
'action_name': 'update_status',
}
return render(request, 'admin/change_status.html', context)

update_domain_status.short_description = '批量修改状态'

actions = ['update_domain_status']

以上代码中, 中间页面的 html 文件位于 admin/change_status.html,此文件主要实现用户选择状态,然后提交一个表单到指定的 url,进行实际状态的修改动作,文件内容如下

admin/change_status.html
{% extends 'admin/base_site.html' %}

{% block content %}
<form method="post" action="{% url 'my_app:update_status' %}">
{% csrf_token %}
<label for="target">修改对象:</label>
<div class="results">
<table id="result_list">
<tbody>
{% for query in queryset %}
<tr><td>{{ query }}</td></tr>
{% endfor %}

</tbody>
</table>
</div>

<label for="status">Status:</label>
<select name="status" required>
{% for choice in status_choices %}
<option value="{{ choice.0 }}">{{ choice.1 }}</option>
{% endfor %}
</select>
<br><br>
<input name="queryset" value="{{ queryset }}" hidden="true">
<input type="submit" name="submit" value="Change status">
</form>
{% endblock %}

<input name="queryset" value="{{ queryset }}" hidden="true"> 的主要作用是将用户选择的 QuerySet 提交到要处理状态变更的 url

用户选择状态并提交,数据会被提交给 {% url 'my_app:update_status' %},因此需要实现此 url,以接收数据并执行实际的状态变更的动作

urls.py 中定义以下内容

urls.py
from django.urls import path
from .views import update_status_view

app_name = 'my_app'
urlpatterns = [
path('update_status/', update_status_view, name='update_status'),
]

views.py 文件中定义视图函数 update_status_view,接收用户提交的数据,并更新状态,最后重定向到列表页面

views.py
from django.shortcuts import redirect
from django.urls import reverse
from django.apps import apps
from django.contrib import messages


def update_status_view(request):

status = post_data['status']
queryset_string = post_data['queryset']
model_name = "Project"
model = apps.get_model(app_label="my_app", model_name=model_name)
obj_str_list = queryset_string.replace('<QuerySet', '').replace('<DomainProjectInfo: ', '').replace('>', '').replace('[', '').replace(']', '').replace(' ', '').split(',')

queryset_list = []
for obj_str in obj_str_list:
obj = model.objects.get(project=obj_str)
queryset_list.append(obj)

queryset = model.objects.filter(pk__in=[obj.pk for obj in queryset_list])

for obj in queryset:
obj.status_id = int(status)
obj.save()

messages.add_message(request, messages.SUCCESS, 'Changes saved successfully.')
return redirect(reverse('admin:my_app_project_changelist'))

自定义页面或者视图中,向 admin 添加显示消息

在 Django 中,可以使用 messages 框架在重定向后向用户显示消息。在视图函数中使用 messages.add_message() 函数将消息添加到消息框架中,并使用 messages.SUCCESSmessages.ERRORmessages.WARNING 等常量指定消息的级别。

下面是一个示例视图函数,演示如何重定向到 myapp_model 的更改列表并显示成功消息:

from django.contrib import messages
from django.shortcuts import redirect, reverse

def my_view(request):
# Do some processing here
messages.add_message(request, messages.SUCCESS, 'Changes saved successfully.')
return redirect(reverse('admin:myapp_model_changelist'))

这将 Changes saved successfully. 消息添加到消息框架中,并重定向到 myapp_model 的更改列表视图。当用户从消息框架中看到消息时,它将以绿色背景突出显示,因为我们在这里使用了 messages.SUCCESS 常量。

自定义的视图中向 Admin 写入操作日志

如果要在 Django 项目中的自定义视图中将操作记录写入 Django Admin 的操作记录日志中,可以参考以下配置实现。在 Django Admin 中显示操作日志

from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import force_str

def admin_log_action(request, obj, action_flag, message):
user = request.user
content_type = ContentType.objects.get_for_model(obj)
LogEntry.objects.log_action(
user_id=user.pk,
content_type_id=content_type.pk,
object_id=obj.pk,
object_repr=force_str(obj),
action_flag=action_flag,
change_message=str(message),
)

def create_object(request):
if request.method == 'POST':
# ... 处理创建对象的逻辑 ...
created_object = YourModel.objects.create(...) # 举例

admin_log_action(request, created_object, ADDITION, "Object created.")

return render(request, 'success_template.html', {'message': 'Object created successfully'})

return render(request, 'create_object.html')

如果要在自定义页面中展示操作日志,参考以下代码:

def index(request):
log_entries = LogEntry.objects.all().order_by('-action_time')
return render(request, 'index.html', {'logs': log_entries})

HTML 模板文件如下

{% for log in logs %}
<li class="item">
<div class="item-row">
<div class="item-col fixed item-col-title">
<div class="item-heading">Time</div>
<div>
<h4 class="item-title"> {{ log.action_time }} </h4>
</div>
</div>
<div class="item-col fixed item-col-title">
<div class="item-heading"> User </div>
<div> {{ log.user }} </div>
</div>
<div class="item-col fixed pull-left item-col-title">
<div class="item-heading">Object</div>
<div> {{ log.object_repr }} </div>
</div>
<div class="item-col fixed pull-left item-col-title">
<div class="item-heading">Object ID</div>
<div> {{ log.object_id }} </div>
</div>
<div class="item-col fixed pull-left item-col-title">
<div class="item-heading">Action</div>
<div> {{ log.get_action_flag_display }} </div>
</div>
<div class="item-col fixed pull-left item-col-title">
<div class="item-heading">Content Type</div>
<div> {{ log.content_type }} </div>
</div>
<div class="item-col fixed pull-left item-col-title">
<div class="item-heading">Message</div>
<div> {{ log.change_message }} </div>
</div>
</div>
</li>
{% endfor %}
  • 注意其中的 {{ log.get_action_flag_display }},如果直接使用 LogEntryaction_flag 属性,显示的是 action_flag 的值,主要为
    • 1:表示添加记录(ADDITION)
    • 2:表示更改记录(CHANGE)
    • 3:表示删除记录(DELETION)
      如果要使用更友好的字符串展示,需要使用 get_action_flag_display 方法。这个方法会根据 action_flag 的值返回对应的友好名称

常见错误

此问题说明示例

脚注