Integration of custom methods in browsable api - python

I'm creating an application that has Item and Customer objects. The Customer has a property watchList which is a list of Item.
Now I want to create a REST api for these watch lists. It should list all items in the watch list of the current customer and offer a method to add (already existing) items to the list.
class WatchListViewSet(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
serializer_class = ItemSerializer
def get_queryset(self):
return Customer.objects.get(user = self.request.user).watchList
def list(self, request):
queryset = Customer.objects.get(user = self.request.user).watchList
serializer = ItemSerializer(queryset, context={'request': request}, many=True)
return Response(serializer.data)
#list_route(methods=['POST'])
def add(self, request, *args, **kwargs):
#request.data.id contains the id of the item that should be added
# ...
return Response(status=status.HTTP_201_CREATED)
However, when I request localhost:800/api/watchList/add/, I see a form for an item not an input for the id of an existing item (or even better, a dropdown/selection field).
How can I inform the browsable api that the requested input type differs from the rest of the view set? Can this be connected to some kind of automatic validation (the method won't be executed if no id is passed)?

It turned out this solves my problem:
class ItemIdSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.IntegerField()
class Meta:
model = Item
fields = ('id',)
and
class WatchListViewSet(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
def get_serializer(self):
if self.action == WatchListViewSet.add.__name__:
return ItemIdSerializer()
return super(WatchListViewSet, self).get_serializer()
The method get_serializer returns the special ItemIdSerializer if the add action is executed.
By defining the attribute id as serializers.IntegerField() the default behaviour of hiding the read-only attribute id is overwritten.
However, this approach doesn't provide any automatic verification, it's still possible to execute the add method without providing an id.

Related

Return existing record rather than creating Rest API framework

In my API, I have a create view tied that references another record OneToOne. However, occasionally it seems that users send through two requests at once and the second fails due to a duplicate record clash:
class CreateProfileLink(generics.CreateAPIView):
def perform_create(self, serializer):
ins = serializer.save(user=self.request.user)
serializer_class = ProfileLinkSerializer
Is there a way I could override the create method to return the record if it already exists rather than creating it?
You could use get_or_create in your serializer class, by overriding its create() method:
class ProfileLinkSerializer(serializers.ModelSerializer):
...
class Meta:
model = Profile
fields = (...)
def create(self, validated_data):
profile, _ = Profile.objects.get_or_create(**validated_data)
return profile
Since you haven't provided your models.py, I am using Profile as a model name here. Make sure to replace it if it is different in your project.

How to create foreign key related objects during object create call with the Django Rest Framework?

Let's say I have model A and B.
Model A has a range of fields including a user field which is filled in with the user who creates an object A.
When object A is created I want to create a bunch of object B's using the user that created object A and possible using object A itself as a foreign key.
My Model A ViewSet looks like this:
class OrganizationViewSet(DynamicModelViewSet):
queryset = Organization.objects.all()
serializer_class = OrganizationSerializer
permission_classes = (IsAdminOfPermission,)
So when an organization is created I want to create a bunch of object B's using the user that created the organization and possibly using the organization object itself.
How do I do this?
Implement the logic in the Serializer create method
class OrganizationSerializer(serializers.ModelSerializer):
# ...
# ...
# ...
def create(self, validated_data):
user = validated_data.get('user')
organization = Organization.objects.create(**validated_data)
# create model B with `user` and `organization`
# like ModelB.objects.create(user=user, organization=organization, ...)
return organization
To pass the user from view to the serializer, you have to send it through the save method of the serializer.
for example
class OrganizationList(APIView):
queryset = Organization.objects.all()
serializer_class = OrganizationSerializer
permission_classes = (IsAdminOfPermission,)
def post(self, request, format=None):
serializer = OrganizationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(user=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
This might not be the most elegant approach... but here's how I'd do it:
class DemoHandler(APIView):
authentication_classes = (CsrfExemptSessionAuthentication,)
permission_classes = (IsAuthenticated,)
def post(self, request, format=None):
m = ModelA(..., user=request.user, ...)
m.save()
m2 = ModelB( ... )
...
In other words, I'd just manually define with APIView vice a ModelViewset. Hopefully somebody has a better approach.

django rest framework: Get url parameter value in a model property

I have a Product model and one propery in it is "my_test_fn". This is called from my serializer. My requirement is, I want to do some calculations based on the filter passing through the url. How can I get the url parameter values in a model property?
I want to get "filters" value in my_test_fn
models.py
class Product(AbstractProduct):
product_id = models.UUIDField(default=uuid.uuid4, editable=False)
##more fields to go
def my_test_fn(self):
filters = self.request.query_params.get('filters', None)
return {"key":"value"}
serializer.py
class MySerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('id','product_id','sku', 'title', 'my_test_fn',)
views.py
class ProductDetailConfiguration(viewsets.ViewSet):
lookup_field = 'product_id'
def retrieve(self, request, product_id=None):
queryset = Product.objects.filter(product_id=product_id)[0]
serializer = ProductConfigurationCustomSerializer(queryset, context={'request': request})
return Response(serializer.data)
API url:
http://127.0.0.1:8000/api/v1/product-configuration/2FC2AA43-07F5-DCF4-9A74-C840FDD8280A?filters=5
This logic belongs in the serializer, not the model. You can access it there via self.context['request'].
I guess what you want is not possible (have the my_fn on the model itself).
You would need to use a SerializerMethodField, so you will have access to the object, but to the request (and the various parameters of it) as well.

Django Rest Framework ModelSerializer Set attribute on create

When creating an object initially I use the currently logged-in user to assign the model field 'owner'.
The model:
class Account(models.Model):
id = models.AutoField(primary_key=True)
owner = models.ForeignKey(User)
name = models.CharField(max_length=32, unique=True)
description = models.CharField(max_length=250, blank=True)
Serializer to set owner:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = models.Account
fields = ('name', 'description')
def restore_object(self, attrs, instance=None):
instance = super().restore_object(attrs, instance)
request = self.context.get('request', None)
setattr(instance, 'owner', request.user)
return instance
It is possible for a different user in my system to update another's Account object, but the ownership should remain with the original user. Obviously the above breaks this as the ownership would get overwritten upon update with the currently logged in user.
So I've updated it like this:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = models.Account
fields = ('name', 'description')
def restore_object(self, attrs, instance=None):
new_instance = False
if not instance:
new_instance = True
instance = super().restore_object(attrs, instance)
# Only set the owner if this is a new instance
if new_instance:
request = self.context.get('request', None)
setattr(instance, 'owner', request.user)
return instance
Is this the recommended way to do something like this? I can't see any other way, but I have very limited experience so far.
Thanks
From reviewing #zaphod100.10's answer. Alternatively, in the view code (with custom restore_object method in above serializer removed):
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
if serializer.is_valid():
serializer.object.owner = request.user
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Basically you want the owner to be set on creation and not on subsequent updates. For this I think you should set the owner in the POST view. I think it is more logical and robust that way. Update is done via PUT view so your data should always be correct since no way on updation the owner can be changed if the owner is not editable on PUT.
For making the views you can use DRF's generic class based views. Use the RetrieveUpdateDeleteView as it is. For ListCreateView override the post method. Use a django model form for validating the data and creating an account instance.
You will have to copy the request.DATA dict and insert 'owner' as the current user.
The code for the POST method can be:
def post(self, request, *args, **kwargs):
data = deepcopy(request.DATA)
data['owner'] = request.user
form = AccountForm(data=data)
if form.is_valid():
instance = form.save(commit=false)
instance.save()
return Response(dict(id=instance.pk), status=status.HTTP_201_CREATED)
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
Potential other option using pre_save which I think seems to be intended for just this kind of thing.
class AccountList(generics.ListCreateAPIView):
serializer_class = serializers.AccountSerializer
permission_classes = (permissions.IsAuthenticated)
def get_queryset(self):
"""
This view should return a list of all the accounts
for the currently authenticated user.
"""
user = self.request.user
return models.Account.objects.filter(owner=user)
def pre_save(self, obj):
"""
Set the owner of the object to the currently logged in user as this
field is not populated by the serializer as the user can not set it
"""
# Throw a 404 error if there is no authenticated user to use although
# in my case this is assured more properly by the permission_class
# specified above, but this could be any criteria.
if not self.request.user.is_authenticated():
raise Http404()
# In the case of ListCreateAPIView this is not necessary, but
# if doing this on RetrieveUpdateDestroyAPIView then this may
# be an update, but if it doesn't exist will be a create. In the
# case of the update, we don't wish to overwrite the owner.
# obj.owner will not exist so the way to test if the owner is
# already assigned for a ForeignKey relation is to check for
# the owner_id attribute
if not obj.owner_id:
setattr(obj, 'owner', self.request.user)
I think this is the purpose of pre_save and it is quite concise.
Responsibilities should be split here, as the serializer/view only receives/clean the data and make sure all the needed data is provided, then it should be the model responsibility to set the owner field accordingly. It's important to separate these two goals as the model might be updated from elsewhere (like from an admin form).
views.py
class AccountCreateView(generics.CreateAPIView):
serializer_class = serializers.AccountSerializer
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, *args, **kwargs):
# only need this
request.data['owner'] = request.user.id
return super(AccountCreateView, self).post(request, *args, **kwargs)
models.py
class Account(models.Model):
# The id field is provided by django models.
# id = models.AutoField(primary_key=True)
# you may want to name the reverse relation with 'related_name' param.
owner = models.ForeignKey(User, related_name='accounts')
name = models.CharField(max_length=32, unique=True)
description = models.CharField(max_length=250, blank=True)
def save(self, *args, **kwargs):
if not self.id:
# only triggers on creation
super(Account, self).save(*args, **kwargs)
# when updating, remove the "owner" field from the list
super(Account, self).save(update_fields=['name', 'description'], *args, **kwargs)

Returning id value after object creation with django-rest-framework

I am using django-rest-framework generic views to create objects in a model via POST request. I would like to know how can I return the id of the object created after the POST or more general, any additional information about the created object.
This is the view class that creates (and lists) the object:
class DetectorAPIList(generics.ListCreateAPIView):
serializer_class = DetectorSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
parser_classes = (MultiPartParser, FileUploadParser,)
def pre_save(self, obj):
obj.created_by = self.request.user.get_profile()
def get_queryset(self):
return (Detector.objects
.filter(get_allowed_detectors(self.request.user))
.order_by('-created_at'))
The model serializer:
class DetectorSerializer(serializers.ModelSerializer):
class Meta:
model = Detector
fields = ('id', 'name', 'object_class',
'created_by', 'public', 'average_image', 'hash_value')
exclude = ('created_by',)
Thanks!
Here, DetectorSerializer inherits from ModelSerializer as well as your view inherits from generics ListCreateAPIView so when a POST request is made to the view, it should return the id as well as all the attributes defined in the fields of the Serializer.
Because it took me a few minutes to parse this answer when I had the same problem, I thought I'd summarize for posterity:
The generic view ListCreateApiView does return the created object.
This is also clear from the documentation listcreateapiview: the view extends createmodelmixin, which states:
If an object is created this returns a 201 Created response, with a serialized representation of the object as the body of the response.
So if you have this problem take a closer look at your client side!
post$.pipe(tap(res => console.log(res)))
should print the newly created object (assuming rxjs6 and ES6 syntax)
As mentioned above, To retrieve the id for the new created object, We need to override the post method, find the the update code for more details:
class DetectorAPIList(generics.ListCreateAPIView):
serializer_class = DetectorSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
parser_classes = (MultiPartParser, FileUploadParser,)
def post(self, request, format=None):
serializer = DetectorSerializer(data=request.data)
if serializer.is_valid():
obj = serializer.save()
return Response(obj.id, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Categories

Resources