Django管理后台 - 特定用户(管理员)内容

50

我开始组织一个新项目,假设我会有一些模型如产品目录

我将允许我的客户(不是访客,只有特定客户)登录Django管理站点,创建、编辑和删除自己的目录。

假设我创建了一个叫做"商店"的模型,创建每个商店(名称、地址、徽标、联系信息等),并创建一个与该商店绑定的管理员用户。

现在我希望这个新管理员(不是站点管理员,而是商店管理员--可能是一个用户组)只能看到并编辑与他所在的商店相关联的目录

这是可能的吗?

我应该在Django管理界面内实现这个功能还是创建一个新的"商店管理员"应用程序?

4个回答

64

首先,需要注意的是:Django管理员设计哲学认为任何能够访问管理员页面(is_staff==True)的用户都是受信任的用户,例如员工,因此即使获得访问管理员的权限也需要具备“员工”身份。虽然您可以定制管理员以限制区域,但允许组织外的任何人访问您的管理员被认为是有风险的,并且Django不保证在这一点上的任何安全性。

现在,如果您仍想继续,可以通过简单地不将那些权限分配给用户来限制大部分内容,但商店除外。您需要给所有商店所有者授予编辑它们需要访问的任何商店模型的权限,但应该在他们的权限列表中留下其他所有内容。

接下来,对于每个需要仅限所有者查看的模型,您需要添加一个字段来存储“所有者”或被允许访问它的用户。您可以通过 ModelAdmin 上的 save_model 方法实现这一点,该方法可以访问请求对象:

class MyModelAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        obj.user = request.user
        super(MyModelAdmin, self).save_model(request, obj, form, change)

你还需要将ModelAdmin的queryset限制为仅属于当前用户的项目:

class MyModelAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = super(MyModelAdmin, self).get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(owner=request.user)

然而,这仅仅会限制被列出的内容,用户仍可以通过更改URL访问他们没有权限访问的其他对象,因此您需要覆盖每个ModelAdmin的易受攻击视图,如果用户不是所有者,则重定向。

from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse

class MyModelAdmin(admin.ModelAdmin):
    def change_view(self, request, object_id, form_url='', extra_context=None):
        if not self.queryset(request).filter(id=object_id).exists():
            return HttpResponseRedirect(reverse('admin:myapp_mymodel_changelist'))

        return super(MyModelAdmin, self).change_view(request, object_id, form_url, extra_context)

    def delete_view(self, request, object_id, extra_context=None):
        if not self.queryset(request).filter(id=object_id).exists():
            return HttpResponseRedirect(reverse('admin:myapp_mymodel_changelist'))

        return super(MyModelAdmin, self).delete_view(request, object_id, extra_context)

    def history_view(self, request, object_id, extra_context=None):
        if not self.queryset(request).filter(id=object_id).exists():
            return HttpResponseRedirect(reverse('admin:myapp_mymodel_changelist'))

        return super(MyModelAdmin, self).history_view(request, object_id, extra_context)

更新 06/05/12

感谢@christophe31指出,由于ModelAdmin的查询集已经被限制在用户范围内,你只需要在更改、删除和历史视图中使用self.queryset()。这样可以很好地抽象出模型类名,使代码更加健壮。我还改用了filterexists,而不是try...except块与get。这种方式更为简洁,实际上也会导致一个更简单的查询。


2
对于更改和删除视图,当它们获取对象时,它们使用:"self.queryset().get(pk=pk)",因此如果用户无法查看该项,则会返回错误。 - christophe31
@ChrisPratt:这个答案很好,但有一个问题(截至2012年11月)。默认的change_view需要四个参数,而extra_context是可选的第四个参数。通过你的方式传递它,你会覆盖form_url参数,这决定了管理页面上表单操作的行为。解决方案要么是捕获并传递表单URL,要么更加健壮/具有未来性,使用extra_context=extra_context - jfmatt
4
Django的较新版本(具体是哪个版本我不知道)将queryset方法更改为get_queryset - Ekin Ertaç
1
@ChrisPratt 感谢你的警告。这是否仍适用于django 2.0+?如果是,那么对于“非员工”设置is_staff=True的最佳替代方案是什么?是否可以像这里建议的那样拥有第二个管理员站点来限制权限? - djvg
"用户仍可以通过更改URL访问其他未授权的对象" - 我认为现在不再是这种情况了。我刚在Django 3.0.8中尝试过,如果我尝试查看通过使用get_queryset从特定用户中排除的项目,则会收到404错误。超级用户通过相同的URL查看时会返回预期结果。" - Saturnix
显示剩余2条评论

14

由于置顶评论不再是最新答案,我在此发布我的回答。我正在使用Django 1.9,我不确定这个更改是何时发生的。

例如,您有不同的场馆和与每个场馆相关联的不同用户,模型将如下所示:

class Venue(models.Model):
    user = models.ForeignKey(User)
    venue_name = models.CharField(max_length=255)
    area = models.CharField(max_length=255)

现在,如果用户允许通过Django管理面板登录,则用户的员工状态必须为真。

admin.py看起来像这样:

class FilterUserAdmin(admin.ModelAdmin): 
    def save_model(self, request, obj, form, change):
        if getattr(obj, 'user', None) is None:  
            obj.user = request.user
        obj.save()
    def get_queryset(self, request):
        qs = super(FilterUserAdmin, self).queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(user=request.user)
    def has_change_permission(self, request, obj=None):
        if not obj:
            return True 
        return obj.user == request.user or request.user.is_superuser


@admin.register(Venue)
class VenueAdmin(admin.ModelAdmin):
    pass

函数名称已从queryset更改为get_queryset。

编辑:我想扩展我的回答。还有另一种方法可以返回过滤后的对象,而不使用queryset函数。我想强调的是,我不知道这种方法是否更有效或者更低效。

get_queryset方法的另一种实现方式如下:

def get_queryset(self, request):
    if request.user.is_superuser:
        return Venue.objects.all()
    else:
        return Venue.objects.filter(user=request.user)

此外,如果关系更深入,我们还可以过滤内容。

class VenueDetails(models.Model):
    venue = models.ForeignKey(Venue)
    details = models.TextField()

现在,如果我想筛选这个模型,它具有场馆作为外键但没有用户,我的查询将如下所示:

def get_queryset(self, request):
    if request.user.is_superuser:
        return VenueDetails.objects.all()
    else:
        return VenueDetails.objects.filter(venue__user=request.user)

Django ORM允许我们通过'__'访问不同类型的关系,这些关系可以深入到我们想要的程度。

这里是以上内容的官方文档链接。


1
感谢您抽出时间更新这个内容。对我来说非常有用,我可以确认它在Django 1.9上有效(至少)。 - S_alj
1
很棒的回答,chatuur。有一个评论。你不应该使用你的备用get_queryset函数,因为它将在返回结果时忽略任何管理器过滤器。最好在覆盖的函数中调用super,这样Django中提供的任何功能(如过滤器)都不会丢失。 - NateB80

0

很抱歉,我知道已经晚了,但也许对其他人会有帮助。我猜 django-permission 应用程序可以帮助实现目的。


0

我认为RelatedOnlyFieldListFilter应该能帮到你。 这是Django文档的链接:RelatedOnlyFieldListFilter

list_filter can be : a tuple, where the first element is a field name and the second element is a class inheriting from django.contrib.admin.FieldListFilter, for example:

class PersonAdmin(admin.ModelAdmin):
    list_filter = (
        ('is_staff', admin.BooleanFieldListFilter),
    )

You can limit the choices of a related model to the objects involved in that relation using RelatedOnlyFieldListFilter: (Vous pouvez limiter les choix d’un modèle lié aux objets concernés par la relation en utilisant RelatedOnlyFieldListFilter:)

  class BookAdmin(admin.ModelAdmin):
      list_filter = (
           ('author', admin.RelatedOnlyFieldListFilter),
      ) 

Assuming author is a ForeignKey to a User model, this will limit the list_filter choices to the users who have written a book instead of listing all users. (En supposant que author est une clé ForeignKey vers un modèle User, cela va limiter les choix de list_filter aux utilisateurs qui ont écrit un livre au lieu d’énumérer tous les utilisateurs.)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接