I have many endpoints which use ModelViewSet to manage CRUD operations for my models.
What I am trying to do, is to add bulk create, update, and delete at these same endpoints. In other words, I want to add POST, PUT, PATCH and DELETE to the collection endpoint (e.g.: /api/v1/my-model). There is a django-rest-framework-bulk package available, but it seems to be abandoned (hasn't been updated in 4 years) and I'm not comfortable using a package in production that is no longer active.
Additionally, There are several similar questions here that have solutions, as well as blog posts I've found. However, they all seem to use the base ViewSet, or APIView, which would require re-writing all of my existing ModelViewSet code.
Finally, there is the option of using the #action decorator, however this would require me to have a separate list endpoint (e.g.- /api/v1/my-model/bulk) which I'd like to avoid.
Are there any other ways to accomplish this while keeping my existing ModelViewSet views? I've been looking at GenericViewSet and mixins, and am wondering if creating my own mixin might be the way to go. However, looking at the mixin code, it doesn't appear that you can specify an HTTP Request method to be attached to a given mixin.
Finally, I have tried creating a separate ViewSet that accepts PUT and adding it to my URLs, but this doesn't work (I get a 405 Method not allowed when I try to PUT to /api/v1/my-model). The code I tried looks like this:
# views.py
class MyModelViewSet(viewsets.ModelViewSet):
serializer_class = MyModelSerializer
permission_classes = (IsAuthenticated,)
queryset = MyModel.objects.all()
paginator = None
class ListMyModelView(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
def put(self, request):
# Code for updating list of models will go here.
return Response({'test': 'list put!'})
# urls.py
router = DefaultRouter(trailing_slash=False)
router.register(r'my-model', MyModelViewSet)
router.register(r'my-model', ListMyModelView, base_name='list-my-model')
urlpatterns = [
path('api/v1/', include(router.urls)),
# more paths for auth, admin, etc..
]
Thoughts?
I know you said you wanted to avoid adding an extra action but in my opinion it's the simplest way to update your existing views for bulk create/update/delete.
You can create a mixin that you add to your views that will handle everything, you'd just be changing one line in your existing views and serializers.
Assuming your ListSerializer look similar to the DRF documentation the mixins would be as follows.
core/serializers.py
class BulkUpdateSerializerMixin:
"""
Mixin to be used with BulkUpdateListSerializer & BulkUpdateRouteMixin
that adds the ID back to the internal value from the raw input data so
that it's included in the validated data.
"""
def passes_test(self):
# Must be an update method for the ID to be added to validated data
test = self.context['request'].method in ('PUT', 'PATCH')
test &= self.context.get('bulk_update', False)
return test
def to_internal_value(self, data):
ret = super().to_internal_value(data)
if self.passes_test():
ret['id'] = self.fields['id'].get_value(data)
return ret
core/views.py
class BulkUpdateRouteMixin:
"""
Mixin that adds a `bulk_update` API route to a view set. To be used
with BulkUpdateSerializerMixin & BulkUpdateListSerializer.
"""
def get_object(self):
# Override to return None if the lookup_url_kwargs is not present.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
if lookup_url_kwarg in self.kwargs:
return super().get_object()
return
def get_serializer(self, *args, **kwargs):
# Initialize serializer with `many=True` if the data passed
# to the serializer is a list.
if self.request.method in ('PUT', 'PATCH'):
data = kwargs.get('data', None)
kwargs['many'] = isinstance(data, list)
return super().get_serializer(*args, **kwargs)
def get_serializer_context(self):
# Add `bulk_update` flag to the serializer context so that
# the id field can be added back to the validated data through
# `to_internal_value()`
context = super().get_serializer_context()
if self.action == 'bulk_update':
context['bulk_update'] = True
return context
#action(detail=False, methods=['put'], url_name='bulk_update')
def bulk_update(self, request, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(
queryset,
data=request.data,
many=True,
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data, status=status.HTTP_200_OK)
Then you would just inherit from the mixins
class MyModelSerializer(BulkUpdateSerializerMixin
serializers.ModelSerializer):
class Meta:
model = MyModel
list_serializer_class = BulkUpdateListSerializer
class MyModelViewSet(BulkUpdateRouteMixin,
viewsets.ModelViewSet):
...
And your PUT request would just have to point to '/api/v1/my-model/bulk_update'
Updated mixins that don't require extra viewset action:
For bulk operations submit a POST request to the list view with data as a list.
class BulkUpdateSerializerMixin:
def passes_test(self):
test = self.context['request'].method in ('POST',)
test &= self.context.get('bulk', False)
return test
def to_internal_value(self, data):
ret = super().to_internal_value(data)
if self.passes_test():
ret['id'] = self.fields['id'].get_value(data)
return ret
In get_serializer() there's a check to ensure that only POST requests can be accepted for bulk operations. If it's a POST and the request data is a list then add a flag so the ID field can be added back to the validated data and your ListSerializer can handle the bulk operations.
class BulkUpdateViewSetMixin:
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
if self.request.method in ('POST',):
data = kwargs.get('data', None)
is_bulk = isinstance(data, list)
kwargs['many'] = is_bulk
kwargs['context']['bulk'] = is_bulk
return serializer_class(*args, **kwargs)
def create(self, request, *args, **kwargs):
if isinstance(request.data, list):
return self.bulk_update(request)
return super().create(request, *args, **kwargs)
def bulk_update(self, request):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(
queryset,
data=request.data,
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data, status=status.HTTP_200_OK)
I've tested that this works but I have no idea how it will affect API schema documentation.
Related
I am using Django-rest for developing the API. In my case, when the user posts the data, I have to process the posted data (It will take 2-3 min). I wrote the Django signal for preprocessing data. My signal.py file looks like this,
#receiver(post_save, sender=ExposureIndex)
def calculate_exposure(instance, created, *args, **kwargs):
ear_table = instance.ear_index.name
haz_dir = instance.hazard_index.file.path
# calling celery task
task = exposure_calculation.delay(ear_table,haz_dir)
return task.id
And my celery calculation function is here,
#shared_task(bind=True)
def exposure_calculation(self, ear_table, haz_dir):
progress_recorder = ProgressRecorder(self)
CalculateExposure(ear_table, haz_dir)
return 'Done'
My django-rest view function is looks like this,
class ExposureIndexViewSet(viewsets.ModelViewSet):
queryset = ExposureIndex.objects.all()
serializer_class = ExposureIndexSerializer
permission_classes = [permissions.IsAuthenticated]
My question is when the user posts the data, I want to return the task.id instead of returning the actual response (I tried to return it from the Django signal but it is not returning in the actual API). Can anyone suggest to me how to return task.id instantly when the user posts the exposureIndexData?
I think you should override the create method in views.py rather than creating the signal instance. Do something like this in views.py file
class ExposureIndexViewSet(viewsets.ModelViewSet):
queryset = ExposureIndex.objects.all()
serializer_class = ExposureIndexSerializer
permission_classes = [permissions.IsAuthenticated]
def create(self, request, *args, **kwargs):
response = super().create(request, *args, **kwargs)
instance = response.data
ear_table = instance['ear_table']
haz_dir = instance['haz_dir']
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
task = exposure_calculation.delay(ear_table,haz_dir)
return Response({'task_id': task.id})
instead of using signals u can simply send response through your view
def post(self,request):
#create database manaully
task=exposure_calculation.delay(ear_table,haz_dir)
return Response({"message",task.id})
I have the following view:
class ReadClass(generics.RetrieveUpdateDestroyAPIView):
queryset = MyCModel.objects.all()
serializer_class = MySerializer
def post(self, request, *args, **kwargs):
''' defined my post here'''
I know retrieveupdatedestroyapiview doesn't have post in it. And I have created my own post in the view here and on the front end, I see both post and put! Is there any way to remove the put.
Or is there any other way to do it better, I tried using ListCreateApi view. The problem with that is while it gives me the post functionality, it lists all the values, while I am looking for a specific pk. I cannot see any other generic view that gives me get and post functionality.
EDIT
I have added the edit as requested, try and except might seem unnecessary here at the moment, but I will add more functionality later on.
class ReadClass(generics.GenericAPIView, mixins.CreateModelMixin, mixins.RetrieveModelMixin):
queryset = MyCModel.objects.all()
serializer_class = MySerializer
def post(self, request, *args, **kwargs):
try:
s1 = MySerializer.objects.get(mRID=kwargs["pk"])
serializer = MySerializer(s1, data=request.data)
except MySerializer.DoesNotExist:
pass
if serializer.is_valid():
if flag == 0:
pass
else:
serializer.update(s1,validated_data=request.data)
else:
return Response(serializer.errors)
urlpatterns = [path('temp/<int:pk>', ReadClass.as_view(), name = " reading"),]
DRF has mixins for List, Create, Retrieve, Update and Delete functionality. Generic views just combine these mixins. You can choose any subset of these mixins for your specific needs. In your case, you can write your view like this, if you only want Create and Retrieve functionalty:
class ReadClass(GenericAPIView, CreateModelMixin, RetrieveModelMixin):
queryset = MyCModel.objects.all()
serializer_class = MySerializer
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
This would provide default functionality for post and get requests. If you prefer, you can override post method like you did in your example to customize post requset behavior.
You can read more about mixins and generic views here
I want to create two endpoints /comments/ and /comments/requests/ or something to that effect. The first shows your comments, and the second shows your pending comments (Comments that people sent you that you need to approve). They both work with a comments model. How could I achieve this in Django Rest Framework?
Right now, my view is
class CommentsListview(APIView):
serializer_class = CommentSerializer
def get(self, request, format=None):
comments, _, _, = Comments.get_comment_users(request.user)
comments_serializer = CommentSerializer(comments, many=True)
return Response({'comments': comments_serializer.data})
def requests(sel,f request, format=None):
_, requests, _ = Comments.get_comment_users(request.user)
requests_serializer = CommentSerializer(requests, many=True)
return Response({'requests': requests_serializer.data})
I'd like to allow a user to go to localhost:8000/comments/ to view their comments and localhost:8000/comments/requests/ to view their pending comment requests. Since I haven't been able to figure this out, the only other sollution would be to require the user to switch the behavior of the endpoint using a parameter as a flag /comments/?requests=True but that just seems sloppy.
use list_route decorator and genericviewset
from rest_framework import viewsets
from rest_framework.decorators import list_route
class CommentsListview(viewsets.GenericViewSet):
serializer_class = CommentSerializer
def list(self, request, format=None):
comments, _, _, = Comments.get_comment_users(request.user)
comments_serializer = CommentSerializer(comments, many=True)
return Response({'comments': comments_serializer.data})
#list_route()
def requests(sel,f request, format=None):
_, requests, _ = Comments.get_comment_users(request.user)
requests_serializer = CommentSerializer(requests, many=True)
return Response({'requests': requests_serializer.data})
/comments/ will call list method
/comments/requests/ will call requests method
also look at GenericViews and ViewSet docs it might be helpfull
Im trying to perform partial update in DRF using angular $http.
In my DRF model viewset i override the partial_update function (server side).
class AnimalTreatmentViewSet(viewsets.ModelViewSet):
queryset = MyObject.objects.all()
serializer_class = MyObjectSerializer
def create(self, request):
# It works with $http.post()
pass
def update(self, request, pk=None):
# It works with $http.put()
pass
def list(self, request):
# It works with $http.get()
pass
def partial_update(self, request, pk=None):
# This one wont work with $http.patch()
instance = self.get_object()
serializer = self.serializer_class(instance, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
In the client side angular handle the user request.
$http.patch('/api/my_viewset_url/1', data);
But i got this response Method Not Allowed (PATCH): /api/my_viewset_url/1
When using $http.get() request with DRF model viewset list(self, request) it works well for getting a list same goes for $http.post() with def create(self, request) for creating object and $http.put() with def update(self, request) for updating object.
What's wrong? or what is the correct http verb for partial_update in DRF model viewset
It seems like the URL is missing a trailing slash.
Related documentation:
By default the URLs created by SimpleRouter/DefaultRouter are appended with a
trailing slash. This behavior can be modified by setting the trailing_slash argument to False when instantiating the router. For example:
router = routers.DefaultRouter(trailing_slash=False)
I am using http://www.django-rest-framework.org/
I have the scenario where I want to pass two or more variables based on that I need to fetch data from database. In the following code only pk is there which I want to replace with two other fields in database.
Also please suggest how can I write my urlconfig the same.
Views.py
class ExampleViewSet(viewsets.ReadOnlyModelViewSet):
model = myTable
def list(self, request):
queryset = myTable.objects.all()
serializer = mySerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
queryset = myTable.objects.all()
s = get_object_or_404(queryset, pk=pk)
serializer = mySerializer(s)
return Response(serializer.data)
Serializer.py
class Serializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = myTable
fields = ('attr1', 'attr2', 'attr3')
Here is how you would do it with the recent Django REST Framework.
Assuming your variables are in the resource URLs like so:
GET /parent/:id/child/
GET /parent/:id/child/:id/
Then:
urls.py:
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'parent/(?P<parent_id>.+)/child', views.ExampleViewSet)
urlpatterns = router.urls
views.py:
class ExampleViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = Serializer
def get_queryset(self):
parent = self.kwargs['parent']
return myTable.objects.filter(parent=parent)
Where the 'parent' in the queryset part is your parent object. You may need to adjust it a little, of course, but the idea is encapsulated in the kwargs.
This solution will also save you a little code and you can make it into a full blown ModelViewSet just by subclassing it.
Hope that helps.
More here: DRF Filtering against the URL.
Here is an example of how you might implement what you want:
class ExampleViewSet(viewsets.ReadOnlyModelViewSet):
# This code saves you from repeating yourself
queryset = myTable.objects.all()
serializer_class = mySerializer
def list(self, request, *args, **kwargs):
# Get your variables from request
var1 = request.QUERY_DICT.get('var1_name', None) # for GET requests
var2 = request.DATA.get('var2_name', None) # for POST requests
if var1 is not None:
# Get your data according to the variable var1
data = self.get_queryset().filter(var1)
serialized_data = self.get_serializer(data, many=True)
return Response(serialized_data.data)
if var2 is not None:
# Do as you need for var2
return Response(...)
# Default behaviour : call parent
return super(ExampleViewSet, self).list(request, *args, **kwargs)
def retrieve(self, request, *args, **kwargs):
# Same for retrieve
# 1. get your variable xyz from the request
# 2. Get your object based on your variable's value
s = myTable.objects.get(varX=xyz)
# 3. Serialize it and send it as a response
serialized_data = self.get_serializer(s)
return Response(serialized_data.data)
# 4. Don't forget to treat the case when your variable is None (call parent method)
As for the urlconf, it depends on how you want to send your variables (get, post or through the url).
Hope this helps.
urls.py
url(
regex=r'^teach/(?P<pk>\d+?)/(?P<pk1>\d+?)/$',
view=teach_update.as_view(),
name='teach'
)
Templates
<td><a href="/teach/{{tid}}/{{i.id}}"><button type="button" class="btn
btn-warning">Update</button></a></td>
Views.py
class teach_update(view):
def get(self,request,**kwargs):
dist=self.kwargs['pk']
ddd=self.kwargs['pk1']