How do I serialize indirect relationships with querysets? - django-rest-framework

I have built a Django REST application to serve as backend API For an iOS project. In my object model I use 'Subscription' to join 'User' objects with 'Workspace' objects. Here's a part of my models.py simplified:
class User(models.Model):
# some property fields
class Workspace(models.Model):
# some property fields
class Subscription(models.Model):
# some property fields
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='subscriptions')
workspace = models.ForeignKey(
Workspace,
on_delete=models.CASCADE,
related_name='subscriptions')
I have built class-based views for the objects so I can get a list of workspace objects with http GET from my iOS front end. For convenience reasons I want to include more than just the model fields, for example in the list of workspaces i want to include a list of subscribed users for every workspace object. I was advised to use SerializerMethodField() and querysets for serializing the field, but I don't know how to construct the queries. I've got this far:
class WorkspaceSerializer(serializers.ModelSerializer):
subscribed_users = serializers.SerializerMethodField()
class Meta:
model = Workspace
fields = ('id', 'subscribed_users')
def get_users(self, workspace):
users = User.objects.filter(???)
serializer = UserSerializer(instance=users, many=True)
return serializer.data
Getting subscriptions related to the workspace is easy because they're directly related, but how do I get users that are subscribed to the workspace in question?

The syntax I was looking for was double underscore, called spanning in DRF. For example:
def get_users(self, workspace):
users = User.objects.filter(subscription_set__workspace=workspace)
serializer = UserSerializer(instance=users, many=True)
return serializer.data

Related

Serialize available choices and mark selected

I am new to REST and django-rest-framework. I want to get list of available ManyToMany choices along with some way to know which ones are currently selected.
I have model like this:
class PGroup(models.Model):
.
permissions = models.ManyToManyField(
Permission, related_name="group_permissions", help_text=_('Select permissions for this group.')
)
Serializers.
class PermissionSerializer(serializers.ModelSerializer):
class Meta:
model = Permission
fields = ['pk', 'name',]
class PGroupSerializer(serializers.ModelSerializer):
permissions = PermissionSerializer(many=True)
class Meta:
model = PGroup
fields = [....'permissions']
Looking at Browseable API, with this setup I get 'permissions: []'(empty list) for generics.createAPIView and get the associated 'permissions[....]'(non-empty list) for generics.RetrieveUpdateAPIView.
I want a list of available permissions on both API views and also want to know which permissions are already selected for Update API view.
Can anyone please help.
Thanks
There are 2 ways to get the list of choices.
Using the SerializerMethodField,
from rest_framework import serializers
from .models import Permission
class PGroupSerializer(serializers.ModelSerializer):
permissions = PermissionSerializer(many=True)
all_available_permissions = serializers.SerializerMethodField()
def get_all_available_permissions(self, obj):
return Permission.objects.all()
class Meta:
model = PGroup
fields = ['permissions', "all_available_permissions"]
or using source, we can define a custom method on the model and point the serializer to use it using the source argument.
### models.py
class PGroup(models.Model):
.
permissions = models.ManyToManyField(
Permission, related_name="group_permissions", help_text=_('Select permissions for this group.')
)
def all_permissions(self):
return Permission.objects.all()
### serializers.py
class PGroupSerializer(serializers.ModelSerializer):
permissions = PermissionSerializer(many=True)
all_available_permissions = PermissionSerializer(many=True, read_only=True, source="all_permissions")
class Meta:
model = PGroup
fields = ['permissions', "all_available_permissions"]
2nd option is much better, IMO.
Note: you may not always want to send a full list of choices as that could get really slow overtime when u have hundreds or thousands of objects.

Manipulate data on serializer before creating model instance in DangoRestFramework

I have three related models as such
Order model
class Order(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
Name = models.CharField(max_length=250)
orderType = models.ForeignKey(OrderType, on_delete=models.CASCADE, null=True)
class Meta:
ordering = ['id']
def __str__(self):
return '{}'.format(self.Name)enter code here
OrderPricing Model
class OrderPricing(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
TotalPrice = models.DecimalField(decimal_places=2, max_digits=10)
#related field
order = models.ForeignKey(Order, on_delete=models.CASCADE, null=True)
class Meta:
ordering = ['order']
def __str__(self):
return self.TotalPrice
OrderType Model
class OrderType(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
Name = models.CharField(max_length = 100)
Premium = models.BooleanField()
class Meta:
ordering = ['id']
Let's ignore the order in which the models appear above.
I have three SerializerModels for each model.
I can crud each model on the BrowsableAPI
Q1:
From the browsableAPI I can create an Order.
I haven't gotten to the 'Writable Nested Serializer' yet and I believe Django has that figured out in their docs through the drf-writable-nested class.
I have two orderTypes
1 = {'Not Premium':'False'} #not Premium
2 = {'Premium':'True'} #Premium
Assume I have a variable order_price = 5 #£5
How can I
Create an order,
If order is premium, then set order_price to 10 #order_price * 2
If order is NOT premium, then set order_price to 5
Create an instance of OrderPricing, that's related to the order. Also, pass the order_price variable to the property TotalPrice when creating the instance
from what I have seen and tried, I can override the Create() on the serializer as such
class OrderSerializer(WritableNestedModelSerializer):
"""OrderSerializer"""
# orderPricing = OrderPricingSerializer(required=False)
class Meta:
model = Order
fields = ('__all__')
def create(self, validated_data):
#create instance of order
#determine of order is premium
typeid = uuid.UUID(validated_data.pop('orderType'))#get FK value
isPremium = OrderType.objects.get(id = str(typeid.id))#determine if **Premium** is True/False
# set/calculate the price of the order
#create a related instance of OrderPricing
Q2
I am aware of GenericViews and the CreateModelMixin, what I don't know is, which is better, overriding the .create() at the serializer or overriding the CreateModelMixin method at the GenericView
Well, where to put business logic is always question hard to answer.
You have multiple places where it can be - view, serializer, model or some other separate module/service.
All have pros and cons- you can find many articles on this topic.
But in your case, I would probably go with perform_create of your view and I would create a method in the serializer which would update the price. If I needed to use the code to update price, I'd move to separate shared module and call it from there.
So let's say you use CreateModelMixin or better ListCreateAPIView
class YourView(ListCreateAPIView):
serializer = OrderSerializer
queryset = your_queryset
def perform_create(self, serializer):
serializer.update_price()
serializer.save()
perform_create is called after data is validated, so you can access the validated data.
update_price is your code where you update the price.
You can argue to move this logic to serializer's create or save method but they do many other things, so unless you need to override these methods for other reasons - you can take advantage of the perform_create method.

Writable many-to-many field in Django Rest Framework tries to save new child objects?

Edit: This is using django rest framework 2.3
I have a model structure that has 3 relationship "levels", one of which is many to many.
class Shipment(models.Model):
stuff...
class ShipmentItem(models.Model):
shipment = models.ForeignKey(Shipment)
assets = models.ManyToMany(ShipmentAsset)
class ShipmentAsset(models.Model)
serial_number = models.CharField(unique=True)
Using Django rest framework I want to be able to post to the "Shipment" endpoint with a payload that contains the ShipmentItems for the Shipment, and the ShipmentAssets for the ShipmentItems ideally in one request.
The serializers are as follows..
class ShipmentAssetSerializer(serializers.ModelSerializer):
class Meta:
model = ShipmentAsset
field = ('id', 'serial_number', )
class ShipmentItemSerializer(serializers.ModelSerializer):
assets = ShipmentAssetSerializer(
many=True, required=False, allow_add_remove=True,
)
class Meta:
model = ShipmentItem
fields = ('id', 'assets', )
class ShipmentSerializer(serializers.ModelSerializer):
class Meta:
model = Shipment
fields = (
'id',
)
The shipmentItem/Shipment relationship seems to work when I post to it with the assets part disabled, but when I try to post assets in the payload, It appears to be trying to create NEW assets with the posted data (I get an error regarding the unique constraint on the serial number) rather than creating a new many-to-many table object. Any idea what I'm doing wrong?
Edit: Important clarification, I'm using Django Rest Framework 2.3.13

Django REST framework restrict posting and browsable Api-fields

I use the Django Rest framework together with an JavaScript app. I have some difficulties to get the posting of new data items right with the generic ModelViewSet
Most importantly I want to restrict what a poster can submit
(they should only be allowed to post items that have the user_id of this user (the authenticated user of the session).
I don't know when/where I should check for this? Is this a validation problem?
How I understand the permission classes is that they restrict the method (Post/Get) or check for user groups.
Also my user field in the item model is a foreign key to the user model
so the browsable api suggest in the Html-form a dropdown with the information about other users. (their email adresses and some other fields).
My data items look like this
[{
"id": 792,
"name": "test",
"category": 1,
"value": 5,
"user": "33"
}]
Here is my Serializer and the Viewset:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ('id',
'name',
'category',
'value',
'user',
)
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
I can't believe this issue with the DRF Create/Update (Post/Put) form isn't more widely discussed.
It's a huge data privacy issue - e.g. One can restrict the List API view to only show items owned by a User via overriding the get_queryset method inside as below:
# views.py
class ItemViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
But as OP notes, when accessing the API Create/Post or Update/Put form for the ItemViewSet, there is seemingly no easy way to restrict the user options to the user itself.
I had a similar issue myself building a survey platform, where I want to restrict choice of survey/question/options etc. to those owned by the user, and prevent users from inadvertently seeing each other's data.
Jocelyn's answer works for the OP's particular situation where we already know that the Item.user must equal request.user, so we override this on the perform_create method.
But Jocelyn's solution is insufficient for situations where you do not know in advance what the relationship between model instances will be (e.g. in my case where a new question objected could be added to any one of a user's surveys).
The solution I came up with was the nuclear option: do away with the Viewset altogether for Create and Update functionality, and use a set of custom views.APIView classes instead, as below (adapted for the case of the OP, only showing Create).
class ItemCreateView(views.APIView):
def post(self, request, format=None):
post_user_id = int(request.data['user'].split('/')[-2])
request_user_id = request.user.id
serializer = ItemSerializer(data=request.data, context={'request': request})
if post_user_id == request_user_id:
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response('Not Allowed: Owner is not User', status=status.HTTP_401_UNAUTHORIZED)
Please note, I'm using a HyperlinkedModelSerializer rather than a plain ModelSerializer, hence the need for .split('/')[-2] to grab the post_user_id
Handling the user field
First set the user field to be readonly:
# serializers.py
class ItemSerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField()
class Meta:
model = Item
fields = ('id',
'name',
'category',
'value',
'user',
)
Then auto-set the user id on creation:
# views.py
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user.customer)
Handling permissions
Just use standard permissions mechanism to define a custom one :
# permissions.py
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return (request.user.is_authenticated() and
(obj.user == request.user.customer))
...and use it in your viewset :
# views.py
from permissions import IsOwner
class ItemViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwner]
...

Model Serializer : choose which fields to display and add custom fields

Let's say I have this simple model :
class BlogPost(models.Model):
author = models.ForeignKey(MyUser)
body = models.TextField()
title = models.CharField(max_length=64)
urlid = models.CharField(max_length=32)
private_data = models.CharField(max_length=64)
private_data contains data that I do not want to expose to the API (!). I'm using a ModelSerializer :
class BlogPostSerializer(serializers.ModelSerializer):
class Meta:
model = BlogPost
def __init__(self, *args, **kwargs):
# Don't pass the 'request' arg up to the superclass
request = kwargs.pop('request', None)
# Instatiate the superclass normally
super(ModelSerializer, self).__init__(*args, **kwargs)
self.request = request
def absolute_url(self, blogpost):
return blogpost.get_absolute_url(self.request)
The absolute_url method needs the request to determine the domain name (dev or prod for example) and if it was made in http or https.
I want to specify which fields in the model are going to get returned by the serializer (not expose private_data for example). Simple enough:
class BlogPostSerializer(serializers.ModelSerializer):
class Meta:
model = BlogPost
fields = ('author', 'body', 'title', 'urlid',)
# The same jazz after that
All right, it works. Now I also want to return absoluteUrl:
class BlogPostSerializer(serializers.ModelSerializer):
absoluteUrl = serializers.SerializerMethodField('absolute_url')
class Meta:
model = BlogPost
fields = ('author', 'body', 'title', 'urlid',)
# The same jazz after that
Well, without surprises, this returns only the fields I specified, without the absoluteUrl. How can I return only certain fields of the model AND the absoluteUrl, calculated from the serializer?
If I don't specify fields I do get the absoluteUrl, but with all the model's fields (including private_data). If I add 'absoluteUrl' to fields I get an error because blogpost.absoluteUrl doesn't exist (no surprises there). I don't think I could use this method http://django-rest-framework.org/api-guide/serializers.html#specifying-fields-explicitly because I need the request to obtain the absoluteUrl (or can I specify arguments to the model's method ?)
If I don't specify fields I do get the absoluteUrl, but with all the model's fields (including private_data). If I add 'absoluteUrl' to fields I get an error because blogpost.absoluteUrl doesn't exist (no surprises there).
You should just be adding 'absoluteUrl' to the fields tuple, and it should work just fine - so what error are you seeing?
The absolute_url method needs the request to determine the domain name (dev or prod for example) and if it was made in http or https.
Note that you can also pass through context to the serializer without modfiying the __init__, just pass a context={'request': request} when instantiating the serializer. The default set of generic views do this for you, so you can access self.context['request'] in any of the serializer methods. (Note that this is how hyperlinked relationships are able to return fully qualified URLs)

Resources