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_excel
,change_payStatusToPay
,change_payStatusToUnPay
,分别完成导出选中数据到 Excel、修改支付状态为支付/未支付
其中,每个方法函数的 short_description
属性定义了显示在 admin 页面上的功能名称,allowed_permissions
定义了执行此操作需要的权限。
admin.py from django.http import HttpResponseimport 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 ) 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 adminfrom domains_collect import modelsfrom django.utils.translation import gettext_lazy as _import datetimeimport 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 adminfrom django.db.models import Qfrom domains_collect import modelsclass 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
是和 RawDomains
的 OneToOneField
的关系。
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 AppConfigclass 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 pathfrom .views import update_status_viewapp_name = 'my_app' urlpatterns = [ path('update_status/' , update_status_view, name='update_status' ), ]
在 views.py
文件中定义视图函数 update_status_view
,接收用户提交的数据,并更新状态,最后重定向到列表页面
views.py from django.shortcuts import redirectfrom django.urls import reversefrom django.apps import appsfrom django.contrib import messagesdef 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.SUCCESS
,messages.ERROR
或 messages.WARNING
等常量指定消息的级别。
下面是一个示例视图函数,演示如何重定向到 myapp_model
的更改列表并显示成功消息:
from django.contrib import messagesfrom django.shortcuts import redirect, reversedef my_view (request ): 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, DELETIONfrom django.contrib.contenttypes.models import ContentTypefrom django.utils.encoding import force_strdef 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 }}
,如果直接使用 LogEntry
的 action_flag
属性,显示的是 action_flag
的值,主要为
1
:表示添加记录(ADDITION)
2
:表示更改记录(CHANGE)
3
:表示删除记录(DELETION) 如果要使用更友好的字符串展示,需要使用 get_action_flag_display
方法。这个方法会根据 action_flag
的值返回对应的友好名称
常见错误 此问题说明示例
脚注