Django Rest Framework depth based on direction - python

I have two models:
class Organization(models.Model):
name = models.CharField(max_length=64)
class OrgUser(User):
organization = models.ForeignKey(Organization, related_name='users')
role = models.CharField(max_length=1, choices=USER_TYPE_CHOICES)
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = OrgUser
depth = 1
fields = ('email', 'role', 'organization',)
class OrganizationSerializer(serializers.HyperlinkedModelSerializer):
users = USerSerializer(many=True)
class Meta:
model = Organization
depth = 1
fields = ('name', 'users',)
I'm using the Django REST Framework and I'm trying to get the following output for the given URLS:
GET /organization/
{
'name':'Hello World',
'users':[{ 'email':'test#gmail.com', 'role':'A' }]
}
GET /user/
{
'email':'test#gmail.com',
'role':'A',
'organization':{ 'name':'Hello World' }
}
So what's happening is GET /organization/ is giving me the users array and the organization information again.
I've been racking my brain setting the depth property on my serializer, but I can't figure it out for the life of me. If someone could please point me in the right direction, I'd greatly appreciate it.

The problem is that you want different output from your UserSerializer depending on if it's being used alone (i.e. at GET /user/) or as a nested relation (i.e. at GET /organization/).
Assuming you want different fields in both, you could just create a third Serializer to use for the nested relationship that only includes the fields you want in the OrganizationSerializer. This may not be the most elegant way to do it, but I can't find any alternatives.
Sample code:
class Organization(models.Model):
name = models.CharField(max_length=64)
class OrgUser(User):
organization = models.ForeignKey(Organization, related_name='users')
role = models.CharField(max_length=1, choices=USER_TYPE_CHOICES)
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = OrgUser
depth = 1
fields = ('email', 'role', 'organization',)
class OrganizationUserSerializer(serializers.HyperlinkedModelSerializer): # New Serializer
class Meta:
model = OrgUser
depth = 1
fields = ('email', 'role',)
class OrganizationSerializer(serializers.HyperlinkedModelSerializer):
users = OrganizationUserSerializer(many=True) # Change to new serializer
class Meta:
model = Organization
depth = 1
fields = ('name', 'users',)

Related

Serializing 3 Interconnected Django relational models

I am trying to write a set of serializers for my models. I want the serializer for the deck to spit out the cards that match the decks ID in the CardToDeck Model and then fetch the card matching the card_id in the Card Model and Ideally be able to write with the given solution.
models.py
from django.db import models
# Create your models here.
class CardToDeck(models.Model):
deck_id = models.ForeignKey("Deck", related_name="card_to_deck", on_delete=models.CASCADE)
card_id = models.ForeignKey("Card", related_name="card_to_deck", on_delete=models.CASCADE)
class Card(models.Model):
id = models.AutoField(primary_key=True)
multiverseid = models.IntegerField(unique=True) # Used to fetch the card from the API
name = models.CharField(max_length=145)
class Deck(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=145)
serializers.py:
from .models import *
from rest_framework import serializers
class CardSerializer(serializers.ModelSerializer):
class Meta:
model = Card
fields = ('id', 'name')
class CardToDeckSerializer(serializers.ModelSerializer):
class Meta:
model = CardToDeck
fields = ('deck_id', 'card_id')
# Serializer for Deck that grabs the nested model references
class DeckSerializer(serializers.ModelSerializer):
cards = serializers.PrimaryKeyRelatedField(queryset=CardToDeck.objects.all(), many=True)
class Meta:
model = Deck
depth = 2
fields = ('name', 'cards')
What I would like the JSON to look like:
{
"name": "deckNameGoHere",
"cards": [{"id":1, "name":"cardNameGoHere", "quantity":3},{"id":2, "name":"cardNameGoHere", "quantity":2}]
}
I just spent the last 4 hours trying to figure this out and feel quite dumb, any help would be appreciated. I looked into the docks but couldn't find the resources I needed.
According to DRF documents:
PrimaryKeyRelatedField may be used to represent the target of the
relationship using its primary key.
You can use CardToDeckSerializer as DeckSerializer's field without PrimeryKeyRelated and as a serializer object if you want to read the data and bring it back:
from .models import *
from rest_framework import serializers
class CardSerializer(serializers.ModelSerializer):
class Meta:
model = Card
fields = ('id', 'name')
class CardToDeckSerializer(serializers.ModelSerializer):
class Meta:
model = CardToDeck
fields = ('deck_id', 'card_id')
# Serializer for Deck that grabs the nested model references
class DeckSerializer(serializers.ModelSerializer):
# if not intended to use bulk action it's better to keep the "many" fields as read only
cards = CardToDeckSerializer(many=True, read_only=True)
class Meta:
model = Deck
depth = 2
fields = ('name', 'cards')
If you wanted to show a list of related linkes, you could use the following:
HyperlinkedRelatedField
I highly recommend reading DRF's document on nested Serializers
Assuming quantity is in CardToDeck, you can do something like this:
class CardToDeckSerializer(serializers.ModelSerializer):
id = serializers.CharField(source='card_id.id')
name = serializers.CharField(source='card_id.name')
class Meta:
model = CardToDeck
fields = ('id', 'name', 'quantity')
class DeckSerializer(serializers.ModelSerializer):
cards = CardToDeckSerializer(source='card_to_deck' many=True)
class Meta:
model = Deck
fields = ('name', 'cards')

How to get model data to appear as a field in another model's response

These are simplified versions of my models (the user model is just an id and name)
class Convo(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='convo_owner')
users = models.ManyToManyField(User, through='Convo_user')
class Convo_user (models.Model):
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
convo = models.ForeignKey(Convo, on_delete=models.CASCADE)
class Comments(models.Model):
name = models.CharField(max_length=255)
content = models.TextField(max_length=1024)
convo = models.ForeignKey(Convo, on_delete=models.CASCADE)
This is my view
class ConvoViewSet(viewsets.ModelViewSet):
serializer_class = serializers.ConvoSerializer
def get_queryset(self):
return None
def list(self, request):
curr_user = request.user.id
# Collecting the list of conversations
conversations = models.Conversation.object.filter(ConvoUser__user_id=request.user.id)
#Getting list of conversation id's
conv_ids = list(conversations.values_list('id', flat=True).order_by('id'))
#Getting list of relevant comments
comments = models.Comments.objects.filter(conversation_id__in=conv_ids)
return Response(self.get_serializer(conversations, many=True).data)
And my current serializer
class ConvoSerializer(serializers.ModelSerializer):
"""A serializer for messaging objects"""
# access = AccessSerializer(many=True)
# model = models.Comments
# fields = ('id', 'name', 'content', 'convo_id')
class Meta:
model = models.Convo
fields = ('id', 'owner_id')
The current response I get is of the form
[
{
"id": 1,
"owner_id": 32
}, ...
]
But I would like to add a comments field that shows all the properties of comments into the response, so basically everything in the second queryset (called comments) and I'm not sure how to go about this at all. (I retrieve the comments in the way I do because I'm trying to minimize the calls to the database). Would I need to create a new view for comments, make its own serializer and then somehow combine them into the serializer for the convo?
The way you've set up your models, you can access the comments of each Convo through Django's ORM by using convo_object.comments_set.all(), so you could set up your ConvoSerializer to access that instance's comments, like this:
class ConvoSerializer(serializers.ModelSerializer):
"""A serializer for messaging objects"""
comments_set = CommentSerializer(many=True)
class Meta:
model = models.Convo
fields = ('id', 'owner_id', 'comments_set')
and then you define your CommentSerializer like:
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = models.Comments
fields = ('id', 'name', 'content')
No data appears because my serializers are using the default database, not sure why but a step forward
EDIT:
Django: Database used for prefetch_related is not the same that the parent query Provided me the correct answer, I was able to choose the database with this method because for some reason inner queries use the default DB

Django Rest Framework: Saving ForeignKey inside OneToOne model

I have 2 models that are OneToOne related and model that is FK to 2nd model
models.py
class Legal(TimeStampedModel):
name = models.CharField('Name', max_length=255, blank=True)
class LegalCard(TimeStampedModel):
legal = models.OneToOneField('Legal', related_name='legal_card', on_delete=models.CASCADE)
branch = models.ForeignKey('Branch', related_name='branch', null=True)
post_address = models.CharField('Post address', max_length=255, blank=True)
class Branch(TimeStampedModel):
name = models.CharField('Name',max_length=511)
code = models.CharField('Code', max_length=6)
Using DRF I made them to behave as single model so I can create or update both:
serializer.py
class LegalSerializer(serializers.ModelSerializer):
branch = serializers.IntegerField(source='legal_card.branch', allow_null=True, required=False)
post_address = serializers.CharField(source='legal_card.post_address', allow_blank=True, required=False)
class Meta:
model = Legal
fields = ('id',
'name',
'branch',
'post_address',
)
depth = 2
def create(self, validated_data):
legal_card_data = validated_data.pop('legal_card', None)
legal = super(LegalSerializer, self).create(validated_data)
self.update_or_create_legal_card(legal, legal_card_data)
return legal
def update(self, instance, validated_data):
legal_card_data = validated_data.pop('legal_card', None)
self.update_or_create_legal_card(instance, legal_card_data)
return super(LegalSerializer, self).update(instance, validated_data)
def update_or_create_legal_card(self, legal, legal_card_data):
LegalCard.objects.update_or_create(legal=legal, defaults=legal_card_data)
views.py
class LegalDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Legal.objects.all()
serializer_class = LegalSerializer
I'm trying to save this by sending FK as integer (I just want to post id of the branch), but I receive error
ValueError: Cannot assign "2": "LegalCard.branch" must be a "Branch" instance.
Is there any way to pass over only ID of the branch?
Thank you
In Django, if you only need the FK value, you can use the FK value that is already on the object you've got rather than getting the related object.
Assume you have a Legal and Branch object with id's as 1. Then you can save a LegalCard object by:
LegalCard(legal_id=1,branch_id=1,post_address="Istanbul Street No:1")
Just use legal_card.branch_id instead of legal_card.branch to get just an id, not a related object.
And depth = 1

Django Rest Framework Custom Serializer Method

I'm trying to create a custom serializer method that counts the number of passed and failed quizzes from my QuizResults model. A failed quiz is under .7 and a passed quiz is .7 or over.
I want to be able to look into the Users QuizResult and count the number of passed quizzes(.7 or over). I would then duplicate the method to count the failed quizzes (under .7).
So far I don't have much idea on how to do so. I want to be able to grab the percent_correct field of the model and do a calculation and add it to a field in the serializer called "quiz_passed".
Here is my QuizResult model:
class QuizResult(models.Model):
quiz = models.ForeignKey(Quiz)
user = models.ForeignKey(User, related_name='quiz_parent')
percent_correct = models.FloatField(validators=[MinValueValidator(0.0), MaxValueValidator(1.0)])
date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return 'Quiz Results for : ' + self.quiz.title
Here is my serializer:
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
todo_count = serializers.IntegerField(source='todo_parent.count', read_only=True)
discussion_count = serializers.IntegerField(source='comment_parent.count', read_only=True)
quiz_passed = serializers.SerializerMethodField()
class Meta:
model = User
fields = ('todo_count', 'discussion_count', 'quiz_passed', 'username', )
def get_quiz_passed(self, obj):
return passed
Any help is appreciated.
Edit:
I extended the User model and added a model method like you suggested.
class Profile(User):
def get_quizzes_passed_count(self):
return self.quiz_parent.filter(percent_correct__gte=0.8).count()
I then added your suggestion to my ProfileSerializer.
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
todo_count = serializers.IntegerField(source='todo_parent.count', read_only=True)
discussion_count = serializers.IntegerField(source='comment_parent.count', read_only=True)
num_quizzes_passed = serializers.ReadOnlyField(source="get_quizzes_passed_count")
class Meta:
model = Profile
fields = ('todo_count', 'discussion_count', 'num_quizzes_passed', 'username')
Unfortunately when I add this nothing appears in the framework once these have been added. Any suggestions? Thank you.
You can use a model method on the user model to count that user's number of passed quizzes:
class User(models.model):
# rest of your User attributes
def get_quizzes_passed_count(self):
return self.quiz_parent.filter(percent_correct__gte=0.7).count()
Then add that to your serializer using a DRF ReadOnlyField to serialize that method:
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
todo_count = serializers.IntegerField(
source='todo_parent.count', read_only=True
)
discussion_count = serializers.IntegerField(
source='comment_parent.count', read_only=True
)
quiz_passed = serializers.SerializerMethodField()
num_quizzes_passed = serializers.ReadOnlyField(source="get_quizzes_passed_count")
class Meta:
model = User
fields = ('todo_count', 'discussion_count', 'quiz_passed', 'username', )
def get_quiz_passed(self, obj):
return passed
You can duplicate this for the number of failed quizzes.

unique_together of two field messes with read_only_fields

I have this code for rating lessons, user and lesson should be added automatically from request authorization and URL:
#views.py
class RatingViewSet(
mixins.ListModelMixin,
mixins.CreateModelMixin,
viewsets.GenericViewSet
):
permission_classes = [permissions.IsAuthenticated]
serializer_class = RatingSerializer
def perform_create(self, serializer):
lessonInstance = Lesson.objects.get(id = self.kwargs['lessonID'])
serializer.save(user=self.request.user, lesson = lessonInstance)
def get_queryset(self):
lessonID = self.kwargs['lessonID']
return Rating.objects.filter(user=self.request.user, lesson=lessonID)
#serializers.py
class RatingSerializer(serializers.ModelSerializer):
class Meta:
model = Rating
fields = ('id', 'lesson','user', 'difficulty')
read_only_fields = ('id', 'user','lesson')
#models.py
class Rating(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
lesson = models.ForeignKey('lessons.Lesson')
difficulty = models.IntegerField()
class meta:
unique_together('user','lesson')
I want to have max 1 rating per user/lesson, hence unique_together('user','lesson'). But there is a problem: as long as that constraint is in the code, requests without user or lesson fields get denied with field required error, even though they are read_only.
(If I migrate with unique_together('user','lesson'), then delete that line it works, but as soon as it's there I get errors.)
I want to keep that bit of code there so I don't accidentally remove the unique_together constraint on later migrations.
This is a special-case that requires a different approach. Here's what django-rest-framework documentation (see the Note) says about this case:
The right way to deal with this is to specify the field explicitly on
the serializer, providing both the read_only=True and default=…
keyword arguments.
In your case, you need to explicitly define the user and lesson fields on your RatingSerializer, like this:
class RatingSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(read_only=True, default=serializers.CurrentUserDefault()) # gets the user from request
lesson = serializers.PrimaryKeyRelatedField(read_only=True, default=None) # or any other appropriate value
class Meta:
model = Rating
fields = ('id', 'lesson','user', 'difficulty')
Good luck!
If a field is read_only=True then the validated_data will ignore data of it => Cause error required field, read more at doc
I also met this issue in a similar context, then tried #iulian's answer above but with no luck!
This combo read_only + default behavior is not supported anymore, check this
I resolved this issue by 2 solutions:
My model:
class Friendship(TimeStampedModel):
"""Model present Friendship request"""
from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='friendship_from_user')
to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='friendship_to_user')
class Meta:
unique_together = ('from_user', 'to_user')
Solution 1. Write your own CurrentUserDefault class to get the user id then set to default attribute data of serializer(Ref from #51940976)
class CurrentUserDefault(object):
def set_context(self, serializer_field):
self.user_id = serializer_field.context['request'].user.id
def __call__(self):
return self.user_id
class FriendshipSerializer(serializers.ModelSerializer):
from_user_id = serializers.HiddenField(default=CurrentUserDefault())
class Meta:
model = Friendship
fields = ('id', 'from_user', 'from_user_id', 'to_user', 'status')
extra_kwargs = {
'from_user': {'read_only': True},
}
Solution 2. Override the create method of serializer to set data for user id(Ref from this)
class FriendshipSerializer(serializers.ModelSerializer):
class Meta:
model = Friendship
fields = ('id', 'from_user', 'to_user', 'status')
extra_kwargs = {
'from_user': {'read_only': True},
}
def create(self, validated_data):
"""Override create to provide a user via request.user by default.
This is require since the read_only `user` filed is not included by default anymore since
https://github.com/encode/django-rest-framework/pull/5886.
"""
if 'user' not in validated_data:
validated_data['from_user'] = self.context['request'].user
return super(FriendshipSerializer, self).create(validated_data)
I hope this helps!

Categories

Resources