Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: implement multiple user levels / roles / types

I have been using Django for quite a while but never have I thought of this until now.

Currently, I have a project that contains different user levels. Usually, in my past experience, I only developed systems using Django with only two user levels which are superuser and normal/regular user. So my question is what are the effective ways to present these different user levels in the model/database? Here, I'm going to use a school system as an example and also provide some of my initial thoughts on implementing it.

User levels:

  1. Admin (superuser & staff)
  2. Principal
  3. Teacher
  4. Students

Method #1: Add new tables based on each user level

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)

Method #2: Add additional user types attributes in the User model

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
'''

My thoughts:

In Method #1, as the built-in User model has two fields, is_staff and is_superuser, Is it possible to implement/change the fields into a SuperUser/Admin table as in the example above? This means that when I create an admin/superuser, I want it to add a new row into the Admin table, instead of adding a new user and updating the user's is_superuser and is_staff fields into 1 in the built-in User model.

In Method #2, the problem with it is that tables with different access privileges are directly connected to it. For example, Salary model (which cannot be accessed by Student user) has a direct link with the User model (contains Student user).

I hope I am able to get some insights and also a proper effective way of implementing this so that to prevent any implementation inconvenience and mistakes in the future. Thank you very much.

like image 448
Kyle_397 Avatar asked Apr 05 '20 09:04

Kyle_397


People also ask

How does Django handle multiple users?

In short, you will always have one User model to handle authentication. Do not spread username and passwords across multiple models. Usually extending the default User model and adding boolean flags such as is_student and is_staff work for most cases. Permissions can be managed at a higher level using view decorators.

How do I create different types of users in Django?

First in your project create a new app called customuser. Inside models.py paste the following code: Yo don't need to understand what's happening here right now, just know that we have successfully changed the built-in User class of Django to use Email as the primary key instead of Username.

How do I create multiple roles in Django REST framework?

You need to create a group for each user role, and add needed permissions for each group. (Django has a default model permission, created automatically, look at the docs on the given links) or create the needed permission manually in the model definition.


3 Answers

I think you are in the right path with method #2. It is lighter, and more straightforward.

I would not use a custom "user-like" model for each permission level. Over-complicated, does not scale, and multiply the number of queries, with no very benefit for your problem. Not your UML schema but its content must guarantee your permission requirements.

If the permission levels are not mutual-exclusive :

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),
    )

But you need to have a wider reflexion.


I think you are talking about two separate problems : polymorphism, and permissions

  • Polymorphism :

Polymorphism is the ability of an object to take on many forms. For a Django model, it can be done with many strategies : OneToOneField -as you mentioned- multi-table inheritance, abstract models, or proxy-models.

Very good resources : this article, and Django doc about model inheritance

This very complex problem all refer to : how much your several forms of a same entity are similar, or different. And which operations are particularly similar or different (data shape, querying, permission, ...etc)

  • Permissions design :

You can choose among several patterns

  • Model-oriented permission : A user is granted "add", "view", "edit" or "delete" permission to a Model. This is done in Django with the built-in Permission model, that have a ForeignKey to ContentType
  • Object-oriented permission : A user is granted "add", "view", "edit" or "delete" permission for each Model instance. Some packages provides this ability, django-guardian for example.
  • Rule-oriented permission : A user is granted permission to a Model instance through custom logic instead of M2M table. The django rules package provide this kind of architecture.
like image 145
Timothé Delion Avatar answered Oct 28 '22 11:10

Timothé Delion


You can create from AbstractUser (a full User model, complete with fields, including is_superuser and is_staff) a Profile and then, once you have the profile, give the chance of users to create other type of profile (Student, Teacher or Principle) which could have functionalities of its own.

For instances, in your 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')

To that model you can add class methods, such as

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

Then, your Teachers model could look like this

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)
like image 22
Tiago Martins Peres Avatar answered Oct 28 '22 12:10

Tiago Martins Peres


One of the methods that I used in several projects is this (pseudo code):

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

Then you can write wrapper function with this logic for any action check if action level is bigger than user level e.g.: if someone wants to change superuser password he/she should be logged-in with level-0-user but for changing normal user's password he/she should be level 0, 1. This logic also can be applied to class, functions, etc actions.

Create base class and then add lvl_decorator to it then inherent from it => this keeps your code super clean and prevents further copy paste.

example of what i mean:

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')

Use this base class in any where you want.

like image 27
kia Avatar answered Oct 28 '22 12:10

kia