Add dynamic attribute to serializer - django

I'd like to add a dynamic attribute to my serializers whenever they are called. Note that I'm talking about an attribute, not a field. Here's the idea:
User call the API
Viewset calls the serializer with the data
I manage to set self.my_key = my_dynamic_value
Checks and returns the serializer data (normal process)
To set my dynamic value, I need to use self.context meaning I can't override the __init__ method of the serializers (they are called on server start and context is empty)
Any idea on how I could do it?

Using another topic (Pass extra arguments to Serializer Class in Django Rest Framework), I managed to find a suitable solution.
I simply pass my dynamic value within the context by overriding the get_serializer_context method in the view, like that:
class MyViewSet(viewsets.GenericViewSet):
def get_serializer_context(self):
context = super().get_serializer_context()
context["my_key"] = my_value
return context
Then in my serializer, I can get it using self.context["my_key"]

Related

Getting fields from extra manager methods using django-rest-framework

I have the following custom model manager in Django that is meant to count the number of related comments and add them to the objects query set:
class PublicationManager(models.Manager):
def with_counts(self):
return self.annotate(
count_comments=Coalesce(models.Count('comment'), 0)
)
Adding this manager to the model does not automatically add the extra field in DRF. In my API view, I found a way to retrieve the count_comments field by overriding the get function such as:
class PublicationDetails(generics.RetrieveUpdateAPIView):
queryset = Publication.objects.with_counts()
...
def get(self, request, pk):
queryset = self.get_queryset()
serializer = self.serializer_class(queryset.get(id=pk))
data = {**serializer.data}
data['count_comments'] = queryset.get(id=pk).count_comments
return Response(data)
This works for a single instance, but when I try to apply this to a paginated list view using pagination_class, overriding the get method seems to remove pagination functionality (i.e. I get a list of results instead of the usual page object with previous, next, etc.). This leads me to believe I'm doing something wrong: should I be adding the custom manager's extra field to the serializer instead? I'm not sure how to proceed given that I'm using a model serializer. Should I be using a basic serializer?
Update
As it turns out, I was using the model manager all wrong. I didn't understand the idea of table-level functionality when what I really wanted was row-level functionality to count the number of comments related to a single instance. I am now using a custom get_paginated_response method with Comment.objects.filter(publication=publication).count().
Original answer
I ended up solving this problem by creating a custom pagination class and overriding the get_paginated_response method.
class PaginationPublication(pagination.PageNumberPagination):
def get_paginated_response(self, data):
for item in data:
publication = Publication.objects.with_counts().get(id=item['id'])
item['count_comments'] = publication.count_comments
return super().get_paginated_response(data)
Not sure it's the most efficient solution, but it works!

How to read query string values in ModelViewSet logic?

I need to get at query string values in some viewset logic (in this case, derived from ModelViewSet). Everything I've read, including the Django REST Framework doc, says that request is an attribute of the viewset. But when I actually try to refer to it in code, no matter how I do so, I am shown the runtime error 'AddressViewSet' object has no attribute 'request' . Here's one simplified version of the class definition that triggers the error:
class AddressViewSet(viewsets.ModelViewSet):
def __init__(self, suffix, basename, detail):
attendee = ""
if self.request.query_params.get('attendee'):
attendee = self.request.query_params.get('attendee')
self.serializer_class = AddressSerializer
self.queryset = Address.objects.all()
How does one read request properties in viewset logic in DRF?
You are overriding the incorrect method. The ViewSet like all class based views in Django (All DRF views inherit from django.views.generic.View down the line) is instantiated (The pattern usually seen as View.as_view() internally creates an instance of the class) before the request is even received. This instance is used in the url patterns and when a request matching the url pattern is found a dynamically created function is called for the view which then calls dispatch.
Coming back to the point __init__ is not the correct method to override, if you want to filter the queryset you should instead be overriding get_queryset:
class AddressViewSet(viewsets.ModelViewSet):
queryset = Address.objects.all()
serializer_class = AddressSerializer
def get_queryset(self):
queryset = super().get_queryset()
attendee = ""
if self.request.query_params.get('attendee'):
attendee = self.request.query_params.get('attendee')
# Filter queryset here
return queryset
What I did not understand is that you can override a function get_queryset() but if you do that you do not assign queryset directly - that happens automatically. Once that overridden function is called the request is available as a property of the viewset, so it can be referred to there.

How to pass a request.user to a model Manager?

I have a model manager with a get_queryset:
class BookManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(author=self.request.user
This results in the error:
AttributeError: 'BookManager' object has no attribute 'request`
I understand managers are "request-unaware". How would I make a method in your manager/queryset that takes a parameter for the request.user?
Short answer: The request object is only available in a view, so you can't assume it's available in your model manager.
Pass it from your view to a method in your manager that accepts it:
class BookManager(models.Manager):
def by_author(self, user):
return self.get_queryset().filter(author=user)
Then in your view you can do Book.objects.by_author(request.user). You'd need to make sure first that request.user is a logged in user otherwise you'll get an exception.
Note: In this particular case I wouldn't go through the trouble of defining by_author since it hardly makes your code more readable. But if there are more complex queries that you often need, then it might make sense to assign them a method in your manager to create DRY code.

How to serialize data not coming from the request and properly validate it (ModelSerializer in Django Rest Framework)?

Using Django Rest Framework 3, Function Based Views, and the ModelSerializer (more specifically the HyperlinkedModelSerializer).
When a user submits a form from the client, I have a view that takes the request data, uses it to call to an external API, then uses the data from the external API to populate data for a model serializer.
I believe I have this part working properly, and from what I read, you are supposed to use context and validate()
In my model serializer, I have so far just this one overidden function:
from django.core.validators import URLValidator
def validate(self, data):
if 'foo_url' in self.context:
data['foo_url'] = self.context['foo_url']
URLValidator(data['foo_url'])
if 'bar_url' in self.context:
data['bar_url'] = self.context['bar_url']
URLValidator(data['bar_url'])
return super(SomeSerializer, self).validate(data)
Just in case, the relevant view code is like so:
context = {'request': request}
...
context['foo_url'] = foo_url
context['bar_url'] = bar_url
s = SomeSerializer(data=request.data, context=context)
if s.is_valid():
s.save(user=request.user)
return Response(s.data, status=status.HTTP_201_CREATED)
Now assuming I have the right idea going (my model does populate its foo_url and bar_url fields from the corresponding context data), where I get confused is how the validation is not working. If I give it bad data, the model serializer does not reject it.
I assumed that in validate(), by adding the context data to the data, the data would be checked for validity when is_valid() was called. Maybe not the case, especially when I print out s (after using the serializer but before calling is_valid()) there is no indication that the request object's data has been populated with the context data from validate() (I don't know if it should be).
So I tried calling the URLValidators directly in the validate() method, but still doesn't seem to be working. No errors despite giving it invalid data like 'asdf' or an empty python dict ({}). My test assertions show that the field indeed contains invalid data like '{}'.
What would be the proper way to do this?
You're not calling the validator.
By doing URLValidator(data['bar_url']) you're actually building an url validator with custom schemes (see the docs) and that's it. The proper code should be:
URLValidator()(data['bar_url'])
Where you build a default url validator and then validate the value.
But anyway I would not use this approach, what I would do instead is directly add the extra data (not using the context) and let DRF do the validation by declaring the right fields:
# Somewhere in your view
request.data['bar_url'] = 'some_url'
# In serializer:
class MySerializer(serializers.ModelSerializer):
bar_url = serializers.URLField()
class Meta:
fields = ('bar_url', ...)
To answer your comment
I also don't understand how this also manages to make it past the
Django's model validation
See this answer:
Why doesn't django's model.save() call full_clean()?
By default Django does not automatically call the .full_clean method so you can save a model instance with invalid values (unless the constraints are on the database level).

Update serializer if queryset exists

If the queryset exists, I would like to update the existing instance instead of creating a brand new one.
The way I currently have it raises an error saying unexpected kwargs are being passed if the serializer is trying to update.
Could someone please help me do this?
class APNSDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
queryset = APNSDevice.objects.all()
serializer_class = APNSDeviceSerializer
def perform_create(self, serializer):
queryset = APNSDevice.objects.filter(user=self.request.user)
if queryset.exists():
# update serializer to contain new information
serializer.update(
device_type=self.request.data.get('device_type'),
registration_id=self.request.data.get('registration_id'),
device_id=self.request.data.get('device_id'))
else:
# create a new model instance
serializer.save(
user=self.request.user,
device_type=self.request.data.get('device_type'),
registration_id=self.request.data.get('registration_id'),
device_id=self.request.data.get('device_id'))
Thank you!
Peering into the Django Rest Framework code, specifically serializers.py, it's clear you're providing incorrect parameters for the update() method. Its signature is not like the save() method's signature. It expects as a first argument the model instance, and as a second argument a dictionary containing the data. So in your case it would look something like this:
instance = queryset[0]
serializer.update(instance, {'device_type': self.request.data.get('device_type'), 'registration_id': self.request.data.get('registration_id'), 'device_id': self.request.data.get('device_id')})
All you want is a base queryset that is restricted to the current user.
This is performed by the filters in an elegant and simple way.
On a side note you should not invoke serializer.update by yourself. It's called by the serializer through the serializer.save method. The difference between update et create is that the serializer is instantiated with a Model's instance for updates.