Creating user specific form access permissions / validation in django admin - python

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

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.

Chicken and Egg nightmare with Django mixin

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.

How to filter by BooleanField in Django?

I have been having a hard time accomplishing this task and it seems that I cannot get help anywhere I turn. I am trying to send Memos to specific user groups in Django. Each user in each group should receive the Memo and be able to change the BooleanField to True to signify that they have read it.
I then need to be able to access the amount of users which received and have marked the BoolenField as True so I can create a table in a template which says [3/31 Read] and such.
I would like to not use GenericForeignkeys if possible and I would like to keep it as one Model if possible but I know that may not work. Currently I was trying this:
class Memo(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
date_time = models.DateTimeField(default=timezone.now)
sender = models.ForeignKey(User, on_delete=models.CASCADE)
receiver = models.ManyToManyField(Group)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('memos-detail', kwargs={'pk': self.pk})
I was going to access each user (receiver) within the group which is selected on the MemoCreateForm then apply that user to this model:
class ReceivedMemo(models.Model):
memo = models.ForeignKey(
Memo, related_name='user_memos', on_delete=models.CASCADE)
receiver = models.ForeignKey(
User, related_name='memos', on_delete=models.CASCADE)
read = models.BooleanField(default=False)
Then I was going to try to filter the ReceivedMemos by memo to see if each receiver has read the memo or not. But this is starting to get complicated and I am not sure if it will work. Am I going about this the right way? Or should I be able to have one Model such as:
class Memo(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
date_time = models.DateTimeField(default=timezone.now)
sender = models.ForeignKey(User, on_delete=models.CASCADE)
receiver = models.ForeignKey(User, on_delete=models.CASCADE)
read = models.BooleanField(default=True)
Seems that each object would have the BooleanField applied to the object and not the user though.
The ReceivedMemo model seems more appropriate rather than the read bit, but the issue with this approach is that whenever you create a new Memo you need to also create lots of (for every User in the Group) ReceivedMemo objects with read=False? This seems pointless. Maybe you can just store the Users which actually read this thing, and for everyone that's left, consider he has not read it. I.e.
class Memo(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
date_time = models.DateTimeField(default=timezone.now)
sender = models.ForeignKey(User, on_delete=models.CASCADE)
receiver = models.ManyToManyField(Group)
read_by = models.ManyToManyField(User)

Get site-specific user profile fields from user-created object

I am using Django sites framework (Django 2.1) to split an app into multiple sites. All of my models except the User model are site-specific. Here is my Post model:
post.py
class Post(models.Model):
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
related_name='children',
related_query_name='child',
blank=True,
null=True,
)
title = models.CharField(
max_length=255,
blank=True,
)
body_raw = models.TextField()
body_html = models.TextField(blank=True)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
site = models.ForeignKey(Site, on_delete=models.CASCADE)
on_site = CurrentSiteManager()
I have no problem separating posts out by site. When I want to get the posts, I call:
posts = Post.on_site.filter(...)
I have a separate model called UserProfile. It is a many-to-one profile where there is a unique profile created for each user-site combination (similar to profile implementation at SE). The profile has a reputation attribute that I want to access when I get any post. This reputation attribute should be different for each site (like how on SE you have different rep on each site you are a member of).
user_profile.py
class UserProfile(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
reputation = models.PositiveIntegerField(default=1)
site = models.ForeignKey(Site, on_delete=models.CASCADE)
on_site = CurrentSiteManager()
How do I access the user's username (on the User model) as well as the user's reputation (on the UserProfile model) when I get Posts from a query?
I'd like to do something like:
Post.on_site.filter(...)
.select_related('user__userprofile')
.filter_related(user__userprofile.site == get_current_site())
How do I filter a Many-To-One related model?
Better to make UserProfile -> User relationship to be OnetoOne,
because Django doesn't know which of many profiles to show
(but you also need to define related_name)
models.OneToOneField(get_user_model(), related_name='userprofile_rev')
Then you will be able to do this
qs = Post.on_site.filer().select_related('user', 'user__userprofile_rev')
for post in qs:
print(post.user.username, post.user.userprofile_rev.reputation)
If you don't want to change your DB structure you can do like this
(but you need to specify which profile to return)
qs = Post.on_site.filer().select_related('user').prefetch_related('user__userprofile_set')
for post in qs:
print(post.user.username, post.user.userprofile_set[0].reputation)

Access to fields of on reverse relation in django

I have these models:
class Task(models.Model):
user = models.ForeignKey(User)
name = models.CharField()
class Report(models.Model):
task = models.ForeignKey(
Task, blank=True, null=True, related_name='+')
status = models.CharField(max_length=32, choices=Status.CHOICES, default=Status.INCOMPLETE)
Now I want to get all Tasks and their related status.
How do I do this?
At first, '+' is not a valid related_name. It is also not very explicit.
Try replacing '+' with 'reports' instead:
# ...
class Report(models.Model):
task = models.ForeignKey(
Task,
blank=True,
null=True,
related_name='reports' # <<<
)
status = models.CharField(
max_length=32,
choices=Status.CHOICES,
default=Status.INCOMPLETE
)
Then to get all the Tasks with their related status, you can use values:
>>> Task.objects.values('name', 'report__status')
<QuerySet [{'name': 'test', 'report__status': 'OK'}, ...]>
The sign + is exactly what you don't want to have as related_name in this case. It tells Django to not create a backwards relation. Check out here. Chose another valid name or skip this argument, in which case Django will create a reverse relation by default using for the name of the backwards relation the model name lowercased and the suffix _set (see here for details).
However, in your example if you want to get all reports which are related to a task and the corresponding status you do not necessarily need backwards relationships. Try this:
reports = Report.objects.exclude(task__isnull=True).values('task__name', 'status')
Change the value of related_name to something else, like 'reports' for example:
class Report(models.Model):
task = models.ForeignKey(
Task, blank=True, null=True, related_name='reports')
status = models.CharField(max_length=32, choices=Status.CHOICES, default=Status.INCOMPLETE)
Now if you have a Task object (not queryset), you can get a queryset of it's reports using:
reports = task.reports.all()
You can use filter() on the reports if you need to.
reports = task.reports.filter(status='something')

Categories

Resources