I am trying to make serializer class dynamic, but its not working. I have a default serializer class where as a dynamic serializer class for different actions. Here it is my modelviewset.
My view:
class ClassView(viewsets.ModelViewSet):
queryset = Class.objects.all()
serializer_class = ClassSerializer
serializer_action_classes = {
'put': AddStudentstoClassSerializer,
}
def get_serializer_class(self):
"""
returns a serializer class based on the http method
"""
try:
return self.serializer_action_classes[self.action]
except (KeyError, AttributeError):
print("iam ClassSerializer")
return super(ClassView, self).get_serializer_class()
My function inside the same modelviewset above
#action(detail=True, methods=['put'])
def add_remove_students(self, request, *args, **kwargs):
................
MY url is as below:
urlpatterns = [
path("class/<int:pk>/<slug:slug>/",views.ClassView.as_view({"put": "add_remove_students"}),
),
]
Here in the above code snippet, I try to get AddStudentstoClassSerializer inside the add_remove_students function but it is not working. As we can see the print("iam ClassSerializer") code is working, however what i wanted or AddStudentstoClassSerializer.
First of all your serializer_action_classes dictionary should look like this:
serializer_action_classes = {
'add_remove_students': AddStudentstoClassSerializer,
}
because self.action return name of the action, not the method name. What you mean to use is self.request.method attribute which shoud return PUT in this case..
But there's better way to achieve your goal:
#action(detail=True, methods=['put'], serializer_class=AddStudentstoClassSerializer)
def add_remove_students(self, request, *args, **kwargs):
action decorators can ovveride used serializer_class by themselves.
Related
I'm using GenericAPIView with the CreateModelMixin to create a model instance. I need my serializer to add additional fields that aren't defined by the user. My Serializer.create method is already set up for this, but I don't know how to pass fields through to the CreateModelMixin.create method. Here's a minimal version of what I have:
class Foo(mixins.CreateModelMixin, generics.GenericAPIView):
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
return FooSerializer
def post(self, request):
return self.create(
request, requester=request.user # Additional field
)
This doesn't work - the requester field isn't passed to FooSerializer.save, so FooSerializer throws an error when it attempts to access requester in FooSerializer.create. Before, I was using APIView and calling the serializer directly, so I could simply:
serializer = FooSerializer(data=request.data)
if serializer.is_valid():
foo = serializer.save(requester=request.user)
Is there any way to achieve this with the GenericAPIView? I want to embrace DRF's DRY-ness and avoid calling serializers in every endpoint method.
Instead of overriding create method you can override perform_create. Also you may need to define post method:
class Foo(mixins.CreateModelMixin, generics.GenericAPIView):
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
return FooSerializer
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(requester=self.request.user)
I would like to update a record in my database with the REST framework. I'm getting an message of method not allowed for anything but "GET".
views.py
class MetadataViewTest(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
queryset = deployment.objects.all()
serializer_class = SerializerTest
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
urls.py
urlpatterns = [
path('api/metadata_test/<int:pk>/', views.MetadataViewTest.as_view())
]
serializers.py
class SerializerTest(serializers.ModelSerializer):
class Meta:
model = deployment
fields = [field.name for field in deployment._meta.fields]
I've tried the request through postman as a PATCH PUT or POST at api/metadata_test/20/
This is a simplified version, I plan on overriding the get put and delete functions.
Update your code to:
views.py
class MetadataViewTest(generics.RetrieveUpdateDestroyAPIView):
queryset = deployment.objects.all()
serializer_class = SerializerTest
urls.py
urls.py
urlpatterns = [
path('api/metadata_test/', views.MetadataViewTest.as_view()) # Should to the list, not detail
]
Call API like this PUT/PATCH: api/metadata_test/20/
It better if you use viewsets.GenericViewSet instead of generics.GenericAPIView because you are using ...ModelMixin.
Your router like this
from rest_framework import routers
api_router = routers.DefaultRouter()
api_router.register('metadata_test', MetadataViewTest, basename='metadata_test')
...
All you need to do is instead of calling /api/metadata_test/ call /api/metadata_test/12345/
Where 12345 is the id/pk of the record. I am assuming that id is the primary key of the model.
In ListView, I can easily use def post(self, request) method to make a post request from a list view. But I want to make the post request from def get_queryset(self) which I am not yet able to do. When I try to do it it shows "method 405 not allowed!" even though post method is allowed through http_method_names.
How can I access POST request inside get_queryset function?
class ZonListView(SearchMixin, SingleTableMixin, ListView):
template_name = 'cadmin/list.html'
model = Zon
table_class = ZonTable
search_fields = {
'title': 'icontains',
'description': 'icontains',
}
def post(self, request): # ***** this one works! ******
try:
toggle_status = request.POST.get('toggle-status')
pk = int(request.POST.get('pk'))
....
return HttpResponseRedirect(reverse('cadmin:zon_list'))
def get_queryset(self):
qs = super(ZonListView, self).get_queryset()
if self.request.POST: #***** Not working. 405 Error *****#
try:
toggle_status = self.request.POST.get('toggle-status')
pk = int(self.request.POST.get('pk'))
......
if self.request.GET:
try:
status = self.request.GET.get('status')
qs = qs.filter(status=status)
except Exception:
pass
return qs.distinct()
def get_context_data(self, **kwargs):
....
To make method allowed you need to implement function named same as method, post in your case. So to use request.POST in get queryset you also need to define post() method like this:
def post(self, request): # ***** this method required! ******
self.object_list = self.get_queryset()
return HttpResponseRedirect(reverse('cadmin:zon_list'))
def get_queryset(self):
qs = super(ZonListView, self).get_queryset()
if self.request.POST: #***** Now allowed *****#
try:
toggle_status = self.request.POST.get('toggle-status')
pk = int(self.request.POST.get('pk'))
......
Look Django's View source to check how allowed methods defined.
I'm using a mixin on my viewset so that multiple serializers can be used accross different viewset actions and any custom actions.
I have an extra action called invoice which is just a normal update but using a different serializer. I need to perform an OPTIONS request at the endpoint to get options for a <select> element. The problem is that when I perform the request it's picking up the serializer from the default update - OrderSerializer instead of InvoiceSerializer. How can I pick up the options from the correct serializer?
class MultipleSerializerMixin:
"""
Mixin that allows for multiple serializers based on the view's
`serializer_action_classes` attribute.
ex.
serializer_action_classes = {
'list': ReadOnlyListSerializer,
'retrieve': ReadOnlyDetailSerializer,
}
"""
def get_serializer_class(self):
try:
return self.serializer_action_classes[self.action]
except (KeyError, AttributeError):
return super().get_serializer_class()
class OrderAPIViewSet(MultipleSerializerMixin,
viewsets.ModelViewSet):
queryset = Order.objects.all()
serializer_class = serializers.OrderSerializer
serializer_action_classes = {
'invoice': serializers.InvoiceSerializer,
}
#action(detail=True, methods=['put'], url_name='invoice')
def invoice(self, request, *args, **kwargs):
"""
Invoice the order and order lines.
"""
return self.update(request, *args, **kwargs)
Update:
So after inspecting the determine_actions method in metadata.SimpleMetadata it would seem that when performing an OPTIONS request view.action is metadata instead of invoice which explains why the serializer is defaulting to view.serializer_class.
One workaround is to create an extra action as a schema endpoint that could be accessed via a GET request that manually sets the action to invoice.
#action(detail=True, methods=['get', 'put'])
def invoice_schema(self, request, *args, **kwargs):
self.action = 'invoice'
data = self.metadata_class().determine_metadata(request, self)
return Response(data, status=status.HTTP_200_OK)
A more DRY solution if you have multiple actions that use different serializers would be to override the view's options method and set the action from the query parameters. This could be added to MultipleSerializerMixin to make it the default behaviour for all views that use this mixin.
def options(self, request, *args, **kwargs):
self.action = request.query_params.get('action')
return super().options(request, *args, **kwargs)
Override get_serializer_class method is enough and OPTIONS request will detect which serializer to use :
def get_serializer_class(self):
if self.request.method == 'GET':
return ReadOnlyShopSerializer
return ShopSerializer
I am trying to build an API view, to handle user management using django rest framework version 2.3.10 with django 1.6. I tried to build a ModelViewSet which based on the URL pk value it would return either current user or public user.
I tried to add a dispatch function which will assigned pk to current user, but it seems like this function is running too soon that its always seeing the user as anonymous
class UserViewSet(viewsets.ModelViewSet):
"""
"""
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsOwnerOrCreateOnly, )
def dispatch(self, request, *args, **kwargs):
if kwargs.get('pk') == 'current' and not request.user.is_anonymous():
kwargs['pk'] = request.user.pk
resp = super(CurrentUserViewSet, self).dispatch(request, *args, **kwargs)
return resp
I tried to do the below, which works
class UserViewSet(viewsets.ModelViewSet):
"""
"""
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsOwnerOrCreateOnly, )
def retrieve(self, request, *args, **kwargs):
if self.kwargs.get('pk') == u'current' and not request.user.is_anonymous():
self.kwargs['pk'] = request.user.pk
return super(CurrentUserViewSet, self).retrieve(request, *args, **kwargs)
but, I don't want to override each and every function on several ModelViewSet classes I have, so, is there a way to use something similar to the dispatcher whereby I can check if the pk is equal to "current" and then assign current user to it?
Another question, how can I change the returned fields programmatically? for example when querying current user I want to include the first and last name from the user model, but when querying by primary key, I want first and last name to not return as response? any suggestions on how todo that?
I got the same problem I solved it by using method "initial" instead of "dispatch"
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsOwnerOrCreateOnly, )
def initial(self, request, *args, **kwargs):
# logic - code #
if kwargs.get('pk') == 'current' and not request.user.is_anonymous():
kwargs['pk'] = request.user.pk
# end #
resp = super(CurrentUserViewSet, self).initial(request, *args, **kwargs)
return resp
see " dispatch "
method in https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/views.py
for better understanding.
Override viewsets.ModelViewSet class with your pk check implementation and use that new class, something like this:
class GenericUserViewSet(viewsets.ModelViewSet):
def retrieve(self, request, *args, **kwargs):
if self.kwargs.get('pk') == u'current' and not request.user.is_anonymous():
self.kwargs['pk'] = request.user.pk
return super(CurrentUserViewSet, self).retrieve(request, *args, **kwargs)
class UserViewSet(GenericUserViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (IsOwnerOrCreateOnly, )
And for the second question, perhaps creating two serializers (public and current) and changing serializer_class to either one of them in init of GenericUserViewSet may do the trick, I haven't tested this but it's an idea:
class GenericUserViewSet(viewsets.ModelViewSet):
def __init__(self, *args, **kwargs):
if self.kwargs.get('pk') == u'current' and not request.user.is_anonymous():
self.serializer_class = UserSerializer
else:
self.serializer_class = PublicUserSerializer
super(GenericUserViewSet, self).__init__(*args, **kwargs)
I'm assuming that you want to save the current user to your DB model, yes?
If so this should be fairly easy to fix, just add this method to your views:
def pre_save(self, obj):
obj.user = self.request.user
This will execute just before the model is saved. I use this all the time and it works great.
The other thing you can do is write a mixin class in a generic way that does want you want then inherit it in each of the views you need it in. Assuming that is that you have a solution that works, but just don't want to mimic you code all over the place.