DRF nested serialization without raw sql query - django

I've stuck with this problem for few days now. Tried different approaches but without success. I have two classes - Poll and PollAnswer. Here they are:
class Poll(Model):
title = CharField(max_length=256)
class PollAnswer(Model):
user_id = CharField(max_length=10)
poll = ForeignKey(Poll, on_delete=CASCADE)
text = CharField(max_length=256)
what is the right way to get list of polls which have answers with used_id equal to the certain string with nested list of that user's answers? like this:
{
'poll_id': 1,
'answers' : {
'user1_answer1: 'answer_text1',
'user1_answer2: 'answer_text2',
'user1_answer3: 'answer_text3',
},
}
and if it's the simple question i probably need some good guides on django orm.
the first thing i tried was to make serializer's method (inherited from drf's ModelSerializer) but got an error that this class can't have such method. after that i tried to use serializer's field with ForeignKey but got polls nested in answers instead. right now i believe i can make Polls.objects.raw('some_sql_query') but that's probably not the best way.

Your problem is described in documentation (also best practise).
You can use nested serializer:
https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers
otherwise if u want to keep nested answers as you described:
i would use serializer method field
https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield
then do a little loop over your answers ... and return whatever format you want to.

well, thanks for help the final code is:
serializer:
class UserPollsSerializer(ModelSerializer):
answers = SerializerMethodField()
# drf automatically finds this method by mask get_{field}
def get_answers(self, obj):
answers = self.context.get('answers_qs').filter(poll=obj)
data = QuestionAnswerSerializer(answers, many=True).data
return data
class Meta:
model = Poll
fields = '__all__'
view:
class PollMVS(ModelViewSet):
serializer_class = PollSerializer
permission_classes = [IsAdminUser]
queryset = Poll.objects.all()
# getting polls with answers by user_id
#action(detail=False, methods=['get'])
def user_polls(self, request):
# here i get all polls with user's answers
user_id = request.GET.get('user_id')
polls_ids = QuestionAnswer.objects.values('poll').filter(user_id=user_id).distinct()
u_polls = Poll.objects.filter(pk__in=polls_ids)
# and here i pass them as a queryset and answers' queryset as context in a separate serializer
serializer = UserPollsSerializer(u_polls, many=True, context={'answers_qs': QuestionAnswer.objects.filter(user_id=user_id)})
return Response(serializer.data)

Related

Get n number of random records using nested serializers Django REST framework

I'm trying to get random 'n' number of records from foreign key related models. Suppose I have two models named Exam and Questions. I need an API endpoint to get n number of questions for one particular subject(ex. for math, n random math questions). The endpoint is working well in retrieving all the questions for a particular subject.
models.py
class Exam(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Question(models.Model):
exam = models.ForeignKey(Exam, on_delete=models.CASCADE)
question = models.CharField(max_length=255)
def __str__(self):
return '{} - {}'.format(self.question)
serializers.py
class QuestionSerializer(serializers.ModelSerializer):
questions = serializers.CharField(read_only=True)
answer = serializers.CharField(read_only=True)
class Meta:
model = Question
fields = '__all__'
class ExamSerializer(serializers.ModelSerializer):
name = serializers.CharField(read_only=True)
questions = QuestionSerializer(many=True, read_only=True, source='question_set')
class Meta:
model = Exam
fields = '__all__'
api_views.py
class ExamQuestionRetrieveAPIView(generics.RetrieveAPIView):
authentication_classes = [JWTTokenUserAuthentication]
serializer_class = ExamSerializer
queryset = Exam.objects.all()
lookup_field = 'name'
After going through the doc, I tried to filter and get random records using the to_representation() method but failed. Any help is greatly appreciated.
If you want N random questions of 1 exam, I would do the following:
Create a custom action in a Viewset (or a custom view)
It should be a DETAIL model action, meaning it looks like exams/3/your-action-name/
It should be a GET request
Then implement the following logic:
Fetch the exam model
Then fetch the Questions for that exam using "?" to order them randomly and only take a few
Then serialize the question instances
And return the data
Here's what it could look like:
def get_random_questions(self, request, pk=None):
exam = self.get_object()
questions = Question.objects.filter(exam=exam).order_by("?")[:5] # Update with your desired number
serializer = QuestionSerializer(questions, many=True)
return Reponse(serializer.data)
Finally found a way without changing the implementation, the previous attempt to use to_representation() was done in the wrong serializer. Anyway able to return 'n' random records by using np.random.choice() here's how I did it. In Question serializer,
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['questions'] = np.random.choice(
representation['questions'],
10, # n(=10) number of records
replace=False
)
return representation
But, using the to_representation() means all the question objects will be loaded and then from that list, np.random.choice() will get n number of random records. Which is not so cool.
So it seemed that the #Jordan answer was much more convenient so I used APIView instead of generics.RetrieveAPIView and without using nested serialization.
class ExamQuestionAPIView(APIView):
def get_object(self, name):
try:
return Quiz.objects.get(name=name)
except Quiz.DoesNotExist:
raise Http404
def get(self, request, name, format=None):
"""
Return 10 random questions.
"""
questions = QuizQuestion.objects.filter(
quiz=self.get_object(name)
).order_by('?')[:10]
questions_serialized = QuizQuestionSerializer(questions, many=True)
return Response(questions_serialized.data)

Sort DRF serializer output of nested Serializer field by child's field

I have two serializers, in which one refers to the other with a many=True relationship.
class AttributeInParentSerializer(ModelSerializer):
masterdata_type = CharField(max_length=256, source='masterdata_type_id')
class Meta:
model = Attribute
fields = ('uuid', 'masterdata_type')
class ArticleInArticleSetSerializer(ModelSerializer):
attributes = AttributeInParentSerializer(many=True)
class Meta:
model = Article
fields = ('uuid', 'attributes')
The ordering of the attributes in the Article are not always the same, but I want to output them in the same order, so in this case ordering on the field masterdata_type. How can I accomplish this? Note that I do not want to change any client of the serializer if possible, and surely not any model.
Old thread, but because it's still popping up on Google I want to share my answer as well. Try overwriting the Serializer.to_representation method. Now you can basically do whatever you want, including customising the sorting of your response. In your case:
class ArticleInArticleSetSerializer(ModelSerializer):
attributes = AttributeInParentSerializer(many=True)
class Meta:
model = Article
fields = ('uuid', 'attributes')
def to_representation(self, instance):
response = super().to_representation(instance)
response["attributes"] = sorted(response["attributes"], key=lambda x: x["masterdata_type"])
return response
you can set the ordering of attributes on ArticleInArticleSetSerializer by writing viewset for ArticleInArticleSetSerializer.
class ArticleInArticleSetViewSet(viewsets.ModelViewSet):
serializer_class = ArticleInArticleSetSerializer
queryset = Article.objects.all().order_by('-attributes_id')
Or you can write a function for listing.
def list(self, request):
self.queryset = self.get_queryset().order_by('-id')
return super(yourViewSet, self).list(self, request)
This code only for reference
I found an answer I prefer over rewriting the to_representation method or doing an inline call to the sorted method (which loads all the instances in memory):
class ArticleInArticleSetSerializer(ModelSerializer):
attributes = serializers.SerializerMethodField(method_name='get_attributes_sorted')
#staticmethod
def get_attributes_sorted(instance):
attributes = instance.attributes.order_by('masterdata_type')
return AttributeInParentSerializer(attributes, many=True).data
# Other stuff ...
This way you use a pure ORM solution (that's translated to SQL). The best performance and no memory consumption during sorting.
You can't set the ordering on ArticleInArticleSetSerializer but you can set the ordering of attributes on AttributeInParentSerializer. This is because you can only set the ordering when you are consuming a serializer rather than when you're defining one.
You could perhaps set it in the __init__ method when the queryset or data is passed in, but then you're making assumptions on what is being passed in. I'd probably end up specifying it in the consumers of ArticleInArticleSetSerializer to avoid any future problems with passing in a list to the serializer.
You can use order_by inside of your serializer like this:
class AttributeInParentSerializer(ModelSerializer):
masterdata_type = CharField(max_length=256, source='masterdata_type_id')
class Meta:
model = Attribute
fields = ('uuid', 'masterdata_type')
order_by = (('masterdata_type',))
Hope it helps!
UPDATE:
Looks like I am mistaken. Could not find it from the documentation and it does not seem to work. Now, I don't think serializers are the place to do ordering. Best way to do is in the model or in the view.

Advance serialization in DRF

I'm trying to achieve something with Django Rest Framework.
The idea is for a model to have several fields of several types in read-only, and have the same fields writable for the user that would take precedence when serving the data.
Since this should not be very clear, an example :
The model would be :
class Site(models.Model):
title = models.CharField(_('Title'),max_length=300)
title_modified = models.CharField(_('Title'),max_length=300)
The viewset to be defined :
class SiteViewSet(viewsets.ModelViewSet):
serializer_class = SiteSerializer
queryset = Site.objects.all()
The serializer :
class SiteSerializer(serializers.ModelSerializer):
class Meta:
model = Site
depth = 1
What i want to do is be able to only serve the "title" data to the client, but the title would have either the title field if title_modified is empty or the title_modified field if it's not empty.
On the same idea when the client writes to the title i would like my server to write the data to title_modified instead and always leave the title info untouched.
I don't know how to achieve this a way that's generic enough to be applicable to all types of fields.
I thought it would simply require some magic on the serialization/unserialization but i can't seem to find it.
Any idea would be appreciated.
Thanks.
Since you are using ModelViewSets, you can override the default actions like .list(), .retrieve(), .create(), etc to do what you want or create your custom actions. Relevant info for ModelViewSets can be found here and here.
Actually, there are plenty of ways to go about this, and you do not even need to use ModelViewSet. You can actually use the generic views for this one. The real trick is to leverage the power of the CBVs and OOP in general. Here is a sample code wherein you provide a custom retrieval process of a single instance, while retaining all the rest's out-of-the-box behavior that a ModelViewSet provides.
class SiteViewSet(viewsets.ModelViewSet):
serializer_class = SiteSerializer
queryset = Site.objects.all()
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
# You can use the serializer_class specified above (SiteSerializer)
serializer = self.get_serializer(instance)
# Or perform some other manipulation on your instance first,
# then use a totally different serializer for your needs
instance = data_manipulation(instance)
serializer = AnotherSiteSerializer(instance)
# Finally return serialized data
return Response(serializer.data)
# Or if you want, return random gibberish.
return Response({'hello': 'world'})
I think you can override the to_representation() method of serializer to solve your problem:
class SiteSerializer(serializers.ModelSerializer):
class Meta:
model = Site
depth = 1
exclude = ('title')
def to_representation(self, instance):
rep = super(SiteSerializer, self).to_representation(instance)
if not rep.get('title_modified', ''):
rep['title_modified'] = instance.title
return rep
This will return title as title_modified if title_modified is empty. User will always work on title_modified as required.
For more details please read modelserializer and Advanced serializer usage.

How to represent `self` url in django-rest-framework

I want to add a link to a single resource representation which is an URL to itself, self. Like (taken from documentation):
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing')
{
'album_name': 'The Eraser',
'artist': 'Thom Yorke',
'self': 'http://www.example.com/api/album/2/',
}
How should this be done?
If you inherit serializers.HyperlinkedModelSerializer all you need to do is pass a url field to fields. See the docs here:
http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/
Alright, this solved my problem but if you have a better solution please post an answer:
from django.urls import reverse
from rest_framework import serializers
self_url = serializers.SerializerMethodField('get_self')
def get_self(self, obj):
request = self.context['request']
return reverse('album-detail', kwargs={'id': obj.id}, request=request)
here is my solution,
in your view methods create serilizer object like this:
album = AlbumSerializer(data=data, {"request":request})
in your serilizer class override to_representation method (you can read about this method on DRF docs
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
def to_representation(self, obj):
data = super().to_representation(obj)
request = self.context["request"]
return data
According to this issue, you can just add 'url' in the list of fields.
Here is a little more context than you got in the other answers so far. The key is the context argument passed to the serializer constructor and the 'url' in fields.
http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/
In your viewset:
class AlbumViewSet(viewsets.ViewSet):
def list(self, request):
queryset = Album.objects.all()
serializer = AlbumSerializer(queryset, many=True,
context={'request': request})
return Response(serializer.data)
In the serializer:
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing', 'url')
As stated above the HyperlinkedModelSerializer will convert all your related fields and remove the ID of the resource as well. I use one of the following solutions, depending on the situation.
Solution 1: import api settings and add the url field:
For more details see URL_FIELD_NAME.
from rest_framework.settings import api_settings
class AlbumSerializer(SelfFieldMixin, serializers.ModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing', api_settings.URL_FIELD_NAME)
Solution 2: a simple mixin, which only works if default fields are used:
class SelfFieldMixin:
"""
Adds the self link without converting all relations to HyperlinkedRelatedField
"""
def get_default_field_names(self, declared_fields, model_info):
"""
Return the default list of field names that will be used if the
`Meta.fields` option is not specified.
"""
default_fields = super().get_default_field_names(declared_fields, model_info)
return [self.url_field_name, *default_fields]
And it can be used like
class AlbumSerializer(SelfFieldMixin, serializers.ModelSerializer):
class Meta:
model = Album
fields = '__all__'
NOTE: It requires Python 3 due to the super() call, the a mixin must be placed before any of the serializer classes!
P.S.: To achieve the required response in the question one must also set the URL_FIELD_NAME to 'self'.
Edit: get_default_field_names must return a list object for Meta.exclude to work on ModelSerializers.
You can use the HyperlinkedIdentityField like so:
class ThingSerializer(ModelSerializer):
class Meta:
model = Thing
fields = ['self_link', ...]
self_link = HyperlinkedIdentityField(view_name='thing-detail')
You need to have your routes named appropriately but the Default routers do so automatically (documented here).
As others have pointed out the HyperlinkedModelSerializer also works. This is because it uses this field automatically. See here.

How can I apply a filter to a nested resource in Django REST framework?

In my app I have the following models:
class Zone(models.Model):
name = models.SlugField()
class ZonePermission(models.Model):
zone = models.ForeignKey('Zone')
user = models.ForeignKey(User)
is_administrator = models.BooleanField()
is_active = models.BooleanField()
I am using Django REST framework to create a resource that returns zone details plus a nested resource showing the authenticated user's permissions for that zone. The output should be something like this:
{
"name": "test",
"current_user_zone_permission": {
"is_administrator": true,
"is_active": true
}
}
I've created serializers like so:
class ZonePermissionSerializer(serializers.ModelSerializer):
class Meta:
model = ZonePermission
fields = ('is_administrator', 'is_active')
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
current_user_zone_permission = ZonePermissionSerializer(source='zonepermission_set')
class Meta:
model = Zone
fields = ('name', 'current_user_zone_permission')
The problem with this is that when I request a particular zone, the nested resource returns the ZonePermission records for all the users with permissions for that zone. Is there any way of applying a filter on request.user to the nested resource?
BTW I don't want to use a HyperlinkedIdentityField for this (to minimise http requests).
Solution
This is the solution I implemented based on the answer below. I added the following code to my serializer class:
current_user_zone_permission = serializers.SerializerMethodField('get_user_zone_permission')
def get_user_zone_permission(self, obj):
user = self.context['request'].user
zone_permission = ZonePermission.objects.get(zone=obj, user=user)
serializer = ZonePermissionSerializer(zone_permission)
return serializer.data
Thanks very much for the solution!
I'm faced with the same scenario. The best solution that I've found is to use a SerializerMethodField and have that method query and return the desired values. You can have access to request.user in that method through self.context['request'].user.
Still, this seems like a bit of a hack. I'm fairly new to DRF, so maybe someone with more experience can chime in.
You have to use filter instead of get, otherwise if multiple record return you will get Exception.
current_user_zone_permission = serializers.SerializerMethodField('get_user_zone_permission')
def get_user_zone_permission(self, obj):
user = self.context['request'].user
zone_permission = ZonePermission.objects.filter(zone=obj, user=user)
serializer = ZonePermissionSerializer(zone_permission,many=True)
return serializer.data
Now you can subclass the ListSerializer, using the method I described here: https://stackoverflow.com/a/28354281/3246023
You can subclass the ListSerializer and overwrite the to_representation method.
By default the to_representation method calls data.all() on the nested queryset. So you effectively need to make data = data.filter(**your_filters) before the method is called. Then you need to add your subclassed ListSerializer as the list_serializer_class on the meta of the nested serializer.
subclass ListSerializer, overwriting to_representation and then calling super
add subclassed ListSerializer as the meta list_serializer_class on the nested Serializer
If you're using the QuerySet / filter in multiple places, you could use a getter function on your model, and then even drop the 'source' kwarg for the Serializer / Field. DRF automatically calls functions/callables if it finds them when using it's get_attribute function.
class Zone(models.Model):
name = models.SlugField()
def current_user_zone_permission(self):
return ZonePermission.objects.get(zone=self, user=user)
I like this method because it keeps your API consistent under the hood with the api over HTTP.
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
current_user_zone_permission = ZonePermissionSerializer()
class Meta:
model = Zone
fields = ('name', 'current_user_zone_permission')
Hopefully this helps some people!
Note: The names don't need to match, you can still use the source kwarg if you need/want to.
Edit: I just realised that the function on the model doesn't have access to the user or the request. So perhaps a custom model field / ListSerializer would be more suited to this task.
I would do it in one of two ways.
1) Either do it through prefetch in your view:
serializer = ZoneSerializer(Zone.objects.prefetch_related(
Prefetch('zone_permission_set',
queryset=ZonePermission.objects.filter(user=request.user),
to_attr='current_user_zone_permission'))
.get(id=pk))
2) Or do it though the .to_representation:
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Zone
fields = ('name',)
def to_representation(self, obj):
data = super(ZoneSerializer, self).to_representation(obj)
data['current_user_zone_permission'] = ZonePermissionSerializer(ZonePermission.objects.filter(zone=obj, user=self.context['request'].user)).data
return data