I want to display all objects of a particular model, and it's related objects ( object-wise ).
Is there a way to change/customize the JSON output? (In terms of the key names, values, or the nesting?)
views.py
#api_view(['GET'])
def api(request):
q = Question.objects.all()
s = QuestionSerializer(q, many=True)
print(ChoiceSerializer())
return Response(s.data)
serializers.py
class QuestionSerializer(serializers.ModelSerializer):
choices = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Question
fields = '__all__'
models.py
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField(auto_now_add=True, editable=False, name='pub_date')
def __str__(self):
return self.question_text
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='choices')
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
def __str__(self):
return self.choice_text
def related_question(self):
q = self.question.question_text
return q
Output
HTTP 200 OK
Allow: OPTIONS, GET
Content-Type: application/json
Vary: Accept
[
{
"id": 1,
"choices": [
1,
2,
3
],
"question_text": "Question 1",
"pub_date": "2020-12-19T23:07:25.071171+05:30"
},
{
"id": 2,
"choices": [
4,
5
],
"question_text": "Question 2",
"pub_date": "2020-12-21T13:58:37.595672+05:30"
}
]
In the output, choices shows the pk of the objects.
How can I change that to show, say, the choice_text or a custom string?
Also, can the way the objects are nested be customized?
EDIT: When I asked if there is a way to exercise more control, I meant control over the depth of JSON response.
You can use serializer's method field to replace choices key with any related model fields that you want. For instance
class QuestionSerializer(serializers.ModelSerializer):
choices = serializers.SerializerMethodField('get_choices')
class Meta:
model = Question
fields = '__all__'
def get_choices(self, obj):
return obj.choices.values('choice_text').distinct() # or values('id', 'choice_text') based on your requirements
Note that values are a better way than iterating through all objects as a performance point of view (like [choice.choice_text for choice in question.choices.all()], etc).
EDIT: SerializerMethodField is used when we want to run some custom logic to populate a particular field.
When using ModelSerializer, the source keyword can be used to access fields (related, non related both) or even rename fields in the case where the person interacting with the endpoint requires a different name.
Example for renaming(using models mentioned in the question)
class QuestionSerializer(serializers.ModelSerializer):
poll_question = serializers.CharField(source='question_text')
class Meta:
model = Question
fields = '__all__'
Example for getting a FK related field:
class ChoiceSerializer(serializers.ModelSerializer):
poll_question = serializers.CharField(source='question.question_text')
class Meta:
model = Choice
fields = '__all__'
This is a very good foundational read for understanding how and when to use serializers in DRF.
You can use serializers.SerializerMethodField(...) if you want more control over what you wish to return
class QuestionSerializer(serializers.ModelSerializer):
choices = serializers.SerializerMethodField()
def get_choices(self, question):
return [choice.choice_text for choice in question.choices.all()]
class Meta:
model = Question
fields = '__all__'
Instead of PrimaryKeyRelatedField, consider using SlugRelatedField. For example:
class QuestionSerializer(serializers.ModelSerializer):
choices = SlugRelatedField(many=True, read_only=True, slug_field='choice_text')
class Meta:
model = Question
fields = '__all__'
Also, you need to make choice_text field unique, ie add unique=True for this to work.
choice_text = models.CharField(max_length=200, unique=True)
FYI, you can remove __unicode__ method from your model definations. Explanation can be found in this Django documentation link.
Related
I'm trying to build my own poll app and the models have a lot of relation with each other and I need to count how many objects are in relation with some other objects so I need these custom function for it
Here's the model with the custom functions:
class Poll(models.Model):
title = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_available = models.BooleanField(default=True)
date_created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
#property
def total_votes(self):
votes = 0
for o in self.option_set.all():
votes = votes + o.how_much_vote
return votes
#property
def users_involved(self):
list = []
for o in self.option_set.all():
for v in o.vote:
list.append(v.user)
return list
#property
def users_involved(self):
users = []
for o in self.option_set.all():
for v in o.who_voted:
users.append(v)
return users
#property
def total_votes_each_option(self):
dct = {}
for o in self.option_set.all():
dct[o.title]= o.how_much_vote
return dct
My question is how do you include all of those custom functions total_votes, users_involved, etc to my api? because right now my api looks something like this:
{
"id": 1,
"title": "Which is the best frontend framework?",
"is_active": true,
"is_available": true,
"date_created": "2021-07-13T14:08:17.709054Z"
}
which is expected but I want to know how do I add those extra value to make it look like this
{
"id": 1,
"title": "Which is the best frontend framework?",
"is_active": true,
"is_available": true,
"date_created": "2021-07-13T14:08:17.709054Z",
"total_votes": "some number",
"users_involved": [...users]
}
Edit: here's my serializer class
class PollSerializer(serializers.ModelSerializer):
class Meta:
model = Poll
fields = '__all__'
Thank you very much
Simple way we can use SerializerMethodField
from rest_framework import serializers
class PollSerializer(serializers.ModelSerializer):
total_votes = serializers.SerializerMethodField()
users_involved = serializers.SerializerMethodField()
class Meta:
model = Poll
fields = (
'id',
'title',
'is_active',
'is_available',
'date_created',
'total_votes',
'users_involved',
)
def get_total_votes(self, poll):
return poll.total_votes
def get_users_involved(self, poll)
return poll.users_involved
Please note that SerializerMethodField is read_only, cannot for write.
Also make sure your custom model functions well for loop processing, using select_related or prefetch_related.
This one should be easy, you can add addition fields in Serializers by mentioning the field_name and its type before class Meta: something like this.
Model Upload
class Upload(models.Model):
.
.
total_files = models.IntegerField(default=0,null=True,blank=True)
.
.
def add(self):
return (self.total_files + 2)
Serializer FileSerializer
class FileSerializer(serializers.ModelSerializer):
#This is the line you need to add
extra_add = serializers.IntegerField(source = 'add')
class Meta:
model = Upload
fields= ['id','name','uploaded','total_files','created_on','updated_on','extra_add']
Then mention your extra_add field name in fields[..,..,'extra_add'].
You can change the type of field while mentioning the field thereserializer.<field_type>(source='<method_name>').
Result of the Above field i added:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username')
class PollSerializer(serializers.ModelSerializer):
total_votes = serializers.IntegerField(read_only=True)
users_involved = UserSerializer(read_only=True, many=True)
class Meta:
model = Poll
fields = '__all__'
#staticmethod
def setup_eager_loading(queryset):
return queryset.prefetch_related('option_set')
Also note that you defined users_involved twice in your Pool class.
You can use PoolSerializer.setup_eager_loading function in your view/viewset for better db query performance but it's not necessary (I do it in viewset.get_queryset method like self.queryset = self.serializer_class.setup_eager_loading(self.queryset).
I use ModelSerializer to return serialized data of a model:
class CategorySerializer(serializers.ModelSerializer):
category_count = serializers.SerializerMethodField('_category_count')
def _category_count(self, obj):
category_obj = Category.objects.get(id=obj.id)
questions_count = Question.objects.filter(category=category_obj).count()
return questions_count
class Meta:
model = Category
fields = ["id", "category", "category_count"]
What I get write now is here:
[{
"category": "orzecznictwo lekarskie",
"category_count": 0,
"id": 9
},
{
"category": "zdrowie publiczne",
"category_count": 1,
"id": 10
}
]
I want to remove objects with category_count == 0 from serialization
[
{
"category": "zdrowie publiczne",
"category_count": 1,
"id": 10
}
]
Thank you for your help.
EDIT:
class Question(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE)
question = models.TextField(blank=True, null=True)
(...)
class Category(models.Model):
category = models.TextField()
category_slug = models.CharField(max_length=50)
You're generating the field inside the serializer and then want to discard the object. That's not how things work: serializers merely change representation of data. Which data is displayed is determined in views.
class CategoryListView(generics.ListAPIView):
def get_queryset(self):
return models.Category.objects.annotate(
question_count=Count("question_set")
).exclude(question_count=0)
And in the serializer, you don't have to create the count any more and add the 'question_count' to Meta.fields.
Recommended reading.
Edit:
Forgot that DRF will check the field on the model and fail, so you need to declare the field:
class CategoryWithQuestionCount(serializers.ModelSerializer):
question_count = serializers.IntegerField(read_only=True)
class Meta:
model = models.Category
fields = ("pk", "category", "question_count")
I'm not sure about removing it in the ModelSerializer. But you can try overriding get_queryset() or changing the queryset in your View:
queryset = Class.objects.exclude(dzial_liczba_pytan=0)
the model in question:
class CustomerPrices(models.Model):
url = models.OneToOneField('CustomerProductUrls', models.DO_NOTHING, db_column="url", primary_key=True)
written = models.DateTimeField()
reseller = models.CharField(max_length=250)
price = models.FloatField(blank=True, null=True)
class Meta:
managed = False
db_table = 'customer_prices'
unique_together = (('url', 'written', 'reseller'),)
the serializer (and one related serializer) in question:
class CustomerProductUrlsSerializer(serializers.ModelSerializer):
ean = CustomerProductsSerializer()
url = serializers.CharField(max_length=255, required=False)
class Meta:
model = CustomerProductUrls
fields = '__all__'
class CustomerPricesSerializer(serializers.ModelSerializer):
written = serializers.DateTimeField(format='%Y-%m-%d %H:%M:00', required=False)
reseller = serializers.CharField(max_length=250, required=False)
url = CustomerProductUrlsSerializer()
name = serializers.SerializerMethodField('get_name')
ean = serializers.SerializerMethodField('get_ean')
url = serializers.SerializerMethodField('get_url')
price = serializers.FloatField()
class Meta:
model = CustomerPrices
fields = '__all__'
def get_name(self, obj):
return obj.url.ean.name
def get_ean(self, obj):
return obj.url.ean.ean
def get_url(self, obj):
return obj.url.url
and the ListAPIView for the CustomerPrices class:
class CustomerPricesListView(generics.ListAPIView):
serializer_class = CustomerPricesSerializer
filter_backends = (DjangoFilterBackend, OrderingFilter)
fields = ('written', 'url', 'price')
filter_fields = fields
search_fields = fields
def get_queryset(self):
"""
This view should return a list of all the products where price >= 70
"""
return CustomerPrices.objects.filter(price__gte=70)
Inside my CustomerPricesSerializer I've got a field named ean as well as name the values for these two come through the related CustomerProductUrlsSerializer (and corresponding model). The code is working so far, I get a response looking like this:
"results": [
{
"url": "https://www.example.com/p/12345678",
"written": "2020-04-05 12:00:00",
"reseller": "ABC123",
"name": "Product 12345678",
"ean": "1234567890123",
"price": 98.3
}, ...
]
I'm using the DjangoFilterBackend and I would like to filter on the ean as well as the name, which is available in my response. But as soon as I add ean to my fields tupel inside the Serializer I get the Meta.fields contains fields that are not defined on this FilterSet: ean. I do understand that my queryset is returning the fields from CustomerPrices Model inside my ListAPIView, but how do I get get the ean as well as the name to be a part of the queryset and therefore a part of the available fields
The field ean does not belong to the CustomerPrices model but somewhere else. The DRF expects the filter tuples are directly related to the model (here CustomerPrices) and hence the error.
To resolve this issue, you have to provide the actual relationship to the ean field.
class CustomerPricesListView(generics.ListAPIView):
fields = ('written', 'url', 'price', 'url__ean__ean', 'url__ean__name')
filter_fields = fields
# rest of the code
I have a parent id in an object that contains child data. I want to add another field value from the parent id into the serializer for the child table to return back to the view
I made a nested serializer, but the literature only focuses on updating the parent and child object. I only want to update the child object, but I want a field from the parent object added to the data of the serializer.
The reverse relationship is especially confusing because 1. it didn't work for me, and 2. the example here uses it on a parent item which was already used as an example of a forward relationship.
https://www.django-rest-framework.org/api-guide/serializers/
class ChoiceSerializer(serializers.ModelSerializer):
class Meta:
model = Choice
fields = ['choice_text', 'votes', 'question']
class QuestionSerializer(serializers.ModelSerializer):
choice = ChoiceSerializer(many=True)
class Meta:
model = Question
fields = ['id', 'question_text', 'choice']
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
def __str__(self):
return self.question_text
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)# Create your models here.
def __str__(self):
return self.choice_text
s = ChoiceSerializer(data={'choice_text':'tests','votes': 50, 'question':1})
s.is_valid()
s.save()
I got this from s.data:
ReturnDict([('choice_text', 'tests'), ('votes', 50), ('question', 1)])
I would prefer this:
ReturnDict([('choice_text', 'tests'), ('votes', 50), ('question_test', 'hello')])
Mostly for this requirement to change/add additional information from model instance serialize data one possible way to do that is to use SerializerMethodField.
In addition we may want to use same serializer for updating data and also for getting serialize data for response. what i want to mean during saving information through serializer we need question field but during retrive data we need question_test.
class ChoiceSerializer(serializers.ModelSerializer):
question_test = serializers.SerializerMethodField()
class Meta:
model = Choice
fields = ['choice_text', 'votes', 'question', 'question_test']
extra_kwargs = {'question': {'write_only': True}}
def get_question_test(self, obj):
return obj.question.question_test
I'm serialzing a Product model and its comments. Here's my simple code:
class ProductSerializer(serializers.HyperlinkedModelSerializer):
comment_set = CommentSerializer(many=True, read_only=True)
class Meta:
model = Product
fields = [
'title',
'comment_set'
]
class CommentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Comment
fields = [
'text',
]
class Comment(models.Model):
product = models.ForeignKey(Product, null=True, blank=True, db_index=True)
class Product(models.Model):
title = models.CharField(max_length=50)
...
Problem:
If the product has many comments. For example, 500 comments. All 500 of them got serialized.
How to limit the result to a number of my own choosing, like 100 comments?
I've done some research before posting this but only found questions about filtering.
Thank you.
Define a new method on the Product model that returns a query set with a limited number of comments.
Then pass that method as the source of the CommentSerializer inside your ProductSerializer.
class Product(models.Model):
title = models.CharField(max_length=50)
def less_comments(self):
return Comment.objects.all().filter(product=self).order_by("-id")[:100]
Then in the ProductSerializer:
class ProductSerializer(serializers.HyperlinkedModelSerializer):
comment_set = CommentSerializer(many=True, read_only=True, source="less_comments")
PS: Wrote the codes from memory, didn't test them. But should work.
You can write custom ListSerializer and put in CommentSerializer, then create custom field in ProductSerializer, wich source based on default related name:
class LimitedListSerializer(serializers.ListSerializer):
def to_representation(self, data):
data = data.all()[:100]
return super(FilteredListSerializer, self).to_representation(data)
class CommentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
list_serializer_class = LimitedListSerializer
model = Comment
fields = [
'text',]
class Product(serializers.HyperlinkedModelSerializer):
related_comments = CommentSerializer(many=True, read_only=True, source='comment_set')
when you pass many=True, list serrializer will be called.
You'll want to work on the CommentSerializer's queryset to control which ones you keep.
You'll be able to do that by overriding get_queryset. For example, to filter them against the current user. Note that I took this example because it highlights how to use the request's context to filter against:
class CommentSerializer(serializers.HyperlinkedModelSerializer):
def get_queryset(self):
user = self.context['request'].user
queryset = Comment.objects.filter(user=user)
return queryset
class Meta:
model = Comment
fields = [
'text',
]