I am having trouble in showing cross relation data in my api end-point
this is my serlization class:
class ArticleSerializer(serializers.ModelSerializer):
# these two variables are assigned coz, author and category are foregin key
author = serializers.StringRelatedField()
category = serializers.StringRelatedField()
class Meta:
model = Article
fields = '__all__'
Above serialization working fine but not satisfy needs.
And this is my models
from django.db import models
import uuid
class Organization(models.Model):
organization_name = models.CharField(max_length=50)
contact = models.CharField(max_length=12, unique=True)
def __str__(self):
return self.organization_name
class Author(models.Model):
name = models.CharField(max_length=40)
detail = models.TextField()
organization = models.ForeignKey(Organization, on_delete=models.DO_NOTHING)
def __str__(self):
return self.name
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Article(models.Model):
alias = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='author')
title = models.CharField(max_length=200)
body = models.TextField()
category = models.ForeignKey(Category, on_delete=models.CASCADE)
this is my views:
class ArticleSingle(RetrieveAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
lookup_field = 'alias'
and currently my end-point showing like these but i dont like it:
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"alias": "029a4b50-2e9a-4d35-afc6-f354d2169c05",
"author": "jhon doe",
"category": "sic-fi",
"title": "title goes here",
"body": "this is body"
}
I want the end-point should show the data with author details like, author, organization_name, contact, etc if you dont understand it, please see my models how i designe it.
The end-point should show each and every related data that with foreign key, i hope you got my issue, thanks for your time
What you are looking for is nested serialization.
Rather than specifying author and category as StringRelatedFields, you should assign them as serializer classes (the same applies to the Organization model):
class OrganizationSerializer(serializers.ModelSerializer):
...
class Meta:
model = Organization
fields = '__all__'
class AuthorSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer()
...
class Meta:
model = Author
fields = '__all__'
class CategorySerializer(serializers.ModelSerializer):
...
class Meta:
model = Category
fields = '__all__'
class ArticleSerializer(serializers.ModelSerializer):
author = AuthorSerializer()
category = CategorySerializer()
class Meta:
model = Article
fields = '__all__'
This mechanism will serialize your data as you desire:
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"alias": "029a4b50-2e9a-4d35-afc6-f354d2169c05",
"author": {
"name": "jhon doe",
"organization": {
"organization_name": "..."
}
...
}
"category": {
"name": "sic-fi"
}
"title": "title goes here",
"body": "this is body"
}
Note: If you want to perform POST requests for creation of those resources, you can find some useful information here.
Related
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.
I am using Django Rest Framework for a project and I am running into a problem. When the frontend creates a Team they want to reference all relationships with an ID, but when getting the Team, they want the data from the relationship. How can I achieve this?
models:
class Team(models.Model):
class Meta:
db_table = "team"
team_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
class Organization(models.Model):
class Meta:
db_table = "organization"
organization_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
class Position(models.Model):
class Meta:
db_table = "position"
position_id = models.AutoField(primary_key=True)
team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="positions")
class Player(model.Model):
class Meta:
db_table = "player"
player_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
positions = models.ManyToManyField(Position, related_name="players")
serializers:
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ["team_id", "name", "organization", "positions"]
positions = PositionSerializer(many=True) # This is merely for output. There is no need to create a position when a team is created.
organization = OrganizationSerializer() # Since an organization already exists I'd like to just give an organization_id when creating/editing a team.
# I don't think the other serializers matter here but can add them on request.
So when doing POST or PATCH on a team, I'd like the front end to be able to pass this payload
{
"name": "My Team",
"organization": 1
}
but when doing a GET on a team, I'd like the front end to receive this response.
{
"team_id": 1,
"name": "My Team",
"organization": {
"organization_id": 1,
"name": "My Organization"
},
"positions": [{
"position_id": 1,
"players": [{
"player_id": 1,
"name": "Member 1"
}
]
}
Is there a a way to achieve this?
In such situations define two serializers, one is for read operations and one is for write operations.
class TeamWriteSerializer(serializers.ModelSerializer):
# see, here no nested relationships...
class Meta:
model = Team
fields = ["name", "organization"]
class TeamReadSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ["team_id", "name", "organization", "positions"]
positions = PositionSerializer(many=True)
organization = OrganizationSerializer()
and now, use these two serializers properly in your views. For example, I hope you are using the ModelViewSet in views,
class TeamModelViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.request.method.lower() == 'get':
return TeamReadSerializer
else:
return TeamWriteSerializer
In my models.py there are two models:
class Genre(models.Model):
genre_id = models.CharField(max_length=10)
name = models.CharField(max_length=40)
information = models.CharField(max_length=120)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=100)
genre = models.ForeignKey(Genre, on_delete=models.CASCADE)
def __str__(self):
return self.title
They are serialized:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('title', 'author', 'genre')
class GenreSerializer(serializers.ModelSerializer):
class Meta:
model = Genre
fields = ('name', 'information')
and ViewSets are created for each:
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
class GenreViewSet(viewsets.ModelViewSet):
queryset = Genre.objects.all()
serializer_class = GenreSerializer
What I'd like to do is:
Sending a POST request to books/ endpoint. Sent data has to contain existing genre ID, it won't be saved to the database otherwise (it's done by default already).
Receiving information from the Genre model as a response.
Let me give a short example:
I'm sending this JSON:
{
"title": "Hercules Poirot",
"author": "Agatha Christie",
"genre": 1
}
Instead of repeated request from above I receive something like this:
{ "genre": "crime story" }
How to do this?
What you can do is add a custom create method within your BookViewSet to override the return statement.
You can take exemple on the default create method which is implemented within CreateModelMixin (https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py#L12)
You could tell how the nested field will work. Like this:
class GenreSerializer(serializers.ModelSerializer):
class Meta:
model = Genre
fields = ('name', 'information')
class BookSerializer(serializers.ModelSerializer):
genre = GenreSerializer(read_only=True)
class Meta:
model = Book
fields = ('title', 'author', 'genre')
My model looks like this:
class User(TimestampedModel):
name = models.CharField(max_length=30, null=False, blank=False)
device = models.CharField(max_length=255, null=False, blank=False)
class Comment(TimestampedModel):
user = models.ForeignKey(User, on_delete=models.PROTECT, blank=True, null=True)
contents = models.CharField(max_length=510)
rating = models.IntegerField(blank=False, null=False)
And my serializer looks like this:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('name',)
class CommentListItemSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = Comment
fields = ('user', 'contents', 'rating')
And the view:
class CommentsList(generics.ListAPIView):
serializer_class = CommentListItemSerializer
queryset = Comment.objects.all()
It's almost getting the job done ;). The response I'm getting looks like this:
"results": [
{
"user": {
"name": "Ania"
},
"contents": "Very good",
"rating": 6
},
{
"user": {
"name": "Anuk"
},
"contents": "Not very good",
"rating": 1
}
]
There are two problems with that response.
I don't want to have this nested object "user.name". I'd like to receive that as a simple string field, for example "username".
Serializer makes a database query (not a join, but a separate query) for each user, to get his/her name. Since that's unacceptable, how to fix that?
Serializer makes a database query (not a join, but a separate query)
for each user, to get his/her name.
You can use select_related() on the queryset attribute of your view. Then accessing user.name will not result in further database queries.
class CommentsList(generics.ListAPIView):
serializer_class = CommentListItemSerializer
queryset = Comment.objects.all().select_related('user') # use select_related
I don't want to have this nested object "user.name". I'd like to
receive that as a simple string field, for example "username"
You can define a read-only username field in your serializer with source argument. This will return a username field in response.
class CommentListItemSerializer(serializers.ModelSerializer):
# define read-only username field
username = serializers.CharField(source='user.name', read_only=True)
class Meta:
model = Comment
fields = ('username', 'contents', 'rating')
You can add custom functions as fields
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.PROTECT, blank=True, null=True)
contents = models.CharField(max_length=510)
rating = models.IntegerField(blank=False, null=False)
def username(self):
return self.user.name
class CommentListItemSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ('username', 'contents', 'rating')
How do I get the object from the reverse relation in serializers.py?
I have a model like this
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
def __str__(self):
return self.title
class Category(models.Model):
post = models.ForeignKey(Post, related_name='category')
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=70, unique=False)
def __str__(self):
return self.title
And from the Django Rest Framework documentation, I can access the category directly through the related name and this is my serializers.py
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ('title','content','category')
The problem is the the view only return the category post id:
HTTP 200 OK
Vary: Accept
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"title": "Post title 1",
"content": "test content..blah blah blah",
"category": [
1
]
}
]
}
How can I get the category name and slug??
The related_name will only return id's, and this is not wrong at all. If you want the full representation, you will also need to add a serialized version of each child object in your parent. Like this:
class PostSerializer(serializers.ModelSerializer):
category = CategorySerializer(many=True, required=False)
So you first need to have a CategorySerializer, and then you must add the relationship in the PostSerializer. All parameters are optional. Here is a small example.
P.S. : I suggest using 'categories' as related_name, since you can have more than just one.