Django admin - giving users access to specific objects/fields? - django

I need to make an "owners" login for the admin. Say we have this model structure:
class Product(models.Model):
owner = models.ManyToManyField(User)
name = models.CharField(max_length=255)
description = models.CharField(max_length=255)
photos = models.ManyToManyField(Photo, through='ProductPhoto')
class Photo(models.Model):
order = models.IntegerField()
image = models.ImageField(upload_to='photos')
alt = models.CharField(max_length=255)
class ProductPhoto(models.Model):
photo = models.ForeignKey(Photo)
product = models.ForeignKey(Product)
We have a group called Owners that some users are part of. The ProductPhoto is a TabularInline on the Product admin page.
Now, owners need permission to edit
(primary goal) only products where product__in=user.products (so basically, only products owned by them).
(secondary goal) only the description and photos of products
How would I do this with Django's admin/permission system?

This is row (or object) level permission. Django provides basic support for object permissions but it is up to you to implement the code.
Luckily, there are a few apps that provide drop-in object-level permission framework. django-guardian is one that I have used before. This page on djangopackages.com provides some more that you can try out.

You may implement using get_form. For complex rule, you can add this too: https://github.com/dfunckt/django-rules
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
# permission check;
if form.base_fields and not request.user.is_superuser:
# when creating or updating by non-reviewer (except superuser)
# allow only reviewer to allow updating
form.base_fields['usertype'].disabled = True

Related

Is there a way to optimize the creation and retrieval of users (that are also part of another model) in Django with just one request?

My app has Users that can be Doctors/Patients/Secretaries. To create a Doctor, therefore, I perform two POST requests: one for the creation of a User and one for a Doctor. The way I do this, the User has to be created first so that I can later create the Doctor (Doctor requires a 'User' field). I am using Django Rest Framework to create the API.
class User(AbstractUser):
# defined roles so when I retrieve user, I know to perform a
# request to api/doctors/ or api/secretaries/ etc depending on role.
ROLES = (
('d', 'Doctor'),
('s', 'Secretary'),
('p', 'Patient'),
)
role = models.CharField(
max_length=1, choices=ROLES, blank=True, default='p', help_text='Role')
class Doctor(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
national_id = models.CharField(max_length=32, blank=False)
...
Since I'm new to Django, I don't know if two requests is the standard/best way of creating this User/Doctor.
This comes to mind as I am also thinking of the GET methods which will be performed later on (two GET requests when a Doctor logs in if I want to retrieve all of their info (User/Doctor)?)
I read about subclassing, which would be something like Doctor(User), then the only necessary request would be a single POST to create a Doctor (which would alongside create the User). I am, however, skeptical of subclassing the User as I read at least 3 SO answers stating it could cause problems in the future.
have a look at this good tutorial https://simpleisbetterthancomplex.com/tutorial/2018/01/18/how-to-implement-multiple-user-types-with-django.html which explain 2 different approches
extend AbstractUser with flags is_doctor, is_secretary, is_patient
class User(AbstractUser):
is_doctor = models.BooleanField('Doctor status', default=False)
is_secretary = models.BooleanField('Secretary status', default=False)
is_patient = models.BooleanField('Patient status', default=False)
using roles which suites your case:
class Role(models.Model):
'''
The Role entries are managed by the system,
automatically created via a Django data migration.
'''
ROLE_CHOICES = (
('d', 'Doctor'),
('s', 'Secretary'),
('p', 'Patient'),
)
id = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, primary_key=True)
def __str__(self):
return self.get_id_display()
class User(AbstractUser):
roles = models.ManyToManyField(Role)
Problem
Determine best practices for handling multiple user types and adding attributes to a user in Django.
Solution
The following is are recommendations based on design patterns that have been commonly used in Django since version 0.96. These recommendations are present in Django’s documentation (see: References).
Roles
Use Django’s built in permissions module for role and group management instead of rolling your own role and group management.
Instead of a model for each type, create a single UserProfile model, relegating user types to being managed by permissions model.
I recommend using a data migration to add groups so that default groups are automatically seeded on initial migrate call—this reduces overhead for anyone setting up your project for the first time.
OneToOne
Use Django OneToOne field instead of ForeignKey.
UserProfile and Signals
Create a signal that creates a UserProfile on User create.
Example
class UserProfile(models.Model):
user = models.OneToOne(“User”, on_delete=models.CASCADE)
national_id = models.CharField(max_length=32,
def create_profile(sender, **kwargs):
user = kwargs["instance"]
if kwargs["created"]:
user_profile = UserProfile(user=user)
user_profile.save()
post_save.connect(create_profile, sender=User)
References
Django permissions (groups): https://docs.djangoproject.com/en/3.1/topics/auth/default/#groups
Django Data Migrations: https://docs.djangoproject.com/en/3.1/topics/migrations/#data-migrations
Django extending User model recommendation: https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#extending-django-s-default-user
Django post_save signal: https://docs.djangoproject.com/en/3.1/ref/signals/#post-save
in your case (the model you made) you can create the doctor and the user in one post request to the doctor creation by overriding the create function for the
from rest_framework.generics import CreateAPIView, ListAPIView
class CreateDoctorViewSet(CreateAPIView, ListAPIView):
def create(self, request, *args, **kwargs):
data = self.request.data
user_dict_keys = ["username", "email", "first_name", "last_name"]
user_dict = {key: data.pop(key, None) for key in user_dict_keys}
user_dict['role'] = "d"
user_serializer = UserSerializer(data=user_dict)
# if it's not valid it will return the exception details for the requester
user_serializer.is_valid(raise_exception=True)
user = user_serializer.create(user_dict)
user.set_password(data['password'])
data.pop("password", None)
user.save()
response = super().create(request, *args, **kwargs)
if response.statu_code == 201:
return response
# if an error happened while in the doctor model (model error or serializer error) >> delete the created user
user.delete()
return response
or made a little more DRY :
from rest_framework.generics import CreateAPIView, ListAPIView
def create_user(self):
data = self.request.data
user_dict_keys = ["username", "email", "first_name", "last_name"]
user_dict = {key: data.pop(key, None) for key in user_dict_keys}
user_dict['role'] = "d"
user_serializer = UserSerializer(data=user_dict)
# if it's not valid it will return the exception details for the requester
user_serializer.is_valid(raise_exception=True)
user = user_serializer.create(user_dict)
user.set_password(data['password'])
data.pop("password", None)
user.save()
return user
class CreateDoctorViewSet(CreateAPIView, ListAPIView):
def create(self, request, *args, **kwargs):
user = create_user(request)
response = super().create(request, *args, **kwargs)
if response.statu_code == 201:
return response
# if an error happened while in the doctor model (model error or serializer error) >> delete the created user
user.delete()
return response
Personal Advices:
in the case you're providing the User model has one role so it's better to make the user field in the Doctor class OneToOne instead of ForeignKey.
Of course if you have cases where there are people for example converting from Doctor to Secretary and you want them to switch between roles on the same account you can keep the ForeignKey on Doctor model but you have to make multiple roles possible in the user model.
What you mean by subclassing it is called Multi table inheritance. And there is no problem in using it, no side effects and it is perfectly compatible with Django Rest Framework (which you have tagged). This is the way it works:
class User(AbstractUser):
# Your common fields for all user types.
class Doctor(User):
national_id = models.CharField(max_length=32, blank=False)
class Secretary(User):
# Your specific fields for secretary model
class Patient(User):
# Your specifict fields for patient model
In background, it uses a OneToOne relationship for each subtype.
Advantajes of using Multi table inheritance:
It is simple and elegant: you don't have to take care of different tables, queries, etc; Django does it for you.
It also unsures a good and formalized structure of your database: different tables for common and specific data in OneToOne relationship.
It is suitable for your needs? That depends.
If each subtype has its own specific fields -> use multi table inheritance without doubt.
If each subtype has the same set of fields but different behaviours (different class/model methods, code, etc) -> use proxy models.
If all the subtypes have the same set of fields and the same behaviour (same class/model methods, code, etc) -> use role based approach (one field identifying the role).
(extra) If you have dozens of subtypes, each one of them has different fields and you don't care too much about database formalization -> Don't use Multi table inheritance. In this case, you can use a mix of role based approach with JSON fields (for storing all the specifict fields) and proxy models (for handling different behaviours)**.

Django ManyToMany relationship, filter options of modelmultiplechoicefield

I am currently rewriting a web app that is utilizing a hierarchy of relationships which is making it extremely painful to create a reliable work flow for the creation of objects. The current set up is as follows:
Customer Hierarchy:
Organization -> Location -> Room
Contacts can belong to one or many of these entries at any level of the hierarchy. Ex Jim can be assigned to Organization and location.
With this I need to filter the django many to many field that is populated with any contact that doesn't belong anywhere OR belongs to a parent or child level of the customer hierarchy.
I have attempted inline formsets which fails on the many to many model as well as limit_choices_to={'Organization':_get_self_pk} . This works but doesn't allow for the use of django admin style on the fly creation of contacts. I have also attempted to use a queryset in the init function for create, but my form has a nested inline formset that doesn't allow me to use the self.field['contact'] to inject the queryset. (Key Error, contacts doesn't exist as a field)
Models.py
class Organization(AbstractExclusions, AbstractManyToManyCommonInfo, AbstractCommonInfo, AbstractOrganizationLocationCommonInfo, AbstractAcvitivyInfo):
....
contact = models.ManyToManyField('Contact', blank=True)
class Location(AbstractManyToManyCommonInfo, AbstractCommonInfo, AbstractOrganizationLocationCommonInfo, AbstractLocationRoomCommonInfo, AbstractAcvitivyInfo, AbstractExclusions):
....
contact = models.ManyToManyField('Contact', blank=True)
class Room(AbstractManyToManyCommonInfo, AbstractCommonInfo, AbstractLocationRoomCommonInfo, AbstractAcvitivyInfo, AbstractExclusions):
....
contact = models.ManyToManyField('Contact', blank=True)
class Contact(AbstractUserInfo):
phone_number = PhoneNumberField(verbose_name=_('Phone Number'))
is_user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE, verbose_name=_('Link To Authenticated User'),)
role = models.ForeignKey(refModels.Role, verbose_name=_("Role"), on_delete=models.CASCADE, null=True, blank=True)
This question was answered by creating a special init function as shown below:
def __init__(self, *args, **kwargs):
super(OrganizationForm, self).__init__(*args, **kwargs)
if 'initial' in kwargs:
try:
self.fields['contact'].queryset = (custModels.Contact.objects.filter(Q(organization = None) | Q(organization = self.instance.id)))
except:
pass

How to let user fetch only those objects they are related to (by many-to-many relationship)?

I have a project in which there are workspaces, and each workspace can have any number of users. Users can also belong to multiple workspaces, so there's a many-to-many relationship between them.
Now, I have workspace creation and membership working, but I'm having trouble setting the permissions so that only the workspace members can see the workspace. I've tried with a custom object-level permission, but it doesn't seem to work.
The workspace model looks like this:
class Workspace(models.Model):
name = models.CharField(max_length=100)
users = models.ManyToManyField(User, related_name='workspaces')
The view looks like this:
class WorkspaceViewSet(viewsets.ModelViewSet):
queryset = Workspace.objects.all().order_by('name')
serializer_class = WorkspaceSerializer
permission_classes = [permissions.IsAuthenticated|BelongsInWorkspace]
The serializer is like this:
class WorkspaceSerializer(serializers.ModelSerializer):
class Meta:
model = Workspace
fields = ('name', 'users')
def create(self, validated_data):
instance = super(WorkspaceSerializer, self)
instance.users.add(self.context['request'].user)
return instance
And finally, the custom permission I'm trying to use here:
class BelongsInWorkspace(BasePermission):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
return obj.users.filter(pk=request.user).exists()
I would highly recommend django-guardian to handle this problem. Django-guardian allows for straightforward, object-level permission management.
To handle your problem, all you would need to do is add a Meta class to your workspace model, where you can create custom permissions.
class Workspace(models.Model):
name = models.CharField(max_length=100)
users = models.ManyToManyField(User, related_name='workspaces')
class Meta:
default_permissions = ('add', 'change', 'delete')
permissions = (
('access_workspace', 'Access workspace'),
)
Assign either a user or a group that permission, attached to a specific workspace.
assign_perm('access_workspace', user, workspace)
#This will return True if the user has been properly assigned the permission
user.has_perm('access_workspace', workspace)
Then to get all workspaces a user has access to, you would just need to call get_objects_for_user() in your view
queryset = get_objects_for_user(self.request.user, 'project.access_workspace')

How to implement different permission in Django?

I have a model Payment with fields: date_created, comment, description, amount. Each payment belongs to User.
class Payment(models.Model):
author = models.ForeignKey('auth.User')
amount = models.FloatField(max_length=10)
description = models.CharField(max_length=128)
created_date = models.DateTimeField(
default=datetime.now())
comment = models.TextField(max_length=256)
def __str__(self):
return self.description
Now I need to implement different roles for User: regular (can CRUD his payments), manager (can CRUD users), admin (can CRUD everything). So, the main question is how to implement this roles for users during registration (further it can't be changed). I also need to be able to set this role during registration via api (django rest framework).
Finally decided to user Django built-in Permissions
#e.g.
content_type = ContentType.objects.get_for_model(User)
permission = Permission.objects.get(codename='admin_can_manage_users')
user.user_permissions.add(permission)

django admin add data with fixed value in some field

class Facilites(models.Model):
id = models.CharField(max_length=32, primary_key=True)
name = models.CharField(max_length=128)
class Objects(models.Model):
name = models.CharField(max_length=64)
facilityid = models.ForeignKey(Facilities)
class Admins(models.Model):
user = models.OneToOneField(User)
facilities = models.ManyToManyField(Facilities)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Admins.objects.create(user=instance)
post_save.connect(create_user_profile, sender=User)
What i want is to have users (admins) only be able to add or modify "facilityid" in Objects to values specified in their Admins.facilities.
So if some user is named UserA and has facilities = ('FacA', 'FacB'), when he is adding a new object to DB, he shoudln't be able to add something like Object('Random object', 'FacC')
Also, he shouldn't be able to modify existing objects to facilities he doesn't belong to.
I have filtered the Objects with:
def queryset(self, request):
qs = super(ObjectsAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(facitityid__id__in = request.user.get_profile().facilities.all())
so users can only see the object that belong to their facilities. But i have no idea how to prevent them from adding/editing object out of their facilities.
edit:
found the answer here: https://stackoverflow.com/a/3048563/1421572
It turns out that ModelAdmin.formfield_for_foreignkey was the right answer in this situation: http://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey
I would do this with either a pre-made facility list (i.e. You could create an integer field that is hooked to FACILITY_CHOICES for the user to select from.)
If only admins can do it then permissions sounds quite viable. You can also do form validation to check for errors against the db. Depending on how many facilities you have you may want a different approach.
You can do this same technique with a models.CharField as well. So perhaps assign a 3 letter facility code to each facility and require the entry to match one of the 3 letter strings. You could even have the list in a .txt file to read from. There are really so many ways to do this. I will provide an example of a pre-made facility list and accessing the facility a particular user belongs to from the api / template:
NYC_FACILITY = 0
LA_FACILITY = 1
ATL_FACILITY = 2
FACILITY_CHOICES = (
(NYC_FACILITY, 'NYC'),
(LA_FACILITY, 'LA'),
(ATL_FACILITY, 'ATL'),
class Facility(models.Model):
name = models.IntegerField(choices=FACILITY_CHOICES, default="NYC")
class Meta:
order_by = ['name']
verbose_name_plural = "facilities"
verbose_name = "facility"
def __unicode__(self):
return self.name
As far as viewing the facilities page that a particular user belongs to you will have a m2m one to one or FK relationship between the objects. If FK or m2m relationship then you will have access to additional methods of that model type. get_related However, I'm not going to use get_related in my example. Once you are in an instance you then have access to entry_set.
# models.py
from django.auth import User
class Person(User):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
facility_loc = models.ForeignKey('Facility') # ForeignKey used assuming only one person can belong to a facility.
slug = models.SlugField(unique=True)
def get_absolute_url(self):
return "/%s/%s/" % self.facility_loc % self.slug
# views.py - TemplateView is automatically given a context variable called params which parses data from the URL. So, I'll leave the regex in the URLConf up to you.
class UserFacilityView(TemplateView):
model = Facility
template_name = "user_facility.html"
Now in your template you should be able to access facility_set from a User instance or user_set from a facility instance.