Django Tastypie: permissions for ManyToManyFields - python

It's possible give delete permissions only for a m2m field of my model?
Let's think in:
class Site(models.Model):
name = models.CharField(max_length=50)
favourited_by = models.ManyToManyField(User)
If i write this ModelResource:
class SiteResource(ModelResource):
class Meta:
queryset = Site.objects.all()
resource_name = 'Site'
allowed_methods = ['post', 'get', 'delete']
I'm giving delete permissions for the whole model, but i only want to be able to delete entries from "favourited_by" field. There's some way to achieve this?

Doing a delete on that endpoint, by definition, should delete the whole object.
But if you are looking for a custom behaviour, then you could override obj_delete with something like:
def obj_delete(self, bundle, **kwargs):
# Get the object and raise NotFound if couldn't find
try:
bundle.obj = self.obj_get(bundle=bundle, **kwargs)
except ObjectDoesNotExist:
raise NotFound("A model instance matching the provided arguments could not be found.")
#Here you should do any kind of validation before deleting
# And then remove using the parameter you passed (for example user_id)
bundle.obj.favourited_by.remove(User.objects.get(id=bundle.data['user_id'])) #pass in the user_id
return

Related

Converting fields inside a Serializer in django

I have a project where one model, called Request, has two fields (source, dest) that contain two ids which are not known to the user. However, each one is connected to another model User, who let's say that they have one field, username, which is known to the user.
Now, I want to make a serializer that can take usernames, and convert them into ids. (The opposite was simple to achieve, I just modified the to_representation method.) The problem is that when I send {'source': 'john', 'dest': 'jim'} the serializer does not take these data as valid. This was expected behavior, as it expected ids and got strings (usernames). However, even when I overridden the validate_source, validate_dest and validate methods to actually check that the usernames exist (instead of the ids), I am still getting errors that the serializer expected id but got string.
Are the validate, validate_<field> methods the wrong ones to override in this case?
Should I just convert the usernames into ids inside my view?
is it pythonic and good practice, django-wise, to receive some fields from the user and change them inside the serializer (as I change username into id)?
Current Serializer:
class RequestSerializer(serializers.ModelSerializer):
class Meta:
model = Request
fields = '__all__'
def validate_source(self, value):
username = value.get('username')
if username is None:
raise serializers.ValidationError('`user` field is required ')
return value
def validate_dest(self, value):
username = value.get('username')
if username is None:
raise serializers.ValidationError('`user` field is required ')
return value
def validate(self, attrs):
self.validate_source(attrs['source'])
self.validate_dest(attrs['dest'])
return attrs
def to_representation(self, instance):
# do things
pass
Please notice that this is not the whole functionality of my serializer. To convert from an id to a username I have to check the data of another Model, So I cannot use a SlugRelatedField.
Also, username is not the only item returned by the serializer. It also returns a 'class' field, depending on which group the the user has joined. The user may join more than one group, and each user-group combination has its own id. In the same way, when deserializing the data, I will need to read (1) the username, and then (2) the group, and find the correct id.
Thank you.
You probably can work with a SlugRelatedField [drf-doc]:
from rest_framework import serializers
class MyModelSerializer(serializers.ModelSerializer):
source = serializers.SlugRelatedField(
queryset=User.objects.all(),
slug_field='username',
)
dest = serializers.SlugRelatedField(
queryset=User.objects.all(),
slug_field='username',
)
class Meta:
model = MyModel
fields = ('source', 'dest')
This will return the username of the source and dest field of the model object, and in the opposite direction will fetch the User with the corresponding username.

How to auto populate a read-only serializer field in django rest framework?

I have a question regarding django rest framework.
Most of the time, I have a serializer which has some read-only fields. For example, consider this simple model below:
class PersonalMessage(models.Model):
sender = models.ForeignKey(User, related_name="sent_messages", ...)
recipient = models.ForeignKey(User, related_name="recieved_messages", ...)
text = models.CharField(...)
def __str__(self) -> str:
return f"{self.text} (sender={self.sender})"
In this model, the value of sender and recipient should be automatically provided by the application itself and the user shouldn't be able to edit those fields. Alright, now take a look at this serializer:
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
It perfectly prevents users from setting an arbitrary value on the sender and recipient fields. But the problem is, when these fields are marked as read-only in the serializer, the serializer will completely ignore all the values that are passed into the constructor for these fields. So when I try to create a model, no values would be set for these fields:
PersonalMessageSerializer(data={**request.data, 'sender': ..., 'recipient': ...) # Won't work
What's the best way to prevent users from setting an arbitrary value and at the same time auto-populate those restricted fields in django rest framework?
Depending on how you get those two objects, you can use the serializer's save method to pass them, and they will automatically be applied to the object you are saving:
sender = User.objects.first()
recipient = User.objects.last()
serializer = PersonalMessageSerializer(data=request.data)
message = serializer.save(sender=sender, recipient=recipient)
The kwargs should match the field names in your model for this to work. For reference, have a look here
You able to override the serializer context like this;
PersonalMessageSerializer(data={**request.data, context={'sender': sender, 'recipent': recipent})
and catch the context inside serializer.
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
def validate(self, attrs):
attrs = super().validate(attrs)
attrs['sender'] = self.context['sender']
attrs['recipent'] = self.context['recipent']
return attrs
now serializer.validated_data it must returns sender and recipent.
From the question it is not possible to understand what field(s) of the relationship with sender and recipient you want to interact with, but a general answer can be found in the Serializer relations section of Django REST documentation.
Long story short, if you want to interact with one field only, you can use SlugRelatedField, which lets you interact with the target of the relationship using only one of its fields.
If it just the id, you can use PrimaryKeyRelatedField.
If you want to interact with more than one field, the way to go is Nested Relationships. Here you can specify a custom serializer for the target relationship, but you will have to override the create() method in your PersonalMessageSerializer to create the object from your relationship, as nested serializers are read-only by default.
So this is how you can make set a default on create but read only after in DRF. Although in this solution it wont actually be readonly, it's writable, but you now have explicit control on what the logged in user can write, which is the ultimate goal
Given the model
class PersonalMessage(models.Model):
sender = models.ForeignKey(User,...)
recipient = models.ForeignKey(User,..)
text = models.CharField(...)
You would first create your own custom default (I will show an example for only one field)
# Note DRF already has a CurrentUserDefault you can also use
class CurrentSenderDefault:
requires_context = True
def __call__(self, serializer_field):
return serializer_field.context['request'].user
def __repr__(self):
return '%s()' % self.__class__.__name__
Next you make your own field, that knows whats up with the filter.
This queryset prevents people from setting a value they are not allowed to. which is exactly what you want
class SenderField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context['request'].user
if user:
queryset = User.objects.filter(id=user.id)
else:
queryset = User.objects.none()
return queryset
Finally on the serialiser you go
class PersonalMessageSerializer(serializers.ModelSerializer):
sender = SenderField(default=CurrentSenderDefault())
recipient = ...
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')

How can I set permission for seperate request methods in DRF ModelViewSet?

I'm fairly new to Django and Django Rest Framework and I can't figure out why my code isn't working.
I have a Biz model that has a few fields:
class Biz(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
title = models.CharField(max_length=200)
description = models.TextField()
address = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=100)
phone = PhoneNumberField()
which I serializer using ModelSerializer:
class BizSerializer(serializers.ModelSerializer):
class Meta:
model = Biz
fields = "__all__"
And I use ModelViewSet to have an endpoint for it:
class BizViewSet(viewsets.ModelViewSet):
queryset = Biz.objects.all()
authentication_classes = (authentication.TokenAuthentication,)
permission_classes = [HasGroupPermission]
required_groups = {
"GET": ["__all__"],
"POST": ["member", "biz_post"],
"PUT": ["member", "biz_edit"],
"PATCH": ["member", "biz_edit"],
}
serializer_class = BizSerializer
You probably noticed HasGroupPermission. It is a custom permission I made to confirm the requesting user is in required group(s) the code is:
def is_in_group(user, group_name):
"""
Takes a user and a group name, and returns `True` if the user is in that group.
"""
try:
return Group.objects.get(name=group_name).user_set.filter(id=user.id).exists()
except Group.DoesNotExist:
return None
class HasGroupPermission(permissions.BasePermission):
"""
Ensure user is in required groups.
"""
def has_permission(self, request, view):
# Get a mapping of methods -> required group.
required_groups_mapping = getattr(view, "required_groups", {})
# Determine the required groups for this particular request method.
required_groups = required_groups_mapping.get(request.method, [])
# Return True if the user has all the required groups or is staff.
return all(
[
is_in_group(request.user, group_name)
if group_name != "__all__"
else True
for group_name in required_groups
]
) or (request.user and request.user.is_staff)
However, when I make a GET request, the permission function works like it's supposed to and allows everyone to make the request, and when i make a POST request, the permission function also works perfectly (if user isn't in both "member" and "biz_post" groups the request is denied).
The problem arises when I try other methods such as PUT, PATCH, and DELETE. Why is this issue happening? Half the methods work and the other half (sorta) don't. My knowledge in DRF is limited at the moment, and I can't seem to solve the issue.
I realized my problem which I found very silly. My BizViewSet is actually a ViewSet and I didn't realize that I have to make PATCH, PUT, and DELETE requests to the object link (as in localhost:8000/api/biz/$id). Since my User serializer isn't a ViewSet I thought the patch method works the same way which was I pass a primary key in JSON along with the data I wanted to patch but ViewSets are different and I didn't know that. Silly.
Hi you can use DjangoModelPermissions instead of HasGroupPermission
(at the first you must import it)
from rest_framework.permissions import DjangoModelPermissions
This permission check that user have permission for PUT, POST and DELETE
All user have GET permission
You must set permission for user in admin or set permission for group of user
I hope it helps you
has_permission method not provide object-level permission and PUT and PATCH need object-level permission.
You must create object-level permissions, that are only run against operations that affect a particular object instance by using has_object_permission method of permissions.BasePermission class.
See this link.
Hope it helped.

How to serialize Inherited models in Django REST Framework

I'm working on a Django Rest Framework project, in which I have created the following models as:
from django.db import models
# Base Models...
choices = (
('Single', 'Single'),
('Multiple', 'Multiple'),
)
class UserAccountModel(models.Model):
deployment_name = models.CharField(max_length=150, blank=True)
credentials = models.FileField(upload_to='media/credentials/', name='credentials'),
project_name = models.CharField(max_length=150, blank=True)
project_id = models.CharField(max_length=100, blank=False, name='project_id')
cluster_name = models.CharField(max_length=150, blank=False)
zone_region = models.CharField(max_length=150, blank=False)
services = models.CharField(max_length=100, choices=choices)
def __str__(self):
return self.deployment_name
class AwdModel(UserAccountModel):
source_zip = models.FileField(upload_to='media/awdSource/', name='awd_source')
routing = models.TextField(name='routing', null=True)
def __str__(self):
return self.deployment_name
def save(self, **kwargs):
if not self.id and self.services == 'Multiple' and not self.routing:
raise ValidationError("You must have to provide routing for multiple services deployment.")
super().save(**kwargs)
# def clean(self):
# if self.services == 'Multiple' and self.routing is None:
# raise ValidationError('You must have to provide routing for multiple services deployment.')
class AwodModel(UserAccountModel):
source_zip = models.FileField(upload_to='media/awodSource/', name='awod_source')
routing = models.TextField({'type': 'textarea'}, name='routing')
def save(self, **kwargs):
if not self.id and self.services == 'Multiple' and not self.routing:
raise ValidationError("You must have to provide routing for multiple services deployment.")
super().save(**kwargs)
I need to serialize these models, Here's how I have implemented serializers for these models:
from rest_framework import serializers
from .models import UserAccountModel, AwdModel, AwodModel
class UserAccountSerializer(serializers.ModelSerializer):
class Meta:
model = UserAccountModel
fields = ('deployment_name', 'credentials', 'project_name',
'project_id', 'cluster_name', 'zone_region', 'services')
class AWDSerializer(serializers.ModelSerializer):
class Meta(UserAccountSerializer.Meta):
model = AwdModel
fields = UserAccountSerializer.Meta.fields + ('awd_source', 'routing',)
class AWODSerializer(serializers.ModelSerializer):
class Meta:
model = AwodModel
fields = '__all__'
But, when I try to access, AWDSerialzer it return an error as:
AttributeError at /api/v1/deployments/
Got AttributeError when attempting to get a value for field project_id on serializer AWDSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the QuerySet instance.
Original exception text was: 'QuerySet' object has no attribute 'project_id'.
Update: Here's my APIView code:
class DeploymentsList(APIView):
def get(self, request):
MAX_OBJECTS = int(20)
deployments = AwdModel.objects.all()[:MAX_OBJECTS]
data = AWDSerializer(deployments).data
return Response(data)
class DeploymentDetail(APIView):
def get(self, request, *args, **kwargs):
deployment = get_object_or_404(AwdModel, pk=kwargs['pk'])
data = AWDSerializer(deployment).data
return Response(data)
Help me, please!
Thanks in advance!
AttributeError at /api/v1/deployments/ Got AttributeError when
attempting to get a value for field project_id on serializer
AWDSerializer. The serializer field might be named incorrectly and not
match any attribute or key on the QuerySet instance. Original
exception text was: 'QuerySet' object has no attribute 'project_id'.
This is an attribute error, when attempting to get the value from field project_id .
Get rid of the name attribute in the project_id field.
Edit The APIView code
To serialize a queryset or list of objects instead of a single object
instance, you should pass the many=True flag when instantiating the
serializer. You can then pass a queryset or list of objects to be
serialized. [Serializing multiple objects]
class DeploymentsList(APIView):
def get(self, request):
MAX_OBJECTS = int(20)
deployments = AwdModel.objects.all()[:MAX_OBJECTS]
data = AWDSerializer(deployments, many=True).data
return Response(data)
I hope this will help.
The code that you posted appears to be valid and correct. The issue however is unrelated. The exception text 'QuerySet' object has no attribute 'project_id' Refers to an issue that likely originates from your restframework app's views.py file. The exception states that you are attempting to access the attribute 'project_id' from a QuerySet.
A QuerySet is a (lazy loaded) set of models and not a single model. Even if the query set had only one element you'd still be required to access that element before accessing it's attributes.
Because you haven't shared your views.py file I can't say for sure where the issue is however here is an incorrect use case example: MyModel.objects.all().project_id. Here we can see that I am attempting to access the attribute project_id from a query set. A correct use case would be MyModel.objects.all()[0].project_id. However this assumes that the query set is not empty.
Practically, most DjangoRestFramework views inherit from rest_framework.views.APIView which subclasses django's View Class. I would suggest checking the query_set within that class is being used correctly.
Feel free to share your implementation here for further comment.
[EDIT] - After views.py coded was added.
You are attempting to serializer an entire query set with the instantiation of a serializer data = AWDSerializer(deployments).data this is causing the attribute error.
I would recommend the generics.ListAPIView class and the use of the class attributes query_set and serializer_class. These are simple to implement. You can then invoke the APIViews default get method. Here is an example for your DeploymentsList view
from rest_framework import generics
class DeploymentsList(generics.ListAPIView):
serializer_class = AWDSerializer
queryset = AwdModel.objects.all()
def get(self, request, *args, **kwargs):
MAX_OBJECTS = int(20)
self.queryset = self.queryset[:MAX_OBJECTS]
return super(DeploymentsList, self).get(request, *args, **kwargs)
[EDIT] - FileField Serialization
In order to serialize the UserAccount.credentials file field so that we serializer the path, we can use the serializers.SerializerMethodField. I.e Your UserAccountSerializer becomes:
class UserAccountSerializer(serializers.ModelSerializer):
credentials = serializers.SerializerMethodField()
def get_credentials(self, user_account):
return user_account.credentials.path
class Meta:
model = UserAccountModel
fields = ('deployment_name', 'credentials', 'project_name',
'project_id', 'cluster_name', 'zone_region', 'services')
When you inherit from a model class which is not defined as abstract in it’s own meta class, then Django creates a one-to-one relation between the subclass and its parent. Which actually creates two tables in the database; one for the base class and one for the subclass.
I haven’t tried your code, nor used Django 2, but would check using a relational field between the two serializer.

Save a many-to-many model in Django/REST?

I'm writing a REST API for my Django app, and can't get POST requests to work on one model.
Here's the model in question:
class ProjectNode(models.Model):
name = models.CharField(max_length=60)
place = models.CharField(max_length=150)
time_spent = models.BigIntegerField()
parent_project = models.ForeignKey(Project, related_name='tasks')
workers = models.ManyToManyField(User, related_name='tasks_can_do')
def __str__(self):
return self.name
The User model just holds a name field at the moment.
Here's my serializer for ProjectNode:
class ProjectNodeSerializer(serializers.ModelSerializer):
class Meta:
model = ProjectNode
fields = ('id', 'name', 'place', 'time_spent', 'workers',)
And here's the API view (from views.py):
class WebProjectNodeListView(generics.ListCreateAPIView):
queryset = ProjectNode.objects.all()
serializer_class = ProjectNodeSerializer
def pre_save(self, obj):
obj.parent_project = Project.objects.get(pk=self.request.DATA['parent_project'])
for worker_pk in self.request.DATA['workers']:
obj.workers.add(User.objects.get(pk=worker_pk))
obj.final_worker = User.objects.get(pk=self.request.DATA['final_workers'])
I tried a simpler version yesterday at first, which only had the Project ForeignKey relationship, and it seemed to work, so I thought that using add would work too, but I get an error when testing out the API with httpie (I already added some users and projects, and am sure I get their id's correctly).
Here's the request:
http POST :8000/api/tasks/ name="newtask" place="home" time_spent:=50 parent_project:=1 workers:=[1]
And I get this error:
"<ProjectNode: newtask>" needs to have a value for field "projectnode" before this many-to-many relationship can be used.
And the traceback also points to this line of code:
obj.workers.add(User.objects.get(id=worker_pk))
Now, I get the feeling that this is because I'm trying to update the relationship on the User object before a ProjectNode object is created in the database, but I'm not sure how to resolve this?
DRF doesn't works create models which are nested serializers objects or Many to Many fields.
So is necessary to override Serializer create method and create/get M2M models before create ProjectNode.
Try to override create(self, validated_data) in your serializer and work with your data inside this method..
Example:
My model Project has M2M relation with ProjectImages. In ProjectSerializer I override create method like this.
def create(self, validated_data):
try:
# Remove nested and M2m relationships from validated_data
images = validated_data.pop('projectimage_set') if 'projectimage_set' in validated_data else []
# Create project model
instance = Project(**validated_data)
if status:
instance.set_status(status)
project = instance.save()
# Create relations
for image in images:
ProjectImage.objects.create(project=project, **image)
except exceptions.ValidationError as e:
errors_messages = e.error_dict if hasattr(e, 'error_dict') else e.error_list
raise serializers.ValidationError(errors_messages)
return project
Hope this help!

Categories

Resources