Is it possible to use the library 'requests' (HTTP library for python) to post and update nested objects in django rest framework?
I made a new create method in serializers, but I can't post outside the shell, nor with the requests library or in the api webview.
My Serializers:
class QualityParameterSerializer(serializers.ModelSerializer):
class Meta:
model = QualityParameter
fields = ("id","name", "value")
class ProductQualityMonitorSerializer(serializers.ModelSerializer):
parameters = QualityParameterSerializer(many=True)
class Meta:
model = ProductQualityMonitor
fields = ("id","product_name", "area", "timeslot", "processing_line",
"updated_on",'parameters')
def create(self, validated_data):
params_data = validated_data.pop('parameters')
product = ProductQualityMonitor.objects.create(**validated_data)
for param_data in params_data:
QualityParameter.objects.create(product=product, **param_data)
return product
POST HTTP request
If I may suggest the following form for your serializer:
from django.db import transaction
class ProductQualityMonitorSerializer(serializers.ModelSerializer):
parameters = QualityParameterSerializer(many=True)
class Meta:
model = ProductQualityMonitor
fields = (
"id",
"updated_on",
"product_name",
"area",
"timeslot",
"processing_line",
"parameters",
)
def create(self, validated_data):
# we will use transactions, so that if one of the Paramater objects isn't valid
# that we will rollback even the parent ProductQualityMonitor object creation,
# leaving no dangling objects in the database
params_data = validated_data.pop('parameters')
with transaction.atomic():
product = ProductQualityMonitor.objects.create(**validated_data)
# you can create the objects in a batch, hitting the dB only once
params = [QualityParameter(product=product, **param) for param in params_data]
QualityParameter.objects.bulk_create(params)
return product
About using python requests library: you will have to pay attention to the following aspects when posting to a django back-end:
you must provide a valid CSRF token in your request; the way this is done is via csrf-token cookie;
you must provide the proper authentication headers / tokens / cookies; this is your choice, depends how you're implementing this on the DRF back-end
if this is a request from one domain to another domain, then you have to care for CORS setup.
More to the point: what have you tried already and didn't worked ?
Related
When a Django Rest Framework Site is hosted, can authenticated users make direct request to the api .. or does CORS prevent them?
CORS states that every request that is not made from from the same origin will be blocked
provided that it is not in the "allowed hosts". (it concerns requests made from other domains and can be overcome by making the request on the server-side)
CORS will not handle requests made individually by someone if they are from their machine directly
It depends on what you mean by
can authenticated users make direct request to the api
You can always prevent someone from accessing something in the view
from django.http import HttpResponseForbidden
# But this will block them no matter wheter thy are accessing directly or not
if request.user.is_authenticated:
return HttpResponseForbidden()
But in short, yes everyone can make a request to the API directly and there is nothing wrong about it really.
EDIT : What you were really asking had nothing to do withs CORS but with DRM
What you want to do is use custom methods for certain types of requests e.g GET or POST Or more specifically have users only access specific information.
Here is a very simple way to do that
views.py (Writting your fully custom views)
#Mixins allow you to access only specific Methods like create,delete,list etc...
# mixins.CreateModelMixin and mixins.ListModelMixin will almost do for everything
from rest_framework import mixins, viewsets
#Take here as an example an API endpoint for creating users
class userCreation(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet):
serializer_class = UserSerializer #Here the serializers does not event matter you can choose something random
queryset = User.objects.all()
def create(self, request, *args, **kwargs):
#Here you handle user creations
#Access POST data, validations etc
Or if you don't want to write everything by your self
Serializers.py (Just limiting the fields)
#Here is an example where you only want to check if a username or an email exists
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username','email']
views.py
class UserUsernameView(mixins.ListModelMixin,viewsets.GenericViewSet):
serializer_class = UserSerializer
queryset = User.objects.all()
I created a Django REST API using serializers, viewsets and routers. My end points looks something like this:
http://www.website.com/api/items
http://www.website.com/api/items/available
serializer.py (omitting imports)
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = '__all__'
viewsets.py (omitting imports)
class ItemViewSet(viewsets.ModelViewSet):
queryset = Item.objects.all()
serializer_class = ItemSerializer
#action(methods=['GET'], detail=False)
def most_expensive(self, request):
query = self.get_queryset().order_by('price').last()
serialized = self.serializer_class(query)
return Response(serialized.data)
Now I want to able to access this API from my views.py to render the HTML with the available items:
This is the way im doing it right now:
views.py (omitting imports)
class ProductListView(View):
template = 'store/product_list.html'
def get(self, request):
items = requests.get('http://127.0.0.1:8000/api/items/available')
context = {'items': items}
return render(request, self.template, context=context)
Using the requests modules I have a couple of concerns, after measuring I noticed there is a 0.015 second delay for that request to go through and if I ever change the API endpoint I would have to adjust it here since its hard coded.
I can get my items using:
Item.objects.filter(available=True)
Which gives me the result pretty much instantly but I'm writing all the queries twice (once in my API and once in my views.py)
Is there a better way of doing this like calling the viewset class directly and getting the data from there?
Many thanks!
Calling the API endpoint in the same app is not considered a good practise.
An option would be to call your viewset method directly, like in https://stackoverflow.com/a/51149806/290036
The other one that I recommend is to use the same codebase for your API and for the view.
def get_avaialble_items():
items = Item.objects.filter(available=True)
...
return items
# Use get_avaialble_items both in ItemViewSet and ProductListView
I have an endpoint where I need to make make a request to a third party API to get a list of items and return the results to the client.
Which of the following, or any other approach would be better suited to DRF?
Make input parameter validation and the call to the third party API and in the view method, pass the list of items in the response to a serializer for serialization and return serializer data to the client
Pass the request parameters to the serializer as write-only fields, make the field validation, api call and serialization in the serializer
A mixture of 1 and 2; use 2 different serializers, one that takes request parameters as write only fields, validates input parameters and makes the request to the 3rd party api, and another serializer that takes the resulting list from the first serializer and serializes the items for use of client
Since your question not mentioning anything about writing data into DB, undoubtedly you can go with Method-1.
Let's look into this sample api, which return a list of items (a list api).
Case - 1 : We need show the same response as we got from third-party api
In that case, we don't need any serializer or serialization process, all we need is pass the data from third-party API to the client.
from rest_framework.decorators import api_view
from rest_framework.response import Response
import requests
#api_view()
def my_view(request):
tp_api = "https://jsonplaceholder.typicode.com/posts"
response = requests.get(tp_api)
return Response(data=response.json())
Case - 2 : If you don't need complete data, but few parts (id and body)
In this particular situation, you can go with pythonic loop or DRF serializer.
# using DRF serializer
from rest_framework import serializers
# serializer class
class Myserializer(serializers.Serializer):
id = serializers.CharField()
body = serializers.CharField()
#api_view()
def my_view(request):
tp_api = "https://jsonplaceholder.typicode.com/posts"
response_data = requests.get(tp_api).json()
my_serializer = Myserializer(data=response_data, many=True)
my_serializer.is_valid(True)
return Response(data=my_serializer.data)
#Python loop way
#api_view()
def my_view(request):
tp_api = "https://jsonplaceholder.typicode.com/posts"
response_data = requests.get(tp_api).json()
data = [{"id": res['id'], "body": res['body']} for res in response_data]
return Response(data=data)
In case-2, I would reccomend to use DRF serializer, which does lots of things like validation, etc
When coming into your second approch, doing validation of the input data would depends on your requirement. As you said in comments, you need to provide some inputs to the third-party api. So, the validation should be carried out before accessing the third-party api
# Validation example
class MyInputSerializer(serializers.Serializer):
postId = serializers.IntegerField(max_value=10)
class MyOutputSerializer(serializers.Serializer):
id = serializers.CharField()
body = serializers.CharField()
#api_view()
def my_view(request):
input = MyInputSerializer(data=request.GET)
input.is_valid(True)
tp_api = "https://jsonplaceholder.typicode.com/comments?postId={}".format(input.data['postId'])
response_data = requests.get(tp_api).json()
my_serializer = MyOutputSerializer(data=response_data, many=True)
my_serializer.is_valid(True)
return Response(data=my_serializer.data)
Conclusion
The DRF is flexible enough to get desired output format as well as taking data into the system. In short, It all depends on your requirements
I have two related models (Events + Locations) with a serialzer shown below:
class Locations
title = models.CharField(max_length=250)
address = model.CharField(max_length=250)
class Events
title = models.CharField(max_length=250)
locations = models.ForeignKey(Locations, related_name='events'
class EventsSerializer(serializers.ModelSerializer):
class Meta:
model = Events
depth = 1
I set the depth to 1 in the serializer so I can get the information from the Locations model instead of a single id. When doing this however, I cant post to events with the location info. I can only perform a post with the title attribute. If I remove the depth option in the serializer, I can perform the post with both the title and location id.
I tried to create a second serializer (EventsSerialzerB) without the depth field with the intention of using the first one as a read-only response, however when I created a second serializer, viewset, and added it to the router, it would automatically override the original viewset.
Is it possible for me to create a serializer that outputs the related model fields, and allows you to post directly to the single model?
// EDIT - Here's what I'm trying to post
$scope.doClick = function (event) {
var test_data = {
title: 'Event Test',
content: 'Some test content here',
location: 2,
date: '2014-12-16T11:00:00Z'
}
// $resource.save() doesn't work?
$http.post('/api/events/', test_data).
success(function(data, status, headers, config) {
console.log('sucess', status);
}).
error(function(data, status, headers, config) {
console.log('error', status);
});
}
So when the serializers are flat, I can post all of these fields. The location field is the id of a location from the related Locations table. When they are nested, I can't include the location field in the test data.
By setting the depth option on the serializer, you are telling it to make any relation nested instead of flat. For the most part, nested serializers should be considered read-only by default, as they are buggy in Django REST Framework 2.4 and there are better ways to handle them in 3.0.
It sounds like you want a nested representation when reading, but a flat representation when writing. While this isn't recommended, as it means GET requests don't match PUT requests, it is possible to do this in a way to makes everyone happy.
In Django REST Framework 3.0, you can try the following to get what you want:
class LocationsSerializer(serializers.ModelSerializer):
class Meta:
model = Locations
fields = ('title', 'address', )
class EventsSerializer(serializers.ModelSerializer):
locations = LocationsSerializer(read_only=True)
class Meta:
model = Events
fields = ('locations', )
class EventViewSet(viewsets.ModelViewSet):
queryet = Event.objects.all()
serializer_class = EventsSerializer
def perform_create(self, serializer):
serializer.save(locations=self.request.data['locations'])
def perform_update(self, serializer):
serializer.save(locations=self.request.data['locations'])
A new LocationsSerializer was created, which will handle the read-only nested representation of the Locations object. By overriding perform_create and perform_update, we can pass in the location id that was passed in with the request body, so the location can still be updated.
Also, you should avoid having model names being plurals. It's confusing when Events.locations is a single location, even though Locations.events is a list of events for the location. Event.location and Location.events reads a bit more clearly, the Django admin will display them reasonably, and your fellow developers will be able to easily understand how the relations are set up.
I have a working GET / tastypie (read-only) solution.
I've allowed PUT/PATCH requests and been successful in PATCHING a record.
However I want to limit PATCH capability to only certain fields, on appropriate modelresources, for (already) authenticated and authorised users. I still want users to be able to GET (see) all fields.
Where is the best place (method?) to achieve this sort of restriction?
Docs:
https://django-tastypie.readthedocs.org/en/latest/interacting.html?highlight=patch#partially-updating-an-existing-resource-patch
A bit late but maybe this will help somebody.
My solution was to override update_in_place and check for the data passed.
from tastypie.resources import ModelResource
from tastypie.exceptions import BadRequest
class MyResource(ModelResource):
class Meta:
...
allowed_update_fields = ['field1', 'field2']
def update_in_place(self, request, original_bundle, new_data):
if set(new_data.keys()) - set(self._meta.allowed_update_fields):
raise BadRequest(
'Only update on %s allowed' % ', '.join(
self._meta.allowed_update_fields
)
)
return super(MyResource, self).update_in_place(
request, original_bundle, new_data
)
Since you seem to have authorization for users in place already, you should be able to implement this by adding to the Meta class in your ModelResource. For example, using the DjangoAuthorization (from tastypie docs):
from tastypie.authentication import BasicAuthentication
from tastypie.authorization import DjangoAuthorization
...
class SomeResource(ModelResource):
...
class Meta:
...
authentication = BasicAuthentication()
authorization = DjangoAuthorization()
This example would give you user authorization for actions as defined in django.contrib.auth.models.Permission.
I also had this from the tastypie Google Group. It uses the dehydrate method. Here is the example provided in the Google Groups link:
def dehydrate(self, bundle):
bundle = super(self, MyResource).dehydrate(bundle)
# exclude the restricted field for users w/o the permission foo
if not bundle.request.user.has_perm('app.foo'):
del bundle.data['restricted']
return bundle