Serializing 3 Interconnected Django relational models - python

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')

Related

How do you add existing objects to a ManyToMany field using Serializer?

I am trying to create an API with Artists and Songs, with a ManyToMany relationship between the two. Using the API to create a Song with an Artist that is not in the database works fine. The problem arises when I attempt to use the POST method to create a new Song with an Artist that already exists in the database. I tried overwriting the SongSerializer create() method using get_or_create() based on another post here, but I kept getting Bad Request errors when the Artist already exists in the database. The relevant code snippets:
models.py
class Artist(models.Model):
artist_name = models.CharField(max_length=200, unique=True)
class Meta:
ordering = ['artist_name']
def __str__(self):
return self.artist_name
class Song(models.Model):
song_title = models.CharField(max_length=200)
artists = models.ManyToManyField(Artist, related_name='songs')
class Meta:
ordering = ['song_title']
def __str__(self):
return self.song_title
serializers.py
class ArtistNameSerializer(serializers.ModelSerializer):
class Meta:
model = Artist
fields = ('artist_name',)
def to_representation(self, value):
return value.artist_name
class SongTitleSerializer(serializers.ModelSerializer):
songs = serializers.PrimaryKeyRelatedField(read_only=True, many=True)
def to_representation(self, value):
return value.song_title
class Meta:
model = Song
fields = ('songs',)
class ArtistSerializer(serializers.HyperlinkedModelSerializer):
songs = SongTitleSerializer(read_only=True, many=True)
class Meta:
model = Artist
fields = ('id', 'artist_name', 'songs')
class SongSerializer(serializers.HyperlinkedModelSerializer):
artists = ArtistNameSerializer(many=True)
class Meta:
model = Song
fields = ('id', 'song_title', 'artists',)
def create(self, validated_data):
artist_data = validated_data.pop('artists')
song = Song.objects.create(**validated_data)
song.save()
for artist_item in artist_data:
a, created = Artist.objects.get_or_create(artist_name=artist_item['artist_name'])
song.artists.add(a)
return song
I've done some tests and it looks like the program doesn't even go into the create() method I'm using, going straight to showing me the Bad Request error. What am I missing? Thanks in advance!
On you Artist model you have a constrain on the artist_model field (unique=True)
if you print the serializer in question with:
print(SongSerializer())
you get something like this:
SongSerializer():
id = IntegerField(label='ID', read_only=True)
song_title = CharField(max_length=200)
artists = ArtistNameSerializer(many=True):
artist_name = CharField(max_length=200, validators=[<UniqueValidator(queryset=Artist.objects.all())>])
under the artist_name field is a Validator "UniqueValidator"
so in case of a write operation you can disable the validator in the serializer with:
class ArtistNameSerializer(serializers.ModelSerializer):
class Meta:
model = models.Artist
fields = ('artist_name',)
extra_kwargs = {
'artist_name': {
'validators': [],
}
}
hope this help..

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

DjangoRestFramework: Unable to return count in JSON Response

I've a table Orders which contains ID, store_name, order_date, ..... I want to create an endpoint which returns me the JSON which consists of Count of Orders from all the Stores. Something as follows:
[{store_name: 'Target', count: 10}, {store_name: 'Walmart', count: 20}, {store_name: 'Costco', count: 5}]
The query I wrote is:
queryset = Stores.objects.all().values('store_name').annotate(total=Count('store_name'))
When I print queryset, I'm getting what I need as mentioned above.
But when I serialize the data, I get the following:
[{store_name: 'Target'}, {store_name: 'Walmart'}, {store_name: 'Costco'}]
Not sure what am I doing wrong.. I've included my code. (I'm not including import statements)
serializer.py
class StoresSerializer(ModelSerializer):
class Meta:
model = Stores
exclude = ['order_date',]
views.py
class StoresViewSet(ModelViewSet):
queryset = Stores.objects.all().values('store_name').annotate(total=Count('store_name'))
serializer_class = StoresSerializer
What am I missing?
Using one old topic it would go as follows
models.py (simplified version just to demonstrate the idea)
class Store(models.Model):
name = models.CharField(max_length=100)
class Order(models.Model):
store_name = models.ForeignKey(Store)
order_date = models.DateTimeField(auto_now_add=True)
serializer.py
from rest_framework.serializers import ModelSerializer
from rest_framework import serializers
from .models import Store
class StoresSerializer(ModelSerializer):
orders = serializers.IntegerField()
class Meta:
model = Store
fields = ('name', 'orders')
views.py
class StoresViewSet(viewsets.ModelViewSet):
queryset = Store.objects.all().values('name').annotate(orders=Count('order'))
serializer_class = StoresSerializer

Django Rest Framework depth based on direction

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',)

Django ModelForm with foreign key

I'm trying to create a ModelForm that updates a table with foreign keys. What I have seems to work, but I was hoping someone could tell me if there's a better way to do this or if there's a problem with the way I'm doing it below.
Is it correct to use the queryset on the Author and Genres table? It feels like I should be using a queryset on the Book model, and relate the foreign key to those tables.
models.py:
class Author(models.Model):
name = models.CharField(max_length=200)
class Genre(models.Model):
name = models.CharField(max_length=200)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey('Author')
genre = models.ForeignKey('Genre')
forms.py:
class BookForm(ModelForm):
title = forms.CharField(max_length=200)
author = forms.ModelChoiceField(queryset=Author.objects.all())
genre = forms.ModelChoiceField(queryset=Genre.objects.all())
class Meta:
model = Book
fields = ['title', 'author', 'genre']
views.py:
def add_book(request):
if request.method == 'POST':
form = BookForm(request.POST)
if form.is_valid():
form.save(commit=True)
return HttpResponseRedirect('/add/')
else:
form = BookForm()
The only thing that's wrong with this code is you are overriding the default behaviour of your model form.
Change it to look like:
class BookForm(ModelForm):
class Meta:
model = Book
fields = ['title', 'author', 'genre']
And let django handle with the definition of those.
if you need to add labels or widgets, you can define them in the Meta class:
class BookForm(ModelForm):
class Meta:
model = Book
fields = ['title', 'author', 'genre']
labels = {'title': 'Book title', }
For example.
I'm not really sure what you're asking here, or why you would want a queryset on Book.
You haven't actually changed any of the defaults on any of the fields, so there is no need to redefine them: your form could be simply
class BookForm(ModelForm):
class Meta:
model = Book
and it would work exactly the same.

Categories

Resources