I need to select a serializer based on the value of an object instance. How do I pass an additional argument to get_serializer_class to be able to do the validation there?
def get_serializer_class(self, extra_option):
if extra_option:
return ModelSerializer
return ModelSerializer2
serializer = self.serializer_class(data=request.data, extra_option=smth_instance)
Error:
TypeError: Field.__init__() got an unexpected keyword argument 'extra_option'
If I make a custom class then serializer selection works... maybe this is legit workaround?
def choose_serializer(self, product, data):
if product.document.type == 1:
return Serializer1(product, data=data)
elif product.document.type == 2:
return Serializer2(product, data=data)
serializer = self.choose_serializer(data=request.data, product=product)
It's much easier to check the action to decide what serializer to use. Ex.
def get_serializer_class(self):
if self.action == "retrieve":
return ModelOutputSerializer
else:
return ModelDefaultSerializer
Otherwise you likely need to override the action you want the different kwarg on and pass that down, overriding the necessary methods as you go. Ex.
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance, extra_option=...)
return Response(serializer.data)
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class(*args, **kwargs)
kwargs.setdefault('context', self.get_serializer_context())
return serializer_class(*args, **kwargs)
def get_serializer_class(self, *args, **kwargs):
if kwargs.pop("extra_option", None):
return ExtraOptionSerializer
else:
return super().get_serializer_class()
Also check out classy DRF for reference: https://www.cdrf.co/3.13/rest_framework.viewsets/ModelViewSet.html#retrieve.
Edit: also self.serializer_class defaults to whatever you define on the view set so you likely want to instead call self.get_serializer_class(...) like you did with your custom method above.
Related
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'm trying to get a copy of the model created by super().create() in my ModelViewSet's create() method. What's the simplest way to do that?
I have a ModelViewSet that takes a generic POST request, which I know is good because I end up with a new record in my DB. What I want to do is get the object that was just created so I can return its pk to the client. However, the following will not work:
class ItemViewSet(viewsets.ModelViewSet):
model = Item
# ...
def create(self, request, *args, **kwargs):
super().create(request, *args, **kwargs) # Successfully creates instance
instance = self.get_object() # Throws error
return Response({'status': 'success', 'pk': instance.pk})
Like with other DRF ModelViewSet methods, I'd expect self.get_object() to get me the instance created, though this is usually only usable in a "detail route". What I get instead is the following error:
AssertionError: Expected view CultivarStockViewSet to be called with a URL keyword argument named "pk". Fix your URL conf, or set the `.lookup_field` attribute on the view correctly.
Any insight is appreciated!
With a little further debugging, I figured it out. For posterity's sake, here's my working create() method:
def create(self, request, *args, **kwargs):
response = super().create(request, *args, **kwargs)
instance = response.data
return Response({'status': 'success', 'pk': instance['pk']})
You can indeed not obtain the object by using get_object [classy-doc], since Django uses parameters to perform a filtering. Indeed, the get_object implementation looks like:
def get_object(self):
# ...
queryset = self.filter_queryset(self.get_queryset())
# Perform the lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
# ...
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
these self.kwargs are not available, so the get_object call fails.
We can however patch the create [classy-doc] function and use the serializer.instance [drf-doc] here:
class ItemViewSet(viewsets.ModelViewSet):
model = Item
# ...
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
return Response({'status': 'success', 'pk': serializer.instance.pk})
I know I am answering this question very late. But if anyone stuck on this kind of problem, The answer provided by #JohnnyHammersticks works well. But the problem is, this solution will create the object two times (For my case, I have the perform_create function as well). The best way to handle this situation is as follows,
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
if serializer.is_valid():
instance = serializer.data
pk = data['id']
return Response({'status': 'success', 'pk': instance['pk']})
Which method should be overridden to add additional checks and redirect accordingly?
i.e. I've a DetailView for my product page, and if this product is not published (and brand has more products) I want to redirect to the brand page.
I added this check to get method and I'm calling get_object() manually and then doing my checks, but in the end I'm also calling the super().get() which calls get_object() as well, this makes the SQL run twice.
The solution I've found is overriding the get_object() method as following..
def get_object(self, queryset=None):
if not hasattr(self, 'object') or not self.object:
self.object = super().get_object(queryset=queryset)
return self.object
This doesn't feel right though, what is the best way to do checks without triggering get_object twice?
My code that calls get_object twice looks like this: without the hack above.
def get(self, request, *args, **kwargs):
product = self.get_object()
if not product.published:
if product.brand and #more products from brand exists#
return redirect(reverse('brand',
args=(product.brand.slug,)))
else:
return redirect(reverse('pages:home'))
return super().get(request, *args, **kwargs)
just for reference super().get looks like this, and I don't want to rewrite these lines.
https://ccbv.co.uk/projects/Django/1.10/django.views.generic.detail/DetailView/#get
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
I think this is cleaner - store the response of the super().get(...) call, which will also populate self.object, then redirect if necessary, or return the response stored from the super().get(...) call:
def get(self, request, *args, **kwargs):
super_response = super().get(request, *args, **kwargs)
if not self.object.published:
if self.object.brand and #more products from brand exists#
return redirect(reverse('brand',
args=(self.object.brand.slug,)))
else:
return redirect(reverse('pages:home'))
return super_response
Note that this does have the overhead of creating a valid response for an unpublished object. To avoid that, simply avoid the super call - yes, this means duplicating two lines of code from the superclass' method:
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.published:
if self.object.brand and #more products from brand exists#
return redirect(reverse('brand',
args=(self.object.brand.slug,)))
else:
return redirect(reverse('pages:home'))
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
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.
I'm trying to initialize the form attribute for MyModelAdmin class inside an instance method, as follows:
class MyModelAdmin(admin.ModelAdmin):
def queryset(self, request):
MyModelAdmin.form = MyModelForm(request.user)
My goal is to customize the editing form of MyModelForm based on the current session. When I try this however, I keep getting an error (shown below). Is this the proper place to pass session data to ModelForm? If so, then what may be causing this error?
TypeError at ...
Exception Type: TypeError
Exception Value: issubclass() arg 1 must be a class
Exception Location: /usr/lib/pymodules/python2.6/django/forms/models.py in new, line 185
Combining the good ideas in Izz ad-Din Ruhulessin's answer and the suggestion by Cikić Nenad, I ended up with a very awesome AND concise solution below:
class CustomModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
self.form.request = request #so we can filter based on logged in user for example
return super(CustomModelAdmin, self).get_form(request,**kwargs)
Then just set a custom form for the modeladmin like:
form = CustomAdminForm
And in the custom modelform class access request like:
self.request #do something with the request affiliated with the form
Theoretically, you can override the ModelAdmin's get_form method:
# In django.contrib.admin.options.py
def get_form(self, request, obj=None, **kwargs):
"""
Returns a Form class for use in the admin add view. This is used by
add_view and change_view.
"""
if self.declared_fieldsets:
fields = flatten_fieldsets(self.declared_fieldsets)
else:
fields = None
if self.exclude is None:
exclude = []
else:
exclude = list(self.exclude)
exclude.extend(kwargs.get("exclude", []))
exclude.extend(self.get_readonly_fields(request, obj))
# if exclude is an empty list we pass None to be consistant with the
# default on modelform_factory
exclude = exclude or None
defaults = {
"form": self.form,
"fields": fields,
"exclude": exclude,
"formfield_callback": curry(self.formfield_for_dbfield, request=request),
}
defaults.update(kwargs)
return modelform_factory(self.model, **defaults)
Note that this returns a form class and not a form instance.
If some newbie, as myself, passes here:
I had to define:
class XForm(forms.ModelForm):
request=None
then at the end of the previous post
mfc=modelform_factory(self.model, **defaults)
self.form.request=request #THE IMPORTANT statement
return mfc
i use queryset fot filtering records, maybe this example help you:
.....
.....
def queryset(self, request):
cuser = User.objects.get(username=request.user)
qs = self.model._default_manager.get_query_set()
ordering = self.ordering or () # otherwise we might try to *None, which is bad ;)
if ordering:
qs = qs.order_by(*ordering)
qs = qs.filter(creator=cuser.id)
return qs
Here is a production/thread-safe variation from nemesisfixx solution:
def get_form(self, request, obj=None, **kwargs):
class NewForm(self.form):
request = request
return super(UserAdmin, self).get_form(request, form=NewForm, **kwargs)
class CustomModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
get_form = super(CustomModelAdmin, self).get_form(request,**kwargs)
get_form.form.request = request
return get_form
Now in ModelForm, we can access it by
self.request
Example:
class CustomModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(TollConfigInlineForm, self).__init__(*args, **kwargs)
request = self.request
user = request.user