Django-guardian - where to assign default permission on object creation - django

I am starting an app that has a complex permission structure that will inevitably be managed by the users themselves. I have the following permissions in the model:
class Meta:
permissions = (
('can_view', 'View project'),
('manage_view', 'Can assign View project'),
('can_edit', 'Edit project'),
('manage_edit', 'Can assign Edit project'),
('can_delete', 'Delete project'),
('manage_delete', 'Can assign Delete project'),
('creator', 'Full access to model features.'),
)
These will be managed at the object level with Django-guardian and I am using custom mixins to deal with all of the various combinations and features.
The 'creator' permission is assigned to the initial creator of an object, and they can assign the other permissions to Users via an e-mail based invite system.
My question surrounds the options are for assigning the creator permission on creation of the object.
Some approaches I have thought of so far:
Assign in view after save
newObject.save()
assign('creator', User, newObject)
Generally, I prefer to get these types of events out of my views though.
Override save()
The problem with this method is I have to give the save access to the User object, which means also overriding the init to set self.request = request.
post_save signal
I know I could take advantage of this signal, but compared to the previous two I haven't attempted an implementation of this yet.
Hopefully this provides enough insight into where I am at with things.
I would love to know what the best of these methods would be, or, if they are all bad ideas what an alternative implementation might be.
Thanks,
JD

AD Override save(): You can add user parameter to save method (so you don't have to override init too). This way code would break at runtime if you try to call save method without passing user instance (and as long as you test your code you should be fine with that approach).
AD post_save signal : If you didn't try it... try it! Documentation is pretty well on the signals topic and they are rather easy to learn. What might be tricky is where should you connect signals probably (I prefer to do that at the end of models module).
Unfortunately, there is no best approach for your answer. Also, remember that neither save method nor post_save signal are not fired if you i.e. insert instances with raw SQL or do bulk_create (https://docs.djangoproject.com/en/stable/ref/models/querysets/#bulk-create). So identify places where you want this automatic permission assignment to be happening (that should be probably one place anyway).
Alternatively, you can add FK field pointing at the creator to your model. You would be able to use that information instead of checking permission with guardian (and as you noted about using mixins that should actually fit your problem well too). I used that approach for my project management application some time ago.
Hope that helps!
https://docs.djangoproject.com/en/stable/ref/models/querysets/#bulk-create

Related

How can I redact a Django object instance based on the user's view permission?

I would like to redact object instances based on the user's view permission.
For instance, if I have the following model
class Data(models.Model):
value = models.FloatField()
I would like users that have view permission, i.e. myapp.view_data, to see the full value, and users without view permission to only see a redacted version, say round(value).
I could implement this for every view and template, but that doesn't seem very DRY and would have a risk of overlooking some occurrences. Also, I suppose in a template the mechanism might be bypassed through relation queries.
How can I implement this redaction mechanism in the model? Would this be good practice? Or is there a better way?
Thank you!
You can make a class method called get_modified_value(user). This would basically be a wrapper for the value field. It would take a User object as a parameter. Based on that user's permissions, it would either return the raw value or the rounded value.
I recommend this because Models are completely unaware of context or the http request. So no matter what, every time you want that value, you are going to need to do some sort of manipulation in the view. It seems cleanest to me to pass user to the model method but that might have its own problems.

Can I create a OneToOneField that will create an object if it does not exist?

I am working with a Django model that looks like this:
class Subscription(models.Model):
user = models.OneToOneField(User)
That is, it has a one-to-one association with the User class from Django's auth module. This association is not optional; however, thanks to some legacy code and some manual tinkering with the database, there are cases in which a user does not have an associated subscription, which means that code like this:
sub = user.subscription
will throw a DoesNotExist exception. However, much of the codebase assumes that every user has an associated Subscription object.
Is there a way I could subclass OneToOneField such that, if the associated Subscription object does not exist in the database, I will create one and return it when user.subscription is called, instead of throwing an exception?
The correct way to do this is to catch the post_save signal, creating an object if necessary.
Add a property named subscription and in getter create necessary reference, for that you may have to do monkey-patching, better alternatives are
refactor your code and add a utility function to get subscription
refactor your code and add a proxy model for User
Just fix the db once

what type of model design should I use to add roles to Users in such a way that it works in Django and in the Django admin

I am using Djangos default authentication system (django.contrib.auth) and I would like to add 'roles' to my users in such a way that Django Admin can work with it.
An example:
A user can be a staffmember, teacher, student and/or parent
If the user has a role assigned, he will gain permissions (eg. staffmembers may sign in to the Django admin)
Some roles might have some extra fields (eg. parent has a relation with at least one student and each student has a field with it's classgroup
Not every role has extra fields
A parent can be a teacher or staffmember and vise versa
A student can not have another role
There are all sorts of (conventional) ways to accomplish the above within a model. Django supports a lot of them, but the Django admin does not. The Django admin has a lot of good features so I would like to use it (but I am getting more and more afraid that it will not be possible).
The following model is what I thought of at first:
class ExtendedUser(contrib.auth.models.User):
"""
For the ease of use I inherit from User. I might
want to add methods later
"""
pass
class StaffMember(models.Model):
"""
A staffmember is a co-worker of the organisation
and has permissions to make changes to (parts of)
the system.
For now the staffmember only has methods
"""
user = models.OneToOneField(ExtendedUser, related_name="staff_member")
class Student(models.Model):
"""
A student can participate in some courses
"""
user = models.OneToOneField(ExtendedUser, related_name="student")
class Teacher(models.Model):
user = models.OneToOneField(ExtendedUser, related_name="teacher")
subjects = models.ManyToManyField(..)
class Parent(models.Model):
user = models.OneToOneField(ExtendedUser, related_name="parent")
children = models.ManyToManyField(Student, related_name=parents")
This works in Django (and in a lot of other MVC-based frameworks). But I can't find a proper way to display the above in the admin.
Ideally I would like to add a User and then within the User-changeview add different roles. At first I thought I could use Inlines:
class StudentInlineAdmin(admin.StackedInline):
model = Student
extra = 0
template = 'accounts/admin/role.html'
I then make some slight changes to the inline template to present the editing user button with a caption 'Add Student role'. Once we hit the button, we display the form and a User role is added. Not ideal, but it works.
Too bad, for Staffmembers there are no fields to add to the inline form. This way it is not possible to trigger the 'has_changed' property for inlines forms. This results in the new role not being saved to the database.
To solve this last problem, I hacked around a bit and added a 'dummy' formfield to the empty user-roles and then hide this field using JS. This did trigger the has_changed.
Still this would not work for somehow none of my inline-models are saved during some tests later on.
So I think I am just doing it the wrong way. I did a lot of Googling and found a lot of people hassling with the same sorts of problems. The one that suited my situation the most was http://www.mail-archive.com/django-users#googlegroups.com/msg52867.html. But still this solution does not give me an easy way to implement the admin.
I also thought about using the built-in groups but in that case I have no idea how I should add the different fields.
Another thing I thought of was trying to 'Add a student' instead of adding a User and assigning a role to him. This works pretty well in the admin if you just inherit the user:
class StudentUser(auth.models.User):
pass
But two problems here. At first it is not possible to be a staffmember and a teacher. Second it is not really working in the rest of Django for the request object return a User object for which it is impossible to request the Student, Parent, Staffmember object. The only way to get one of these is to instantiate a new Student object bases on the User object.
So here is the question: what type of model design should I use to add roles to Users in such a way that it works in Django and in the Django admin?
Friendly Regards,
Wout
I'm assuming in the following that you do not want to alter the admin, or make a copy of django.contrib.admin and hack it as desired.
A user can be a staffmember, teacher, student and/or parent
You could store this in a user profile. Inheriting from User will work, too, but instead of using User.get_profile(), you'll need to manually map from User to ExtendedUser.
If the user has a role assigned, he will gain permissions (eg. staffmembers may sign in to the Django admin)
In that specific case, you can't use automatic role-based assignment. Whether or not a person can access the admin is determined by the is_staff field in their User record.
The most automatic way I can think of is to create an "Update Permissions" admin command, which will update admin fields like is_staff and the permissions based on the role set in the user's profile. BTW, even though this is not fully "automatic", it is a denormalization that can improve performance.
Some roles might have some extra fields (eg. parent has a relation with at least one student and each student has a field with it's classgroup
Not every role has extra fields
A parent can be a teacher or staffmember and vise versa
A student can not have another role
Read up on form validation. That's where you can enforce these rules.
In your model, I'd recommend that you alter the related names of your one-to-one fields:
class StaffMember(models.Model):
user = models.OneToOneField(ExtendedUser, related_name="as_staff_member")
class Student(models.Model):
user = models.OneToOneField(ExtendedUser, related_name="as_student")
class Teacher(models.Model):
user = models.OneToOneField(ExtendedUser, related_name="as_teacher")
class Parent(models.Model):
user = models.OneToOneField(ExtendedUser, related_name="as_parent")
Since theUser.as_teacher is a lot clearer than theUser.teacher (which I would read as "the user's teacher").
This works in Django (and in a lot of other MVC-based frameworks). But I can't find a proper way to display the above in the admin.
You're going to have one table in the admin per role. There's no fancy "bottom half of the edit page will redraw itself when you change roles" feature. If you want that, you will need to write your own admin.
Django's admin is great, but it's not trying to be everything to everyone. I have a role-based setup like yours (except the roles themselves are stored in a table), and the admin works fine if a little clunky. The general idea is that if the admin isn't good enough, then you should be writing your own views.
I also thought about using the built-in groups but in that case I have no idea how I should add the different fields.
The built-in groups are not what you're looking for.
Another thing I thought of was trying to 'Add a student' instead of adding a User and assigning a role to him. [...] At first it is not possible to be a staffmember and a teacher.
"Subclassing" is a more restrictive one-to-one. I think your initial model is better.
Second it is not really working in the rest of Django for the request object return a User object for which it is impossible to request the Student, Parent, Staffmember object. The only way to get one of these is to instantiate a new Student object bases on the User object.
No, you instead find the Student object using the auto-generated id from the User object:
try:
theStudent = Student.objects.get(user_ptr_id=theUser.id)
except Student.DoesNotExist:
# That user isn't a student; deal with it here.
If you're going to use the admin, I think you're going to have to live with a two-step process of adding an ExtendedUser, then adding Student or whatever entries for them.
So it comes down to a tradeoff: a little extra work using the built-in admin, or writing your own user management views. WHich route is best really depends on how much this interface will be used: If it's just you, then the admin should be fine, even with its warts. If a lot of people will be using it on a routine basis, then you should just write your own views to handle things.

Is there an easy way to provide a custom django admin action without allowing changes to models?

I have a custom action on a model Foo all set up and ready to go, complete with a new permission I've made.
Problem is, my administrators need the can_change_foo permission to view a change list and perform that custom action (which I don't want to award).
Is there an easier way to set this up without rewriting the model list admin view?
There is a horrid hack I can think of... Completely untested, obviously...
You could disable all other actions by overriding get_actions() and only allow for your custom action. Then you could follow T.Stone's suggestion here and completely disable links to edit individual instances of the model. What that would allow you to do is give your users the can_change_foo permission knowing that the only action they would be able to perform was yours.
Not pretty... Especially the part about not linking to the edit page...
Is re-writing the list admin view that bad? :-)
I ended up overriding the changelist_view() method on my ModelAdmin class, copying the defaults from django and just commenting out the permissions check. The list (at least the way I have it configured) doesn't have links to edit the individual objects, and even if it did, django raises a PermissionDenied if you try and edit an individual object. (since I never awarded the can_change permission).
It's a hack, and some monkeypatching, but until there's a separate permission for viewing a changelist, it works pretty well.

How can I easily mark records as deleted in Django models instead of actually deleting them?

Instead of deleting records in my Django application, I want to just mark them as "deleted" and have them hidden from my active queries. My main reason to do this is to give the user an undelete option in case they accidentally delete a record (these records may also be needed for certain backend audit tracking.)
There are a lot of foreign key relationships, so when I mark a record as deleted I'd have to "Cascade" this delete flag to those records as well. What tools, existing projects, or methods should I use to do this?
Warning: this is an old answer and it seems that the documentation is recommending not to do that now: https://docs.djangoproject.com/en/dev/topics/db/managers/#don-t-filter-away-any-results-in-this-type-of-manager-subclass
Django offers out of the box the exact mechanism you are looking for.
You can change the manager that is used for access through related objects. If you new custom manager filters the object on a boolean field, the object flagged inactive won't show up in your requests.
See here for more details :
http://docs.djangoproject.com/en/dev/topics/db/managers/#using-managers-for-related-object-access
Nice question, I've been wondering how to efficiently do this myself.
I am not sure if this will do the trick, but django-reversion seems to do what you want, although you probably want to examine to see how it achieves this goal, as there are some inefficient ways to do it.
Another thought would be to have the dreaded boolean flag on your Models and then creating a custom manager that automatically adds the filter in, although this wouldn't work for searches across different Models. Yet another solution suggested here is to have duplicate models of everything, which seems like overkill, but may work for you. The comments there also discuss different options.
I will add that for the most part I don't consider any of these solutions worth the hassle; I usually just suck it up and filter my searches on the boolean flag. It avoids many issues that can come up if you try to get too clever. It is a pain and not very DRY, of course. A reasonable solution would be a mixture of the Custom manager while being aware of its limitations if you try searching a related model through it.
I think using a boolean 'is_active' flag is fine - you don't need to cascade the flag to related entries at the db level, you just need to keep referring to the status of the parent. This is what happens with contrib.auth's User model, remember - marking a user as not is_active doesn't prompt django to go through related models and magically try to deactivate records, rather you just keep checking the is_active attribute of the user corresponding to the related item.
For instance if each user has many bookmarks, and you don't want an inactive user's bookmarks to be visible, just ensure that bookmark.user.is_active is true. There's unlikely to be a need for an is_active flag on the bookmark itself.
Here's a quick blog tutorial from Greg Allard from a couple of years ago, but I implemented it using Django 1.3 and it was great. I added methods to my objects named soft_delete, undelete, and hard_delete, which set self.deleted=True, self.deleted=False, and returned self.delete(), respectively.
A Django Model Manager for Soft Deleting Records and How to Customize the Django Admin
There are several packages which provide this functionality: https://www.djangopackages.com/grids/g/deletion/
I'm developing one https://github.com/meteozond/django-permanent/
It replaces default Manager and QuerySet delete methods to bring in logical deletion.
It completely shadows default Django delete methods with one exception - marks models which are inherited from PermanentModel instead of deletion, even if their deletion caused by relation.