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:
Admin (superuser & staff)
Principal
Teacher
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.
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.
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)
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.
I have a model with an attribute that is connected to another model as follow:
class Book(models.Model):
synced = models.OneToOneField('SyncedBook'
related_name='internal',
on_delete=models.CASCADE)
# some more attributes here...
#property
def book_address(self)
return self.synced.book_address
However, the book_address is a also a FK in the SyncedBook table as follow:
book_address = models.ForeignKey('Address', db_index=True, null=True, blank=True,
related_name='address_book', on_delete=models.PROTECT)
I don't know and understand how to be able to edit the book_address through the Django admin page in class BookingAdmin(admin.ModelAdmin), even though I have read over the documentation. At first I have the attribute as readonly, but now I want to be able to edit it and save the new address from the Address table. Is there a way to make it happen through the class BookingAdmin(admin.ModelAdmin) and how? Any example and solution would be appreciate
Model properties are typically used for presenting logically defined data for a particular model instance and not necessarily storing data on the model instance itself.
An example of when to use a model property is as follows:
# Defines a product instance
class Product(model.Models):
name = models.CharField(max_length=100)
description = models.TextField()
active = models.BooleanField(default=True)
cost = models.DecimalField(max_digits=5, decimal_places=2)
price = models.DecimalField(max_digits=5, decimal_places=2)
# calculate profits on product
#property
def profit(self)
p = self.price - self.cost
return p
In your case, you are trying to actually be able to modify data against a related model instance within the django admin. To me this sounds like more specifically an Inline (click here for documentation)
So in your case, you would need to create something like the following to your admin.py file:
class SyncedBookInline(admin.TabularInline):
model = BookInline
#admin.Register(Book)
class BookAdmin(admin.ModelAdmin):
# all your model admin settings
inlines = [SyncedBookInline]
Additional Info:
The Inline solution should still work for you. Please see the working code listed below:
models.py:
from django.db import models
class Hero(models.Model):
name = models.CharField(max_length=50)
class HeroAcquaintance(models.Model):
name = models.CharField(max_length=50)
hero = models.OneToOneField(Hero, on_delete=models.CASCADE)
admin.py:
from django.contrib import admin
from .models import *
class HeroAcquaintanceInline(admin.TabularInline):
model = HeroAcquaintance
#admin.register(Hero)
class HeroAdmin(admin.ModelAdmin):
list_display = (
'name',
)
inlines = [HeroAcquaintanceInline]
#admin.register(HeroAcquaintance)
class HeroAcquaintanceAdmin(admin.ModelAdmin):
list_display = (
'name',
)
Screenshot:
I want to extend the user model in Django (2.2) and combine it with a Host and a Guest entities that have also specific fields.
In the official documentation, it is recommended to create a "Profile" class with a OneToOne field that reference the User primary key.
I can see 3 ways of doing it:
Solution 1: Profile, Host and Guest model:
class Profile(models.Model):
k_user = models.OneToOneField(User, on_delete=models.CASCADE)
language = models.CharField(max_length=2)
class Host(models.Model):
k_user = models.OneToOneField(User, on_delete=models.CASCADE)
host_field= models.CharField(max_length=500)
class Guest(models.Model):
k_user = models.OneToOneField(User, on_delete=models.CASCADE)
guest_field = models.BooleanField(null=False)
Solution 2: Host and Guest model (with Profile fields duplicated)
class Host(models.Model):
k_user = models.OneToOneField(User, on_delete=models.CASCADE)
language = models.CharField(max_length=2)
host_field = models.CharField(max_length=500)
class Guest(models.Model):
k_user = models.OneToOneField(User, on_delete=models.CASCADE)
language = models.CharField(max_length=2)
guest_field = models.BooleanField(null=False)
Solution 3: Profile model (containing Guest and Host fields)
class Profile(models.Model):
k_user = models.OneToOneField(User, on_delete=models.CASCADE)
language = models.CharField(max_length=2)
is_host = models.BooleanField(null=False)
guest_field = models.BooleanField(null=False)
host_field = models.CharField(max_length=500)
All those solutions are working.
My question is: "Which one is the smartest, all things considered" (less database access, less code to write, easier to maintain, less limitations, etc..)
After digging further into Django's doc and reading the article mentioned by #sam that explain how to implement multi user types in Django, I found my answer.
It is written in the Django doc that "it’s highly recommended to set up a custom user model, even if the default User model is sufficient for you".
Here is what it gives in my case:
class User(AbstractUser):
is_guest = models.BooleanField(default=False)
is_host = models.BooleanField(default=False)
language = models.CharField(max_length=2)
class Host(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
host_field = models.CharField(max_length=500)
class Guest(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
guest_field = models.BooleanField(null=False)
in settings.py:
AUTH_USER_MODEL = 'path.to.User'
Guest or Host record are inserted when you create a new user:
user = User.objects.create_user(...)
if is_host:
Host.objects.create(user=user)
else:
Guest.objects.create(user=user)
I appreciate the fact that I can detect the user "type" in the request object (with request.user.is_host).
By extending the user class, you can also use the email field for login, and make it unique:
class User(AbstractUser):
[...]
email = models.EmailField(unique=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
If once in production you chose to add fields in the User model, it is possible if you have set up a custom user model.
Otherwise you will be stuck with profiles, so I advise you to follow Django's guidelines and always extending the user class, even if you don't need it (yet).
I would propose a 4:th way, using a mixin with an abstract model. This will derive the fields of the abstract model to the ones that you apply it to. This way you don't need to rewrite code and still apply it to different models:
class ProfileMixin(models.Model):
k_user = models.OneToOneField(User, on_delete=models.CASCADE)
language = models.CharField(max_length=2)
class Meta:
abstract = True
class Host(ProfileMixin):
host_field = models.CharField(max_length=500)
class Guest(ProfileMixin):
guest_field = models.BooleanField(null=False)
I have the below in my models.py file:
class Film(models.Model):
title = models.CharField(max_length=200)
director = models.CharField(max_length=200)
description = models.CharField(max_length=200)
pub_date = models.DateField('date published')
class Comment(models.Model):
film = models.ForeignKey(Film, on_delete=models.CASCADE)
body = models.CharField(max_length=200)
When I logged into Django admin I added some films, and then added some comments, selecting which film object the comment related to. I then created a couple of users via the admin panel also.
I would like my relationships to be:
Film can have many comments / Comments belong to film
User can have many comments / Comments belong to user
I think, like with comments and films, I just need to define user as a foreign key to comment. I am struggling to do this. I am working through the Django tutorials but I can't see the tutorials covering how I can link other tables to the user.
I thought I would be able to do something like this:
user = models.ForeignKey(User, on_delete=models.CASCADE)
While importing User like this:
from django.contrib.auth.models import User
The result at the moment is if I keep user = models.ForeignKey(User, on_delete=models.CASCADE) I get err_connection_refused
Maybe have you changed your default user model in the settings?
Instead of using User directly with the the Foreign key, you should use user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) in your Comment Model, as follow
class Comment(models.Model):
film = models.ForeignKey(Film, on_delete=models.CASCADE)
body = models.CharField(max_length=200)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
You need to apply migrations to be able to add user to Comment,
python manage.py makemigrations
python manage.py migrate
if at the moment that you are applying migrations, shell shows a message telling You are trying to add a non-nullable field 'user' to comment without a default
You have 2 Options
Skip migrations and add a default value to the field in the models or set the attribute as nullable, whatever else that you need
ie
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True)
and apply migrations again
Or select a default value to the new field, should be an id of an existing user in databse
This is because django should populate existing records in database, if exist
Use "settings.AUTH_USER_MODEL".
So, import "settings" from "django.conf", then use "settings.AUTH_USER_MODEL" as shown below:
from django.db import models
from django.conf import settings # Here
class Comment(models.Model):
film = models.ForeignKey(Film, on_delete=models.CASCADE)
body = models.CharField(max_length=200)
# Here
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
I have the following models:
class UserPost(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
class User(AbstractUser):
MALE = 'M'
FEMALE = 'F'
GENDER_CHOICES = (
(MALE, 'Male'),
(FEMALE, 'Female')
)
posts = models.ManyToManyField(Post, through='UserPost')
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Post(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
content = models.TextField()
status = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
When I run python manage.py makemigrations, it raises the following error:
users.User.posts: (fields.E303) Reverse query name for 'User.posts' clashes with field name 'Post.user'.
HINT: Rename field 'Post.user', or add/change a related_name argument to the definition for field 'User.posts'.
There is a many-to-many relationship between User and Post models. Each user can like many posts and each post can be liked by many users.
There is also a many-to-one relationship between User and Post models. Each user can write many posts and each post can be written by only one user.
Shouldn't reverse query name for 'User.posts' be user_set by default. If so, why is this name clashing with field name 'Post.user'? Can someone explain the meaning of this error? Thanks.
Do you need the UserPost model? It looks to have all the same fields as Post, and if you're after efficient querying, Django automatically creates database indexes on foreign keys. Here's a simple setup that should work pretty well:
class User(AbstractUser):
# Your fields go here, but you might not need the posts field
class Post(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='posts')
This would let you do a user.posts.all() to get all of the Post instances that belong to that user.