DRF CreateModelMixin with additional fields - django

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)

Related

serializer_action_class not working in modelviewset in DRF

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.

How to create multiple instances in DRF?

I have a list of data coming in a request, and after filtering taken out data that needs creation, I'm passing it to the serializer create method, but I'm getting this error:
AssertionError: The `.create()` method does not support writable nested fields by default.
Write an explicit `.create()` method for serializer `apps.some_app.serializers.SomeSerializer`, or set `read_only=True` on nested serializer fields.
My view looks like this:
class MyViewSet(ModelViewSet):
# Other functions in ModelViewset
#action(methods=['post'], url_path='publish', url_name='publish', detail=False)
def publish_data(self, request, *args, **kwargs):
new_data = util_to_filter_data(request.data)
serializer = self.serializer_class(data=new_data, many=True)
if serializer.is_valid(raise_exception=True):
serializer.create(serializer.validated_data)
return Response()
I understood the error, that I am passing nested fileds in the create method. But, when I am directly calling my Viewset with single POST request, it is created successfully, even though it too contains nested fields.
What am I doing wrong here?
Here's my serializer:
class SomeSerializer(ModelSerializer):
# fields
def create(self, validated_data):
print("in create")
return super().create(validated_data)
def save(self, **kwargs):
print("in save")
my_nested_field = self.validated_data.pop('my_nested_field', '')
# Do some operations on this field, and other nested fields
obj = None
with transaction.atomic():
obj = super().save(kwargs)
# Save nested fields
return obj
Here in create is being seen in terminal, but not in save.
you should not use serializer create method directly; instead use save. Also no need to check serializer.is_valid with if when raise_exception=True, if it is not valid it will return the exception
Try following.
class MyViewSet(ModelViewSet):
# Other functions in ModelViewset
#action(methods=['post'], url_path='publish', url_name='publish', detail=False)
def publish_data(self, request, *args, **kwargs):
new_data = util_to_filter_data(request.data)
serializer = self.serializer_class(data=new_data, many=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)

DRF options request on viewset with multiple serializers

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

Pass request context to serializer from Viewset in Django Rest Framework

I have a case where the values for a serializer field depend on the identity of the currently logged in user. I have seen how to add the user to the context when initializing a serializer, but I am not sure how to do this when using a ViewSet, as you only supply the serializer class and not the actual serializer instance.
Basically I would like to know how to go from:
class myModelViewSet(ModelViewSet):
queryset = myModel.objects.all()
permission_classes = [DjangoModelPermissions]
serializer_class = myModelSerializer
to:
class myModelSerializer(serializers.ModelSerializer):
uploaded_by = serializers.SerializerMethodField()
special_field = serializers.SerializerMethodField()
class Meta:
model = myModel
def get_special_field(self, obj):
if self.context['request'].user.has_perm('something.add_something'):
return something
Sorry if it wasn't clear, from the DOCs:
Adding Extra Context
Which says to do
serializer = AccountSerializer(account, context={'request': request})
serializer.data
But I am not sure how to do that automatically from the viewset, as I only can change the serializer class, and not the serializer instance itself.
GenericViewSet has the get_serializer_context method which will let you update context:
class MyModelViewSet(ModelViewSet):
queryset = MyModel.objects.all()
permission_classes = [DjangoModelPermissions]
serializer_class = MyModelSerializer
def get_serializer_context(self):
context = super().get_serializer_context()
context.update({"request": self.request})
return context
For Python 2.7, use context = super(MyModelViewSet, self).get_serializer_context()
For Function based views you can pass request or user as follows:
serializer = ProductSerializer(context = {"request": request}, data=request.data)
Your Serializer may look like:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["id"]
def create(self, validated_data):
user = self.context["request"].user
print(f"User is: {user}")
Feel free to inform if there is any better way to do this.
just use get_serializer() in your viewsets
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
Return parent context in overrided function get_serializer_context will make it easy to access request and its data.
class myModelViewSet(ModelViewSet):
queryset = myModel.objects.all()
permission_classes = [DjangoModelPermissions]
serializer_class = myModelSerializer
def get_serializer_context(self):
"""
pass request attribute to serializer
"""
context = super(myModelViewSet, self).get_serializer_context()
return context
This is very stable as every time we request viewset, it returns context as well.
the values for a serializer field depend on the identity of the currently logged in user
This is how I handle such cases in my ModelViewSet:
def perform_create(self, serializer):
user = self.request.user
if user.username == 'myuser':
serializer.data['myfield'] = 'something'
serializer.save()
Simply add this 2 line method in your class and you are good to go.
def get_serializer_context(self):
return {'request': self.request}
since the posted answers had partial correctness, summarizing here in the interest of completeness.
override get_serializer_context..AND
use get_serializer in your views instead of manually calling the serializer

Return current user on details

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.