Django Wagtail dynamic Menu's based on User and User Role - django

Using Django-Wagtail and Wagtail Menu's. I want to build a system with the following types of characteristics.
I have a class of user (Vendor, Supplier)
Each created user would need to be one of these two classes.
Each class of user has different roles and the roles differ from class to class. e.g.
Vendor: finance, stock, admin
Supplier: finance, stock, admin, driver.
What I have done is create a class called UserType and both "Vendor" & "Supplier" inherit from this class. Then added a field to the User class that selects which type of user they are.
class UserType(model.Model):
common fields.......
class Vendor(UserType):
child_vendor_only_fields.....
class Supplier(UserType):
child_supplier_only_fields.....
class User(AbstractUser):
userVar = models.ForeignKey(UserType, on_delete=models.SET_NULL, null=True, blank=True)
I have also used the wagtail admin to create custom roles for each of these classes as described above.
Clearly what "Vendors" can create, read, update and delete varies from that of a "Supplier". And still within that what the finance role can do within the "Vendor" differs from what "Stock" can do.
How would I create a dynamic menu that displays different menu's for these permutations?
My initial thoughts were inspired by this. Simply adding a choicefield
USER_TYPE_CHOICES = (
('Vendor', 'Vendor'),
('Supplier', 'Suplier'))
class GenericPage(MenuPage):
"""
This model will gain the fields, methods and `setting_panels` attribute
from `MenuPage`, but `settings_panels` is being overridden to include
other fields in the `Settings` tab.
"""
availableTo = CharField(choices=USER_TYPE_CHOICES,)
# 'menupage_panel' is a collapsible `MultiFieldPanel` with the important
# fields already grouped together, making it easy to include in custom
# panel definitions, like so:
settings_panels = [
FieldPanel('custom_settings_field_one'),
menupage_panel
]
I have two questions:
Am I on the correct path for generating custom menu's for type of users? If so how would I then do the same for roles under type of users?
In terms of CRUD should I simply implement a check each time a user does a crud action whether or whether not they can commit that action pragmatically?

For CRUD, I'm not a fan of giving someone the option to do something then tell them that option isn't allowed after the fact. They might have spent half an hour creating/editing that page then find out they've wasted that time. Better to tell them on page load.
For user type, I'd just create those as roles that you add the users to. Then you just have a simple test for the user.groups to see what role they have.
You could easily develop a custom restricted panel for each of the panel types you need to restrict, easier if they're all FieldPanels. In the BoundPanel class you have access to the request object, so you can pull the user object from that at runtime and and decide what to do then.
The Panel.BoundPanel has a method is_shown(), so you can create a custom panel that inherits FieldPanel, override that method to test if the request user in an authorised list parameter and set true/false accordingly.
# restricted_field_panel.py
from wagtail.admin.panels import FieldPanel
class RestrictedFieldPanel(FieldPanel):
def __init__(self, field_name, authorised_groups, **kwargs):
self.field_name = field_name
self.authorised_groups = authorised_groups if isinstance(authorised_groups, list) else [authorised_groups]
super().__init__(self.field_name, **kwargs)
def clone_kwargs(self):
kwargs = super().clone_kwargs()
kwargs.update(
authorised_groups=self.authorised_groups
)
return kwargs
class BoundPanel(FieldPanel.BoundPanel):
def is_shown(self):
show_field = super().is_shown()
is_authorised = self.request.user.groups.get_queryset().filter(name__in=self.panel.authorised_groups).exists()
return (show_field and is_authorised)
In the page model:
content_panels = Page.content_panels + [
RestrictedFieldPanel('some_vendor_field', 'Vendors'),
RestrictedFieldPanel('some_supplier_field', 'Suppliers'),
....
]
For the authorised groups, you might want to append a site admin role by default so that site admins will always see any restricted panel regardless.
Other panel types you'd need to inherit and do test wherever it's doing the rendering.

Related

Django: Custom default Manager with user based queries for all views/interfaces (custom, admin, graphene, rest, ...)

In Django I'm trying to implement some kind of a "security middleware", which gives access to certain db information only, if the logged in user matches.
Up to now I found two approaches: middleware or custom function in manager (Both explained here Django custom managers - how do I return only objects created by the logged-in user?).
Example for cleanest solution: custom function
class UserContactManager(models.Manager):
def for_user(self, user):
return self.get_query_set().filter(creator=user)
class MyUser(models.Model):
# Managers
objects = UserContactManager()
# then use it like this in views
data = MyUser.objects.for_user(request.user)
However, this solution only works, if you have control over the code which invokes this custom function (here: for_user()).
But if you are using third parties apps like Django-REST, Graphene or AdminViews, they don't have the possibility to configure a specific query-func to use.
My goal:
Replace the default Manager
Add user-based filters to query_set
Use the model as normal in all app configurations
Idea (pseudo code!)
from django.x import current_user
class UserBasedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(author=current_user.name)
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=50)
objects = UserBasedManager() # The default manager.
Now I could use the model Book as normal in all extensions.
I know this solution would have some drawbacks, which are okay for me:
Can be only used when user is available (e.g. no direct script support)
Handling no or anonymous user is missing (left it away to keep example short)
My use case
I have a project, which shall give access to data by different interfaces (admin pages, custom views, rest, graphql).
As I have so many interfaces, I don't want to implement the access-rights in the views. This would cost too much time and is hard to maintain and the risk for security problems in one specific view/interface is too high.
Let's say I'm storing commits of git repositories in a database.
And the user shall get access to all commits, which are part of repos the user has READ access.
The question is: How can I implement this as a generic, view/interface independent solution?
Install django-threadlocals package and call get_current_user function to get the current user
from threadlocals.threadlocals import get_current_user
class UserBasedManager(models.Manager):
def get_queryset(self):
current_user = get_current_user()
return super().get_queryset().filter(author=current_user.name)

Dynamically created proxy models do not have listed permissions on admin site

Here is my models.py:
class Item(models.Model):
# ... some irrelevent fields ...
tags = models.ManyToManyField('Tag')
class Tag(models.Model):
name = models.CharField(max_lenght=30)
category_id = models.IntegerField()
Tag is actually a general-purpose name. Each item has many different type of tags - currently there are four types: team tags, subject tags, admin tags and special tags. eventually there will probably be a few more.
The idea is, they all have basically the same fields, so instead of having like 4 tables with manytomany relationship, and instead of adding a new column for Item whenever adding a new type, everything is called a 'tag' and it's very easy to add new types without any change to the schema.
Now to handle this in the admin.py I'm using dyamically created proxy models (based on this), as such:
def create_modeladmin(modeladmin, model, name = None):
class Meta:
proxy = True
app_label = model._meta.app_label
attrs = {'__module__': '', 'Meta': Meta}
newmodel = type(name, (model,), attrs)
admin.site.register(newmodel, modeladmin)
return modeladmin
class TagAdmin(models.Model):
def queryset(self):
return self.model.objects.filter(category_id = self.cid)
class TeamAdmin(TagAdmin):
cid = 1
class SubjectAdmin(TagAdmin):
cid = 2
# ... and so on ...
create_modeladmin(TeamAdmin, name='Teams', model=Tag)
create_modeladmin(SubjectAdmin, name='Subject', model=Tag)
#... and so on ...
This works great for me. However, different staff members need different editing permissions - one guy shouldn't access admin tags, while the other should only have access to edit subject-tags and team-tags. But as far as the admin site is concerned - the dynamic models do not exist in the permission list, and I can't give anyone permissions regarding them.
i.e. a user given all permissions on the list will still not have access to edit any of the dynamic models, and the only way to let anyone access them at all is to give him a superuser which obviously defies the point
I searched SO and the web and I can't anyone with a similar problem, and the docs don't say anything about this not in the dynamic models section or the proxy models section. so I'm guessing this is a different kind of problem. Any help would be greatly appreciated
UPDATE
So after some research into it, the answer was simple enough. Since permissions in django are objects that are saved to the database, what I needed to do was simple - add the relevent permissions (and create new ContentType objects as well) to the db, and then I could give people specific pemissions.
However, this raised a new question - is it a good convention to put the function that creates the permissions inside create_modeladmin as a find_or_create sort of function (that basically runs every time) or should it be used as an external script that I should run once every time I add a new dynamic model (sort of like how syncdb does it)?
And is there a way to also create the permissions dynamically (which seems to me like the ideal and most fitting solution)?
of course you can create permissions, django have django.contrib.auth.management.create_permissions to do this

Better to use 3rd Party Row-Level Permission App or to Filter Query Results in a Django View?

I am writing a Django app that involves the creation of documents. I have a few requirements:
Check to see if the user is able to view any documents at all.
If the user is allowed to view documents, only allow them to view documents they have permission for.
I have come up with two solutions and am wondering if one is philosophically/practically better than the other.
The two solutions I've come up with are:
Solution One (using third-party Django-Guardian)
Models.py
class Document(models.Model)
owner = models.ForeignKey(User)
document_name = models.CharField(max_length=60)
document_content = models.TextField()
class Meta:
permissions = (
('view_document', 'View Document'),
)
views.py
#permission_required('document.view_document', (Document, 'pk', 'document_id'))
def view_document(request, document_id):
document = Document.objects.get(pk=document_id)
return render_to_response("native/view_events.html",
{
'document' : document,
}, context_instance=RequestContext(request))
The downside I see to solution number one is that I have to explicitly set permissions every time I create an object, plus I have to hit the database twice: once to check permissions and again to retrieve the document object.
Solution Two (using built-in Django permissions)
Models.py
class Document(models.Model)
owner = models.ForeignKey(User)
document_name = models.CharField(max_length=60)
document_content = models.TextField()
viewers = models.ManyToManyField(User)
class Meta:
permissions = (
('view_document', 'View Document'),
)
views.py
#permission_required('document.view_document')
def view_document(request, document_id):
document = Document.objects.filter(pk=document_id, viewers__pk=request.user.pk)[0]
return render_to_response("native/view_events.html",
{
'document' : document,
}, context_instance=RequestContext(request))
The downside I see to solution number one is that I have to do two checks, one to see if they are able to view documents at all, and one to see if they can view the particular document. A plus side to this is that if I have an admin that I want to be able to view all documents, I don't need to explicitly grant permission to each one; I can just give him the 'view document' permission.
It seems both solutions have their pros and cons. Is there one that is better in theory/practice?
I find the second approach better. Since, you can check the model level permission on the object. Though in the first I guess, you should be able to accomplish similar things. I am not sure but if django-guardian provides a way to check the permission inside the view code instead of the decorator. You could manually check for Model Level Permission.
For example,
def view_doc(request, doc_id):
if user can not view doc: #Model Level Permission
return HttpResponse("Sorry can not view")
if check django-guardian permission #Object Level Permission
return HttpResponse("Can not view doc")
#further code
But I would suggest the second approach since, you can create an api just to check permission and customize it which is more scalable.

How do I specify (1) an order and (2) a meaningful string representation for users in my Django application?

I have a Django application with users. I have a model called "Course" with a foreign key called "teacher" to the default User model that Django provides:
class Course(models.Model):
...
teacher = models.ForeignKey(User, related_name='courses_taught')
When I create a model form to edit information for individual courses, the possible users for the teacher field appear in this long select menu of user names.
These users are ordered by ID, which is of meager use to me. How can I
order these users by their last names?
change the string representation of the User class to be "Firstname Lastname (username)" instead of "username"?
Firsty, the order. You can define a default order of model using the nested Meta class. Check the ordering section.
Secondly, representation. You have to define a __str__()/__unicode__() methods for your model. They should return a string which represents an object. You can see an example in documentation. BUT, since User is a model from an outer module it may be hard to do it in that way.
You probably can:
monkey-patch the User model meta class during app initialization OR
subclass the User model, add Meta to the subclass and use it in place of the User OR
write a custom field / form template which uses objects attributes instead of calling str()
it all depends on the current case
Abstract
If you're only dealing with the admin, it's better not to tinker with the User model itself, as subclassing User can be a pain down the road (especially when / if other developpers are going to work on your project), but use the admin's customization options.
Solution
To solve your issue in the admin, you could use the ModelAdmin option raw_id_fields.
This will replace the <select> input with a widget that you can click to be redirected to your User admin and choose your user from.
From there, it's trivial to customize your User admin so that it:
Displays your users in a relevant order (ModelAdmin.ordering)
Displays the fields you're interest in (ModelAdmin.list_display)
Just remember than when you're registering your User admin, you need to use:
admin.site.unregister(User) #This!
admin.site.register(User, MyCustomUserAdmin)
Alternate solution
Alternatively, you can always use ModelAdmin.formfield_overrides, which is more powerful but more complicated.
Reference
You should look into the Django documentation for details on how to use those attributes.
You could achieve this by customizing form field, here the forms.ModelChoiceField
class CustomizedModelChoiceField(forms.ModelChoiceField):
def __init__(self, *args, **kwargs):
# change default ordering
super(CustomizedModelChoiceField, self).__init__(*args, **kwargs)
self.queryset = self.queryset.order_by('last_name')
def label_from_instance(self, obj):
# change representation per item
return u'{obj.first_name} {obj.last_name} ({obj.username})'.format(obj=obj)
Then use it in your form to replace default ModelChoiceField.
In Django Admin, it looks like
class CourseAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'teacher':
kwargs['form_class'] = CustomizedModelChoiceField
return super(CourseAdmin, self).formfield_for_dbfield(db_field, **kwargs)

Django admin inline forms - limit foreign key queryset to a set of values

I have some interrelated models that need to co-exist on a single admin page. Here's the idea:
Theater productions have cast members, and cast members have specified roles. A theater production is related to a given written text (play, adaptation, etc.), and the written text holds a list of all the roles for that text. When adding a Production, each cast member needs to be associated with one of those roles.
Here's how the data model is working:
Models: Production, Person, CastMember, Role, WrittenText
Relationships: Production and Person have an M2M relationship through CastMember, which adds a "role" field - a ForeignKey to a Role object. Role itself has a ForeignKey to a WrittenText object.
So, the problem is this: in the admin page for Productions, I have a TabularInline to add CastMembers. The CastMember entries in the table should have their 'role' field limited to only the roles specified in the WrittenText that the Production references.
I made a half-way solution to the problem by overriding the model form:
class CastMemberForm(ModelForm):
class Meta:
model = CastMember
def __init__(self, *args, **kwargs):
super(CastMemberForm, self).__init__(*args, **kwargs)
if 'instance' in kwargs:
self.fields['role'].queryset = Role.objects.filter(source_text=self.instance.production.source_text)
But, this only works if you choose a Person from the drop-down, save, and then choose the role - otherwise you just get a list of all roles. Taking out "if 'instance' in kwargs" gives me a DoesNotExistError.
Is this just way too complex to do without something like client-side JS, or is there a simpler solution that I'm missing?
Here is an example of chained select boxes via javascript/ajax. It should basically be the same principle, but you should need to tweak the js to not update one select box, but all of them in the inline admin... Maybe this gives you a small inspiration!