Django:实现多个用户级别/角色/类型

9

我已经使用Django很长一段时间了,但直到现在我才意识到这一点。

目前,我有一个包含不同用户级别的项目。通常,在我过去的经验中,我只使用Django开发具有两个用户级别的系统,即超级用户和普通/常规用户。所以我的问题是在模型/数据库中呈现这些不同的用户级别的有效方法是什么? 在这里,我将以学校系统为例,并提供一些我最初实现它的想法。

用户级别:

  1. 管理员(超级用户和员工)
  2. 校长
  3. 老师
  4. 学生

方法1:基于每个用户级别添加新表格

from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    user = models.CharfieldField(max_length = 10, unique = True)

class Admin(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)

class Pricipal(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)

class Teacher(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)

class Student(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)

方法2:在用户模型中添加附加的用户类型属性

from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    user = models.CharfieldField(max_length = 10, unique = True)
    is_superuser = models.BooleanField(default = False)
    is_staff = models.BooleanField(default = False)
    is_principal = models.BooleanField(default = False)
    is_teacher = models.BooleanField(default = False)
    is_student = models.BooleanField(default = False

'''
User table in DB:
user | is_superuser | is_staff | is_principal | is_teacher | is_student
'''

我的想法:

在方法 #1 中,由于内置的用户模型具有两个字段 is_staff 和 is_superuser,是否可以像上面的示例一样实现/更改这些字段为 SuperUser/Admin 表?这意味着当我创建管理员 / 超级用户时,我希望它将新行添加到 Admin 表中,而不是在内置的用户模型中创建新用户并更新用户的 is_superuser 和 is_staff 字段为 1。

在方法 #2 中,它的问题在于拥有不同访问权限的表与其直接连接。例如,薪资模型(无法被学生用户访问)与用户模型(包含学生用户)直接链接。

我希望能够得到一些见解,并了解一种适当有效的实现方式,以避免将来的任何实现不便和错误。非常感谢。


2
群组不能满足您的需求吗?https://docs.djangoproject.com/en/3.0/topics/auth/default/#groups - Iain Shelvington
1
@IainShelvington 不是用于根据访问权限对用户进行分类的组吗? - Kyle_397
3个回答

9

我认为采用方法#2是正确的选择。这种方法更加轻便和直接。

我不建议为每个权限级别使用自定义的"类似于用户"的模型。 这样做过于复杂,不易扩展,并会增加查询的数量,对你的问题没有太多好处。不是你的UML架构,而是它的内容必须保证您的权限要求

如果权限级别不是互斥的:

from django.db import models
from django.contrib.postgres.fields import ArrayField


class User(AbstractUser):
    ADMIN = 0
    PRINCIPLE = 1
    TEACHER = 2
    STUDENT = 3
    USER_LEVEL_CHOICES = (
        (ADMIN, "Admin"),
        (PRINCIPLE, "Principle"),
        (TEACHER, "Teacher"),
        (STUDENT, "Student"),
    )
    status = ArrayField(
        models.IntegerField(choices=USER_LEVEL_CHOICES, blank=True, default=STUDENT),
    )

但你需要有更广泛的思考。


我认为你在谈论两个不同的问题:多态性权限

  • 多态性:

多态性是一个对象能够存在多种形式的能力。对于 Django 模型,可以使用许多策略实现:像你提到的 OneToOneField多表继承抽象模型或者代理模型

非常好的资源:这篇文章,以及关于模型继承的 Django 文档。

这个非常复杂的问题都涉及到:同一实体的几种形式间的相似性或差异性,以及哪些操作特别相似或不同(数据形状、查询、权限等)

  • 权限设计:

您可以从以下几种模式中进行选择:

  • 面向模型的权限:用户被授予对 Model 的“添加”、“查看”、“编辑”或“删除”权限。在 Django 中,可通过内置的 Permission 模型实现此目的,该模型具有指向 ContentType 的外键。
  • 面向对象的权限:用户被授予对每个Model实例的“添加”、“查看”、“编辑”或“删除”权限。一些包提供了这种能力,例如 django-guardian
  • 基于规则的权限:用户通过自定义逻辑而不是 M2M 表格被赋予对模型实例的权限。django 的 rules 包提供了这种类型的架构。

如果不同的用户级别具有不同的功能(字段),怎么样?例如,只有“学生”用户级别模型需要上传文件(使用FileField)。因此,在这种情况下,方法#1不是更可取吗? - Kyle_397
2
在第二种方法中,如果您想在执行某些操作之前检查用户权限,那么检查 request.user.status 就很容易了。根据该值,您可以引发错误或不引发错误,执行操作或不执行操作... - Timothé Delion

3
你可以从AbstractUser(一个完整的用户模型,包括is_superuser和is_staff字段)创建一个Profile,然后一旦你有了该配置文件,就可以给用户创建其他类型的配置文件(学生、教师或校长),这些配置文件可能具有自己的功能。
例如,在你的models.py中。
class Profiles(AbstractUser):
    date_of_birth = models.DateField(max_length=128, blank=True, null=True, default=None, verbose_name=_(u'Date of birth'))
    principle = models.OneToOneField(Principles, null=True, blank=True, verbose_name=_(u'Principles'), on_delete=models.CASCADE)
    teacher = models.OneToOneField(Teachers, null=True, blank=True, verbose_name=_(u'Teachers'), on_delete=models.CASCADE)        
    student = models.OneToOneField(Students, null=True, blank=True, verbose_name=_(u'Students'), on_delete=models.CASCADE)  

    class Meta:
        db_table = 'profiles'
        verbose_name = _('Profile')
        verbose_name_plural = _('Profiles')

你可以向该模型添加类方法,例如

def is_teacher(self):
    if self.teacher:
        return True
    else:
        return False

然后,你的Teachers模型可能看起来像这样。
class Teachers(models.Model):
    image = models.FileField(upload_to=UploadToPathAndRename(settings.TEACHERS_IMAGES_DIR), blank=True, null=True, verbose_name=_('Teacher logo'))
    name = models.CharField(blank=False, null=False, default=None, max_length=255, validators=[MaxLengthValidator(255)], verbose_name=_('Name'))
    street = models.CharField( max_length=128, blank=False, null=True, default=None, verbose_name=_('Street'))
    created_by = models.ForeignKey('Profiles', null=True, blank=True, on_delete=models.SET_NULL)

很好,但是外键字段不应该在Teacher类(User类型的类)中吗? - Kyle_397
在这个解决方案中,每次想要添加另一个用户状态时,您都必须添加一个新表。它根本不具备可扩展性。而且,在这些多个模型之间进行额外的查询,并没有太大的好处。 - Timothé Delion
我的意思是Teacher是一种“类型”的用户,就像StudentPrincipal一样。它们可能有更多的共同点而不是不同点。在这种情况下,我认为保持一个唯一的模型,并具有一些“特定类型”的逻辑,比创建多个数据库表更相关。 - Timothé Delion
这真的取决于目标。如果一个人想要同时成为教师和学生,这种方式很容易创建教师和学生档案。反之亦然。你有什么反对更多表格的意见吗? - Tiago Martins Peres
1
我的答案认为同时担任教师和学生是可以的,Kyle_397的第二种方法也不错。更多的表格意味着更多的请求或至少更多的连接:这是有成本的。这是我关于多态性的圣经,如果你也觉得有趣,请看这里:https://realpython.com/modeling-polymorphism-django-python/ - Timothé Delion
显示剩余2条评论

2
我曾在几个项目中使用的一种方法是这样的(伪代码):

class User(AbstractUser):
    ADMIN = 0
    PRINCIPLE = 1
    TEACHER = 2
    STUDENT = 3
    USER_LEVEL_CHOICES = (
        (ADMIN, "Admin"),
        (PRINCIPLE, "Principle"),
        (TEACHER, "Teacher"),
        (STUDENT, "Student"),
    )
    user_level = models.IntgerField(choices=USER_LEVEL_CHOICES)


def lvl_decorator():
  def check_lvl(func):
    def function_wrapper(self, actor, action_on, *args, **kwargs):
        if 'action_lvl' not in action_on: # then action_on is user
            if actor.user_lvl < action_on.user_lvl:
                return True
            return False
        else: # then action_on is action of some kind for that user (you can add action_lvl to ... and pas them to this wapper)
            if actor.user_lvl < action_on.action_lvl:
                return True
            return False
    return function_wrapper
  return check_lvl

您可以编写包装函数,使用此逻辑检查任何操作的级别是否大于用户级别,例如:如果有人想要更改超级用户密码,则他/她应该以级别0的用户登录,但是对于更改普通用户的密码,他/她应该是级别0, 1。此逻辑还可以应用于类、函数等操作。

创建基类,然后添加 lvl_decorator 到它,然后从中继承 => 这可以使您的代码非常干净,并防止进一步复制粘贴。

下面是一个示例:

def lvl_decorator():
    def check_lvl(func):
        def function_wrapper(self, actor, action_on, *args, **kwargs):
            if 'action_lvl' not in action_on:  # then action_on is user
                if actor.user_lvl < action_on.user_lvl:
                    return True
                return False
            else:
                if actor.user_lvl < action_on.action_lvl:
                    return True
                return False

        return function_wrapper

    return check_lvl


class BaseClass(type):
    def __new__(cls, name, bases, local):
        for attr in local:
            value = local[attr]
            if callable(value):
                local[attr] = lvl_decorator()
        return type.__new__(cls, name, bases, local)


# in other locations like views.py use this sample
class FooViewDjango(object, ApiView): # don't remove object or this won't work, you can use any Django stuff you need to inherent.
    __metaclass__ = BaseClass

    def baz(self):
        print('hora hora')

在任何需要的地方使用这个基类。


这个想法似乎很有趣,使用包装函数。由于我不太熟悉它,您能否提供一些关于如何使用它的参考或文档呢 :-( - Kyle_397
@Kyle_397,这个主意是我自己想出来的,没有从任何地方复制,所以也没有参考!但它确实像魅力一样运作! - kia
@Kyle_397 我也添加了一个示例,希望你现在可以使用它 :D。 - kia

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