Chicken and Egg nightmare with Django mixin - python

I am upgrading a large Django-based app from Django 1.7 app to Django 2.2 and am having a lot of trouble with a permissions-related mixin.
class PrincipalRoleRelation(models.Model):
"""A role given to a principal (user or group). If a content object is
given this is a local role, i.e. the principal has this role only for this
content object. Otherwise it is a global role, i.e. the principal has
this role generally.
user
A user instance. Either a user xor a group needs to be given.
group
A group instance. Either a user xor a group needs to be given.
role
The role which is given to the principal for content.
content
The content object which gets the local role (optional).
"""
:::
user = models.ForeignKey(User, verbose_name=_(u"User"), blank=True, null=True, on_delete=models.SET_NULL)
group = models.ForeignKey(Group, verbose_name=_(u"Group"), blank=True, null=True, on_delete=models.SET_NULL)
role = models.ForeignKey(Role, verbose_name=_(u"Role"), on_delete=models.CASCADE)
:::
However, this fails to load during app initialization because User, Group, and Role etc are also apps whose loading is in progress and "populate() is not re-entrant" (so Dango complains)
I tried to work round this by amending the above code to create a sort of "skeleton" class which does not attempt to reference any other apps, e.g. :
app_models_loaded = True
try:
from django.contrib.auth import get_user_model
User = get_user_model()
except:
app_models_loaded = False
if app_models_loaded:
from django.contrib.auth.models import Group
user = models.ForeignKey(User, verbose_name=_(u"User"), blank=True, null=True, on_delete=models.SET_NULL)
group = models.ForeignKey(Group, verbose_name=_(u"Group"), blank=True, null=True, on_delete=models.SET_NULL)
role = models.ForeignKey(Role, verbose_name=_(u"Role"), on_delete=models.CASCADE)
:::
Then in manage.py I would define the full mixin class, called say PrincipalRoleRelation2 and overwrite the skeleton class via the code :
from django.contrib import admin
from permissions.models import PrincipalRoleRelation
if admin.site.is_registered(PrincipalRoleRelation):
admin.site.unregister(PrincipalRoleRelation)
admin.site.register(PrincipalRoleRelation, PrincipalRoleRelation2)
However, although this almost seems to work, I am not seeing some of the PrincipalRoleRelation2 attributes, "role" for example, in what I hoped would be the re-mapped PrincipalRoleRelation class with all attributes present.
I feel I am digging myself into an ever deeper hole, and that the above approach is unsound and will never work properly. So any help would be very much appreciated!
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
edit: In response to schillingt's comment, the User class is defined as follows:
class User(AbstractBaseUser): # , PermissionsMixin):
""" Custom user model
Currently just used by the tests for django-permissions
All unique user fields required for a user
NB: Fields that are customizable across multiple identities will be part of a Profile object
"""
# Dont use PermissionsMixin since not using contrib.auth.models.Permissions
# and not using authentication backend perms ... so its only relevant for groups
# ... however it causes user.groups relations name clashes ..
# But we are using the groups part with django-permissions:
groups = models.ManyToManyField(Group, verbose_name=_('groups'),
blank=True, help_text=_('The groups this user belongs to. A user will '
'get all permissions granted to each of '
'his/her group.'),
related_name="user_set", related_query_name="user")
is_superuser = models.BooleanField(_('superuser status'), default=False,
help_text=_('Designates that this user has all permissions without '
'explicitly assigning them.'))
username = models.EmailField(_('Email (Username)'), max_length=255, unique=True)
# Make username an email and just dummy in email here so its clearer for user.email use cases

As a solution for circular refferrence, django has an ability to specify ForeignKey (or any other relational field) with string refferrence to related model, instead of importing the actual class.
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
This is imo the recommended way to define related fields.

Related

Django User model with same fields

I saw tutorial on how to extend django model by giving 1-to-1 relationship to the django user model.
My question is, if we have same fields on both User and profile(extend from user) model i.e email and username.
When the user register on our site using User model, does the profile model will inherit the same username and email from User model?
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(
User, on_delete=models.CASCADE, null=True, blank=True)
name = models.CharField(max_length=200, blank=True, null=True)
email = models.EmailField(max_length=500, blank=True, null=True)
location = models.CharField(max_length=200, blank=True, null=True)
When the user register on our site using User the model, does the Profile model will inherit the same username and email from User model?
No, you do not inherit from the user model, you simply create a new model that refers to a user, and it happens to have some fields that are the same. It would also be bad from a software design point-of-view. Imagine that you later add a field to your user model and somehow it is the same as the Profile, then all of a sudden the data should be different?
There is no need to store the data an extra time in the Profile. If you have a Profile object like my_profile, you can access the email address stored in the related user with:
my_profile.user.email
You can also make properties that will obtain it from the user, like:
from django.conf import settings
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
blank=True
)
location = models.CharField(max_length=200, blank=True, null=True)
#property
def name(self):
if self.user_id is not None:
return self.user.username
#property
def email(self):
if self.user_id is not None:
return self.user.email
Storing the same data is a form of data duplication and often makes software harder to maintain: it means that for every update of the User model, or the Profile model, you will need to synchronize with the other model. This can easily go wrong, resulting in the fact that the Profile can have a different email address than the related User and vice versa, resulting in a lot of problems where one sends emails to the wrong email address, etc.
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.

Attach additional data to Django Rest (DRF) request object only when user logs in

I have a Branch model that contains all the branches a user could be a part of. Another model UserBranchRelation that contains the relation of a user with a particular branch.
Each user has a particular role inside a particular branch. Admin users have more permissions as compared to a simple member.
Moreover, a user could be an admin inside one branch and a member in another.
I am trying to come up with a neat solution that would let me attach the user's branch and role to the request object on login. Now whenever an authenticated request is received, I should not need to fetch the branch repeatedly as it would already have been attached to the request object on successful login.
Inside my views, I would then use permissions to return data based on user's branch and limit access based on role.
Here are my models.
# Branch Model
class Branch(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(max_length=255)
# User and Branch relationship
class UserBranchRelation(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE)
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
role = models.CharField(
max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER
)

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:
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.

django model design with different user types

I am just learning Django so I thought of creating a project called job board to understand more in detail. I have drawn the following use case.
People can register as job seekers, build their profiles and look for
jobs matching their skillsets
Companies can register, post jobs.
Multiple representatives from a company should be able to register
and post jobs.
Independent Recruiter can create an account as well.
The company can contact to that independent recruiter.
How would be the model design for such a use case? I am confused with the multiple user types in Django. Some favors creating a user profile, while some favors using Groups.
For now, I could only do the following
class User(AbstractUser):
'''
Abstract user because django recommends to start with custom user
'''
username = None
email = models.EmailField(_("Email Address"), unique=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
objects = UserManager()
def __str__(self):
return self.email
class Company(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
I could create a model for User and Company. But I have no idea on handling multiple user types like user can be either job seeker or recruiter. Also, multiple representatives from a company should be able to register and post jobs as well as there can be independent recruiter as well. How would you handle such a case if you have to? Can anyone help me in a step by step guide, please? This way it will clear my confusion and will help me in better design of tables in the future.
Update with example in a nutshell
class User(models.Model):
'''
User can be of any 3 types or can have multiple role as well
'''
is_job_seeker = models.BooleanField(default=False)
is_recruiter = models.BooleanField(default=False)
is_mentor = models.BooleanField(default=False)
class Company(models.Model):
user = models.ForeignKey(User) # only user with is_recruiter flag active can be
class JobSeeker(models.Model):
user = models.OneToOneField(User)
# job seeker profile related fields like experiences, skills, education, profile image etc
class Recruiter(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
company = models.ForeignKey(Company, null=True, blank=True)
# recruiter related profile
Your implementation is almost there. It doesn't look like you need a custom user model right now, so I would just use Django's default.
I would have something like:
from django.conf import settings
from django.db import models
class Company(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
# Other company-related fields
class JobSeeker(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
# Other jobseeker-related fields
class Recruiter(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
company = models.ForeignKey(Company, null=True, blank=True)
# Other recruiter-related fields
When you create any of the models above, you can assign them a user account; and for recruiter, you can assign the company they work for. For example, a company named stack_overflow can have its own company account with a username/password/etc. Then, recruiters who work for stack_overflow could also have their own accounts with their own username/password/etc. Running a command like stackoverflow.recruiter_set will give you all recruiters who work for stack_overflow.
Note that I do not reference User directly. Using the above approach makes your life easier if you decide to switch User models in the future.
I am assuming you don't want to create a User, then create a Company and link it to that user - you just want to do it in one go. That's a slightly different question and the solution will involve you creating a sign-up Form, or something of that sort, where you can add some logic about whether the user is a company, recruiter or jobseeker.
Regarding your other points, it looks like you're looking to set user permissions. Here are the docs for setting default permissions for your custom users, and here are the general docs for Django's built-in permissions system. For example, your Company and Recruiter model could return True for has_perm('your_app.add_job'), while your Jobseeker model returns False. I.e. Companies and Recruiters can create Jobs, but jobseekers cant.
Hope this helps!

Creating user specific form access permissions / validation in django admin

I'm using a 'Task' Model to create operations/administrative tasks in my dashboard. Each task has an assignee, and a reviewer. The assignee completes the task by passing several checks, and the reviewer verifies their work, both of these require each user to edit a check, but neither user should be able to access or modify the other's result.
If the assignee views the Task (with checks inline), they should only be able to modify the "result" and "comment" elements of the check, where as the reviewer can only edit the "review_result" and "reviewer_comment" elements.
To validate this I need to use the fact that given a check, the current user editing the page is equal to check.task.assignee or check.task.reviewer.
I cannot find a simple way to do this, even using django-guardian, as this requires field-level permissions, rather than object level. I considered using modelForm validation, but cannot find a way to access the user from within the model with some hacks such as django-cuser.
Is there another architecture which would allow this? The only way forward that I can see is to use django-guardian combined with two checks, a check and a checkReview, and set object level permissions as the assignee and reviewer are chosen.
class Task(PolymorphicModel):
date_created = models.DateTimeField(auto_created=True)
date_accepted = models.DateTimeField(null=True)
date_reviewed = models.DateTimeField(null=True)
date_closed = models.DateTimeField(null=True)
state = FSMField(default="open")
assignee = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
related_name="assigned_tasks",
related_query_name="assigned_task",
)
reviewer = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
related_name="review_tasks",
related_query_name="review_task",
)
class Check(PolymorphicModel):
result = models.BooleanField(null=True)
comment = models.CharField(max_length=500, null=True, blank=True)
review_result = models.BooleanField(null=True)
reviewer_comment = models.CharField(max_length=500, null=True)
task = models.ForeignKey(Task, on_delete=models.CASCADE)
The correct method to achieve this is to override the get_readonly_fields method of InlineModelAdmin (In your Inline class).
def get_readonly_fields(self, request, obj=None):
if obj is None:
logger.error("An admin has created a check from the dashboard (this should not happen)!")
return []
user = request.user
fields = [field.name for field in self.opts.local_fields]
if user == obj.assignee:
fields.remove(['result', 'comment'])
elif user == obj.reviewer:
fields.remove(['review_result', 'reviewer_comment'])
return fields

Categories

Resources