I have a model named Article and a few api views for it. They are divided for diffrent purposes (for example ArticleUpdateAPI class for UPDATE http method, ArticleDeleteAPI for DELETE method etc). In urls.py they are separated to diffrent endpoints (aritcle/pk/update, /article/pk/delete etc).
As I know, it's not good practice to build endpoint like this, so I want to bind them to single url and use diffrent classes for handling diffrent http methods. Is it possible and how? Examples are below
ArticleAPI.py
class ArticlePostAPI(generics.CreateAPIView):
serializer_class = ArticleSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly
]
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response({
"comment": CommentSerializer.data
}, status=201)
class ArticleRetrieveAPI(generics.RetrieveAPIView):
serializer_class = ArticleSerializer
queryset = Article.objects.all()
permission_classes = [
permissions.AllowAny
]
class ArticleListAPI(generics.ListAPIView):
serializer_class = ArticleSerializer
queryset = Article.objects.order_by('number', 'headline')
permission_classes = [
permissions.AllowAny
]
class ArticleUpdateAPI(generics.UpdateAPIView):
serializer_class = ArticleSerializer
queryset = Article.objects.all()
permission_classes = [
permissions.IsAuthenticated
]
lookup_field = 'pk'
def update(self, request, *args, **kwargs):
instance = self.get_object()
if request.user != instance.author:
return Response({
"errors": "Logged in user and author must be same"
}, status=403)
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
urls.py
urlpatterns = [
...
# Article API
path('article/post/', ArticlePostAPI.as_view(), name='article_creation'),
path('article/<int:pk>/', ArticleRetrieveAPI.as_view(), name='article_retrieve'),
path('article/', ArticleListAPI.as_view(), name='article_list'),
path('article/<int:pk>/update/', ArticleUpdateAPI.as_view(), name='article_update'),
]
Use ModelViewSet for simplifying it.
Using this you can perform all http method actions with a single url and single view.
from rest_framework import viewsets
class ArticleView(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
queryset = Article.objects.all()
permission_classes = [permissions.AllowAny]
urls.py
router = DefaultRouter()
router.register(r'articles', views.ArticleView, basename='articles')
here
GET - /articles/ - gives list of all articles
GET - /articles/id/ - gives one object
POST - /articles/ - creates an article
PUT - /articles/id/ - updates an article
PATCH - /articles/id/ - partially updates an article
DELETE - /articles/id/ - deletes an article
I am passing a value in my API POST request like this
{
"reason": "string"
}
And my view is like this,
class UpdateReason(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = serializers.ReasonSerializer
queryset = Food.objects.all()
def post(self, request, *args, **kwargs):
self.get_serializer().instance = service.update(self.get_object())
return Response({"success": True})
serializer.py
class ReasonSerializer(serializers.ModelSerializer):
class Meta:
model = Food
fields = ("id", "reason")
read_only_fields = ("id",)
In the post, I have to get the value of the reason and pass it to the service. How can I execute this?
Simply request.data.get('reason') :)
I am following this solution on how to get specific fields from a django model:
Select specific fields in Django get_object_or_404
from django.core import serializers as djangoserializer # module 'rest_framework.serializers' has no attribute 'serialize'
class ProjectDetailApiView(APIView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, slug=None, format=None):
project_instance = get_object_or_404(Project.objects.only('project_title', 'project_post'), slug=slug)
data = djangoserializer.serialize('json', [ project_instance, ], fields=('project_title','project_post'))
user = self.request.user
updated = False
viewed = False
if not user in project_instance.project_views.all():
viewed = True
project_instance.project_views.add(user)
updated = True
data = {
"project": data,
"updated":updated,
"viewed":viewed
}
return Response(data)
Output:
{
"project": "[{\"model\": \"webdata.project\", \"pk\": 4, \"fields\": {\"project_title\": \"Project 4\", \"project_post\": \"Blabla\"}}]",
"updated": true,
"viewed": false
}
Desired Output:
{
"project_title": "Project 4",
"project_post": "Blabla",
"updated": true,
"viewed": false
}
Thank you
Use DRF's Serializer instead of Django's built-in serializer.
# serializers.py
from rest_framework import serializers
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ('project_title', 'project_post', 'updated', 'viewed')
# views.py
class ProjectDetailApiView(APIView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, slug=None, format=None):
project_instance = get_object_or_404(Project, slug=slug)
serializer = ProjectSerializer(project_instance)
return Response(serializer.data)
The serialization of regular dictionaries is very good in python.
So instead of configuring the serializer - why not just create a python dictionary with the desired data? (That's how I do it for simple things that I need only in one place.)
data = {
"project_title": project_instance.project_title,
"project_post": project_instance.project_post,
"updated":updated,
"viewed":viewed
}
return JSONResponse(data)
You haven't posted the Project model, I'm just supposing from the serializer config that the fields are named project_title and project_post.
This will return a response with status 200, mimetype application/json and the data dict as valid JSON.
I am using Django Rest Framework here. When I make a post request from the front end with React, the title error is returned.
My perform_create() function is within my view class:
class MaxListCreate(generics.ListCreateAPIView):
queryset = Max.objects.all()
serializer_class = MaxSerializer
filter_backends = (filters.OrderingFilter,)
ordering_fields = ('exercise', 'date',)
ordering = ('exercise', 'date',)
permission_classes = (permissions.IsAuthenticated,)
def perform_create(self):
user = self.request.user
serializer.save(user=user)
# User can only access the data associated with the user.
def get_queryset(self):
user = self.request.user
return Max.objects.filter(user=user)
The endpoint data format should be:
{
"id": 5,
"date": "2018-08-07",
"max_lift": 80,
"user": 1,
"exercise": 1
},
and the form sends:
{"exercise":"3","date":"2018-08-23","max_lift":"70"}
The intention of the perform_create() function is to supply the user key from the back end.
Any help would be appreciated,
Add serializer also, as parameter of perform_create method
def perform_create(self, serializer):
user = self.request.user
serializer.save(user=user)
Read more about generic-views in official docs
I would like to save and update multiple instances using the Django Rest Framework with one API call. For example, let's say I have a "Classroom" model that can have multiple "Teachers". If I wanted to create multiple teachers and later update all of their classroom numbers how would I do that? Do I have to make an API call for each teacher?
I know currently we can't save nested models, but I would like to know if we can save it at the teacher level.
Thanks!
I know this was asked a while ago now but I found it whilst trying to figure this out myself.
It turns out if you pass many=True when instantiating the serializer class for a model, it can then accept multiple objects.
This is mentioned here in the django rest framework docs
For my case, my view looked like this:
class ThingViewSet(viewsets.ModelViewSet):
"""This view provides list, detail, create, retrieve, update
and destroy actions for Things."""
model = Thing
serializer_class = ThingSerializer
I didn't really want to go writing a load of boilerplate just to have direct control over the instantiation of the serializer and pass many=True, so in my serializer class I override the __init__ instead:
class ThingSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
many = kwargs.pop('many', True)
super(ThingSerializer, self).__init__(many=many, *args, **kwargs)
class Meta:
model = Thing
fields = ('loads', 'of', 'fields', )
Posting data to the list URL for this view in the format:
[
{'loads':'foo','of':'bar','fields':'buzz'},
{'loads':'fizz','of':'bazz','fields':'errrrm'}
]
Created two resources with those details. Which was nice.
I came to a similar conclusion as Daniel Albarral, but here's a more succinct solution:
class CreateListModelMixin(object):
def get_serializer(self, *args, **kwargs):
""" if an array is passed, set serializer to many """
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super(CreateListModelMixin, self).get_serializer(*args, **kwargs)
Here's another solution, you don't need to override your serializers __init__ method. Just override your view's (ModelViewSet) 'create' method. Notice many=isinstance(request.data,list). Here many=True when you send an array of objects to create, and False when you send just the one. This way, you can save both an item and a list!
from rest_framework import status, viewsets
from rest_framework.response import Response
class ThingViewSet(viewsets.ModelViewSet):
"""This view snippet provides both list and item create functionality."""
#I took the liberty to change the model to queryset
queryset = Thing.objects.all()
serializer_class = ThingSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, many=isinstance(request.data,list))
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
I couldn't quite figure out getting the request.DATA to convert from a dictionary to an array - which was a limit on my ability to Tom Manterfield's solution to work. Here is my solution:
class ThingSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
many = kwargs.pop('many', True)
super(ThingSerializer, self).__init__(many=many, *args, **kwargs)
class Meta:
model = Thing
fields = ('loads', 'of', 'fields', )
class ThingViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet ):
queryset = myModels\
.Thing\
.objects\
.all()
serializer_class = ThingSerializer
def create(self, request, *args, **kwargs):
self.user = request.user
listOfThings = request.DATA['things']
serializer = self.get_serializer(data=listOfThings, files=request.FILES, many=True)
if serializer.is_valid():
serializer.save()
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)
And then I run the equivalent of this on the client:
var things = {
"things":[
{'loads':'foo','of':'bar','fields':'buzz'},
{'loads':'fizz','of':'bazz','fields':'errrrm'}]
}
thingClientResource.post(things)
I think the best approach to respect the proposed architecture of the framework will be to create a mixin like this:
class CreateListModelMixin(object):
def create(self, request, *args, **kwargs):
"""
Create a list of model instances if a list is provided or a
single model instance otherwise.
"""
data = request.data
if isinstance(data, list):
serializer = self.get_serializer(data=request.data, many=True)
else:
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
Then you can override the CreateModelMixin of ModelViewSet like this:
class <MyModel>ViewSet(CreateListModelMixin, viewsets.ModelViewSet):
...
...
Now in the client you can work like this:
var things = [
{'loads':'foo','of':'bar','fields':'buzz'},
{'loads':'fizz','of':'bazz','fields':'errrrm'}
]
thingClientResource.post(things)
or
var thing = {
'loads':'foo','of':'bar','fields':'buzz'
}
thingClientResource.post(thing)
EDIT:
As Roger Collins suggests in his response is more clever to overwrite the get_serializer method than the 'create'.
You can simply overwrite the get_serializer method in your APIView and pass many=True into get_serializer of the base view like so:
class SomeAPIView(CreateAPIView):
queryset = SomeModel.objects.all()
serializer_class = SomeSerializer
def get_serializer(self, instance=None, data=None, many=False, partial=False):
return super(SomeAPIView, self).get_serializer(instance=instance, data=data, many=True, partial=partial)
I came up with simple example in post
Serializers.py
from rest_framework import serializers
from movie.models import Movie
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = [
'popularity',
'director',
'genre',
'imdb_score',
'name',
]
Views.py
from rest_framework.response import Response
from rest_framework import generics
from .serializers import MovieSerializer
from movie.models import Movie
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
class MovieList(generics.ListCreateAPIView):
queryset = Movie.objects.all().order_by('-id')[:10]
serializer_class = MovieSerializer
permission_classes = (IsAuthenticated,)
def list(self, request):
queryset = self.get_queryset()
serializer = MovieSerializer(queryset, many=True)
return Response(serializer.data)
def post(self, request, format=None):
data = request.data
if isinstance(data, list): # <- is the main logic
serializer = self.get_serializer(data=request.data, many=True)
else:
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
These line are the actual logic of Multiple Instance -
data = request.data
if isinstance(data, list): # <- is the main logic
serializer = self.get_serializer(data=request.data, many=True)
else:
serializer = self.get_serializer(data=request.data)
If you are confused with many=True, see this
When we send data it will be inside list somewhat like this -
[
{
"popularity": 84.0,
"director": "Stanley Kubrick",
"genre": [
1,
6,
10
],
"imdb_score": 8.4,
"name": "2001 : A Space Odyssey"
},
{
"popularity": 84.0,
"director": "Stanley Kubrick",
"genre": [
1,
6,
10
],
"imdb_score": 8.4,
"name": "2001 : A Space Odyssey"
}
]
The Generic Views page in Django REST Framework's documentation states that the ListCreateAPIView generic view is "used for read-write endpoints to represent a collection of model instances".
That's where I would start looking (and I'm going to actually, since we'll need this functionality in our project soon as well).
Note also that the examples on the Generic Views page happen to use ListCreateAPIView.
Most straightforward method I've come across:
def post(self, request, *args, **kwargs):
serializer = ThatSerializer(data=request.data, many=isinstance(request.data, list))
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)