如何在Django Wagtail中为指定ModelAdmin添加类似页面权限的独立权限
要实现和Wagtail原生「页面权限」逻辑一致的「指定用户组仅访问授权科目」效果,属于对象级行权限控制,无法仅靠Django默认的模型级增删改权限完成,需要从权限关联、配置面板、访问拦截三层实现,具体步骤如下:
1. 给Subject模型增加组权限关联字段
首先在科目模型上新增多对多字段,关联Django内置的Group模型,存储每个科目对应的可访问用户组:
from django.db import models from django.contrib.auth.models import Group class Subject(models.Model): name = models.CharField(max_length=100, verbose_name="科目名称") # 保留你原有字段,比如科目编码、描述、所属学院等 # 新增权限关联字段 allowed_groups = models.ManyToManyField( Group, blank=True, related_name="accessible_subjects", verbose_name="可访问用户组" ) def __str__(self): return self.name
字段添加完成后执行python manage.py makemigrations和python manage.py migrate完成数据库迁移。
2. 在用户组编辑页嵌入科目权限配置面板
要和原生页面权限的交互保持一致,直接在用户组新增/编辑页提供科目授权入口,不需要在科目编辑页反向选组,通过Wagtail内置的register_group_permission_panel钩子实现,在对应app的wagtail_hooks.py中添加如下代码:
from django import forms from wagtail import hooks from .models import Subject class SubjectPermissionForm(forms.Form): subjects = forms.ModelMultipleChoiceField( queryset=Subject.objects.all(), required=False, label="可访问科目", widget=forms.CheckboxSelectMultiple, help_text="勾选该用户组可访问的科目,不勾选则默认无任何科目访问权限" ) def __init__(self, *args, **kwargs): group = kwargs.pop("group", None) super().__init__(*args, **kwargs) # 编辑已有用户组时,回填已分配的科目权限 if group: self.fields["subjects"].initial = Subject.objects.filter( allowed_groups=group ).values_list("id", flat=True) def save(self, group): # 保存时先清空旧的关联关系,再写入新的权限配置 Subject.allowed_groups.through.objects.filter(group=group).delete() for subject in self.cleaned_data["subjects"]: subject.allowed_groups.add(group) @hooks.register("register_group_permission_panel") def register_subject_permission_panel(): return SubjectPermissionForm
代码添加完成后,打开后台用户组编辑页,就能看到和原生页面权限区块位置一致的科目权限配置区,交互逻辑完全对齐。
3. 配置ModelAdmin访问拦截规则
配置面板仅负责权限数据存储,还需要在ModelAdmin层做三层拦截:对无权限用户隐藏菜单入口、列表页只展示当前用户有权限访问的科目、拦截直接输入URL的越权访问。
在wagtail_hooks.py中修改原有的SubjectAdmin配置:
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register from django.core.exceptions import PermissionDenied class SubjectAdmin(ModelAdmin): model = Subject menu_label = "科目管理" menu_icon = "book" list_display = ("name",) search_fields = ("name",) # 替换成你自己的app名称 app_label = "your_app_name" def has_module_permission(self, request): # 超级用户默认放行 if request.user.is_superuser: return True # 无任何科目访问权限的用户,直接隐藏科目管理菜单 user_groups = request.user.groups.all() has_base_view_perm = request.user.has_perm(f"{self.app_label}.view_subject") accessible_count = Subject.objects.filter(allowed_groups__in=user_groups).count() return has_base_view_perm and accessible_count > 0 def get_queryset(self, request): qs = super().get_queryset(request) if request.user.is_superuser: return qs # 列表页仅返回当前用户所属组有权限访问的科目 user_groups = request.user.groups.all() return qs.filter(allowed_groups__in=user_groups).distinct() def get_object(self, request, object_id, to_field): obj = super().get_object(request, object_id, to_field) if request.user.is_superuser: return obj # 校验单条对象访问权限,防止URL越权 user_groups = request.user.groups.all() if not obj.allowed_groups.filter(id__in=user_groups).exists(): raise PermissionDenied("你没有访问该科目的权限") return obj # 如果你之前已经注册过旧的SubjectAdmin,先注销再注册新的 modeladmin_register(SubjectAdmin)
4. 权限粒度细化(可选)
如果需要和页面权限一样区分查看、添加、编辑、删除的操作权限,直接复用Django原生的模型级权限即可:
- 先给对应用户组分配Subject模型的新增、编辑、删除基础权限,作为操作前置校验
- 再通过上述对象级权限规则,控制用户组具体可以对哪些科目执行已授权的操作
如果你使用的是Wagtail 3.0+版本的Snippet管理体系而非旧版ModelAdmin,核心逻辑完全一致,只需要把上述查询集过滤、权限校验的方法替换为SnippetViewSet对应的钩子方法即可。
内容的提问来源于stack exchange,提问作者Tharuka Ruwan

