Django restframework serializer with joining tables - python

I'm creating a django API for a discord bot for sounds recorded management.
Here are my models (simplified) :
class Member(models.Model):
name = models.CharField("Discord Name", max_length=100, unique=True)
discord_id = models.BigIntegerField("Discord ID", unique=True, blank=True, null=True)
class Sound(models.Model):
id = models.AutoField("Sound ID", primary_key=True)
file = models.FileField("Sound", upload_to='sounds/')
added_by = models.ForeignKey(Member, verbose_name="Author", on_delete=models.SET_NULL, blank=True, null=True)
tags = TaggableManager("Tags", blank=True)
class MemberinSound(models.Model):
sound = models.ForeignKey(Sound, on_delete=models.CASCADE, related_name="membersinsound")
personality = models.ForeignKey(Member, related_name='membersinsound', on_delete=models.CASCADE)
Explanations : a sound is uploaded by any member (added_by), with multiples optional tags, and personality that appears in this sound (MemberinSound).
You can imagine this 3 tables like (player, match, and playersinmatch).
My view to get a specific sound is :
class SpecificSound(APIView):
def get(self, request, id: int, formate=None, **kwargs):
sound = Sound.objects.select_related().get(id=id)
membersinsound = sound.membersinsound.all()
serializer = SoundSerializer(sound, many=False)
return Response(serializer.data)
And serializers.py :
class MemberInSoundSerializer(serializers.ModelSerializer):
class Meta:
model = MemberinSound
fields = [
'personality'
]
class SoundSerializer(serializers.ModelSerializer):
personality = MemberInSoundSerializer(many=True, read_only=True)
class Meta:
model = Sound
fields = [
'id',
'title',
'volume',
'file',
'stream_friendly',
'is_active',
'personality',
]
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['path'] = instance.file.path
return representation
First problem I got is in my view where I send to my serializer only Sound model (without MemberinSound), I don't find how to get this in a one shot query. Maybe using annotate options ? Thought it could be possible with something like backward relationships shown in the documentation.
First time I use the restframework, maybe I made others mistakes.
For infos, I'm using Django 4.0.3 ; djangorestframework 3.13.1 and Python 3.8 with SQLITE3.

Related

Unable to get related data from ManyToManyField

I'm trying to fetch related objects from below two models.
Following django models with ManyToManyField relationship.
Book
class Book(models.Model):
authors = models.ManyToManyField(
to=Author, verbose_name="Authors", related_name="books_author"
)
bookshelves = models.ManyToManyField(
to=Bookshelf, verbose_name="Bookshelf", related_name="books_shelves"
)
copyright = models.NullBooleanField()
download_count = models.PositiveIntegerField(blank=True, null=True)
book_id = models.PositiveIntegerField(unique=True, null=True)
languages = models.ManyToManyField(
to=Language, verbose_name=_("Languages"), related_name="books_languages"
)
Author
class Author(models.Model):
birth_year = models.SmallIntegerField(blank=True, null=True)
death_year = models.SmallIntegerField(blank=True, null=True)
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Author")
verbose_name_plural = _("Author")
I have to fetch all the Auhtors with their related books. I have tried a lot of different ways none is working for me.
First way : using prefetch_related
class AuthorListAPIView(APIErrorsMixin, generics.ListAPIView):
serializer_class = AuthorSerializer
queryset = Author.objects.exclude(name__isnull=True)
def get_queryset(self):
auths = queryset.prefetch_related(Prefetch("books_author"))
Second way using related_name 'books_auhtor'
class AuthorListAPIView(APIErrorsMixin, generics.ListAPIView):
serializer_class = AuthorSerializer
queryset = Author.objects.exclude(name__isnull=True)
def get_queryset(self):
auths = queryset.books_author.all()
None of the above ways worked for me. I want to prepare a list of Authors and their associated books.
For ex:-
[{'Author1':['Book1','Book2'],... }]
Prefetching is not necessary, but can be used to boost efficiency, you can work with:
class AuthorListAPIView(APIErrorsMixin, generics.ListAPIView):
serializer_class = AuthorWithBooksSerializer
queryset = Author.objects.exclude(name=None).prefetch_related('books_author')
In the AuthorWithBooksSerializer, you can then add the data of the books, for example:
from rest_framework import serializers
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('book_id', 'copyright')
class AuthorWithBooksSerializer(serializers.ModelSerializer):
books = BookSerializer(source='books_author', many=True)
class Meta:
model = Author
fields = ('name', 'books')
Here the books will use the BookSerializer and thus encode a list of dictionaries.
While you can use the name of the author as object key, I strongly advise against this: it makes the object less accessible since the keys are no longer fixed and if these contain spaces, it can also result in more trouble obtaining the value(s) associated with a given attribute name.

Unable to POST JSON data from multiple select element with Django REST Framework

I would like to be able to send an AJAX POST request to my API endpoint to create a new instance of my Asset model with multiple Category instances referenced in my Asset model, hence the many-to-many field type in my Asset model.
I'm able to successfully POST and create new Asset instances, however my category field won't accept any data at all. The category field remains empty when a new Asset instance is created. I think it has something to do with my CategorySerializer. I'm still learning how to use Django REST Framework so I'd appreciate if I could get some help figuring out how to work with serializers in Django REST Framework.
I've already tried modifying the AssetSerializer create method to handle parsing the JSON and validating the data but that hasn't worked. I've also tried other solutions suggested in other posts I've found on StackOverflow but haven't found anything that works for my situation.
Here's my serializers.py file:
class CategorySerializer(serializers.ModelSerializer):
name = serializers.CharField(required=False, read_only=True)
class Meta:
model = Category
fields = ('id', 'name')
class AssetSerializer(serializers.ModelSerializer):
name = serializers.CharField(allow_null=True)
description = serializers.CharField(allow_null=True)
manufacturer = serializers.CharField(allow_null=True)
uid = serializers.UUIDField(read_only=True, allow_null=True)
borrower = BorrowerSerializer(allow_null=True, read_only=True)
condition = serializers.ChoiceField(choices=Asset.CONDITION_TYPE, default='g', allow_null=True)
owner = serializers.ReadOnlyField(source='owner.username')
return_date = serializers.DateField(allow_null=True)
checked_out = serializers.BooleanField(allow_null=True)
category = CategorySerializer(required=False, many=True)
class Meta:
model = Asset
fields = ('uid',
'name',
'manufacturer',
'model',
'description',
'owner',
'condition',
'category',
'borrower',
'checked_out',
'return_date',
'is_dueback',
)
def update(self, instance, validated_data):
instance.borrower = validated_data.get('borrower', instance.borrower)
instance.return_date = validated_data.get('return_date', instance.return_date)
instance.checked_out = validated_data.get('checked_out', instance.checked_out)
instance.name = validated_data.get('name', instance.name)
instance.manufacturer = validated_data.get('manufacturer', instance.manufacturer)
instance.model = validated_data.get('model', instance.model)
instance.description = validated_data.get('description', instance.description)
instance.condition = validated_data.get('condition', instance.condition)
instance.category = validated_data.get('category', instance.category)
instance.save()
return instance
def create(self, validated_data):
return Asset.objects.create(**validated_data)
Here's my Asset model:
class Asset(models.Model):
"""Model representing an Asset"""
uid = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=200)
manufacturer = models.CharField(max_length=64)
model = models.CharField(max_length=128)
description = models.TextField()
category = models.ManyToManyField(Category)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
borrower = models.ForeignKey(Borrower, on_delete=models.CASCADE, null=True, blank=True)
checked_out = models.BooleanField(default=False)
return_date = models.DateField(null=True, blank=True)
CONDITION_TYPE = (
('e', 'Excellent'),
('g', 'Good'),
('f', 'Fair'),
('p', 'Poor'),
)
condition = models.CharField(
max_length=1,
choices=CONDITION_TYPE,
blank=True,
help_text='Asset condition')
class Meta:
ordering = ['return_date']
#property
def is_dueback(self):
if self.return_date and date.today() > self.return_date:
return True
return False
def display_category(self):
"""Create a string for the Category. This is required to display category in Admin."""
return ', '.join(category.name for category in self.category.all())
display_category.short_description = 'Category'
def __str__(self):
return f'{self.uid} - {self.name}'
def get_absolute_url(self):
return reverse('asset-detail', args=[str(self.uid)])
Here's my Category model:
class Category(models.Model):
"""Model representing an Asset category"""
name = models.CharField(max_length=128)
def __str__(self):
return self.name
I'd appreciate any help you could provide. Thank you in advance.
i'm almost new in DRF but i try to help. why you writing all the field in serializer when you using ModelsSerializer? not need to telling ModelSerializer what type of field should be because you are pointing to model in class Meta and DRF know about fields and type and etc . second about allow_null=True in serializer, when Model haven't null=True you can't except DRF can create a not null-able field for instance with null=True so if you wnt a field can be null just add null=True in Model class . for your problem about ManytoMantry field try to use Primary key relation for ManyToMany fields in your serializers then pass id of Category instances in list:
class AssetSerializer(serializers.ModelSerializer):
borrower = BorrowerSerializer(allow_null=True, read_only=True)
category = serializers.PrimaryKeyRelatedField(many=True, queryset=Category.objects.all())
class Meta:
model = Asset
fields = ('uid',
'name',
'manufacturer',
'model',
'description',
'owner',
'condition',
'category',
'borrower',
'checked_out',
'return_date',
'is_dueback',
)
read_only_fields = ( 'uid' , ) # this fields will be read_only
depending on how you using this serializer in your view for save and update have difference way. if your view is generics class so will do create and update itself by POST and PUT method .and for other class view that isn't belong to generics DRF view you can using serializer.save() to create a new instance.wish help you.
pass data something like:
{
"name" : "foo",
"manufacture" : "foo",
.
.
.
"category" : [1,2,3,24,65]
}

Many to Many model with a dropdown in Django Rest Framework?

I am trying to create a Many to Many relation with a model in between, I have a Client model, and a Zone model, each client may have access to different zones, and each zone may have multiple clients.
Therefore I created a model called Access Permission, that stores said relation, and I want to show a dropdown selector in the post form that shows the existing clients and zones, or to ask for the Id of an existing object, instead of showing the form to create new ones.
These are my models:
class Zone(models.Model):
name = models.TextField()
created = models.DateTimeField(auto_now=True)
def __str__(self):
return '%s' % (self.name)
class Client(models.Model):
name = models.TextField()
birthDate = models.DateField()
created = models.DateTimeField(auto_now=True)
def __str__(self):
return '%s' % (self.name)
class AccessPermission(models.Model):
idClient = models.ForeignKey(Client, on_delete=models.CASCADE, null=False)
idZone = models.ForeignKey(Zone, on_delete=models.CASCADE, null=False)
And these my current serializers:
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Zone
fields = ('name',)
class ClientSerializer(serializers.HyperlinkedModelSerializer):
zones = ZonesSerializer(source='accesspermission_set', many=True, read_only=True)
class Meta:
model = Client
fields = ('name', 'birthDate', 'zones')
class AccessPermissionSerializer(serializers.ManyRelatedField):
idClient = ClientSerializer(many=False)
idZone = ZoneSerializer(many=False)
class Meta:
model = AccessPermission
fields = ('idClient', 'idZone')
Is there any way to ask for the Id of an existing object, or show the existing ones, instead of the fields to create new ones?
You can do it like:
models
class AccessPermission(models.Model):
client = models.ForeignKey(Client, on_delete=models.CASCADE, null=False)
zone = models.ForeignKey(Zone, on_delete=models.CASCADE, null=False)
serializers
class AccessPermissionSerializer(serializers.ManyRelatedField):
id = serializers.IntegerField(read_only=True)
client_id = serializers.PrimaryKeyRelatedField(
queryset=Client.objects.all(), source='client', allow_null=False, required=True
)
zone_id = serializers.PrimaryKeyRelatedField(
queryset=Zone.objects.all(), source='zone', allow_null=False, required=True
)
class Meta:
model = AccessPermission
fields = (
'id', 'client_id', 'zone_id'
)

How to SELECT a field from a LEFT JOIN'ed table/model with Django's ORM (1.11)

Context: Using Django Rest Framework, I've created a ModelViewSet.
With these models:
class Claim(models.Model):
permalink = models.SlugField(max_length=255, blank=True, unique=True)
author = models.ForeignKey(get_user_model(), db_index=True, on_delete=models.SET_NULL, null=True, blank=True)
deleted = models.BooleanField(db_index=True, default=False)
collaborators = models.ManyToManyField(get_user_model(), through='ClaimCollaborator', through_fields=('claim', 'user'), related_name='claims')
# ...other fields
class ClaimCollaborator(models.Model):
claim = models.ForeignKey(Claim, db_index=True, on_delete=models.CASCADE)
user = models.ForeignKey(get_user_model(), db_index=True, on_delete=models.CASCADE)
access_project_only = models.BooleanField(default=True)
I'm trying to query Claim to LEFT JOIN ClaimCollaborator and bring back the access_project_only field. ClaimCollaborator is actually an intermediary model handling the ManyToMany relationship between claims and users (collaborators of the claim).
So far I have the following view (cut down for brevity):
class ClaimViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated, )
serializer_class = serializers.ClaimSerializer
lookup_field = 'permalink'
def get_queryset(self):
return models.Claim.objects.filter(Q(author=self.request.user) | Q(claimcollaborator__user=self.request.user))
Serializer:
class ClaimSerializer(serializers.HyperlinkedModelSerializer):
author = serializers.PrimaryKeyRelatedField(allow_null=True, queryset=get_user_model().objects.all())
class Meta:
model = Claim
fields = ('url', 'permalink', 'author', 'deleted')
lookup_field = 'permalink'
extra_kwargs = {
'url': {'lookup_field': 'permalink'},
}
Listing produces this SQL:
SELECT "api_claim"."permalink", "api_claim"."author_id", "api_claim"."deleted"
LEFT OUTER JOIN "api_claimcollaborator" ON ("api_claim"."id" = "api_claimcollaborator"."claim_id")
WHERE ("api_claim"."author_id" = 39 OR "api_claimcollaborator"."user_id" = 39)
So I'm getting the LEFT JOIN on "api_claimcollaborator" (ClaimCollaborator) just fine, but none of the fields. I've tried .only(<claim fields>, 'claimcollaborator__access_project_only') and .selected_related('claimcollaborator') on the query but this just produces errors (I can be more specific about my attempts if that's helpful - just let me know).
I'm guessing this isn't so straightforward because the table in question is used as a ManyToMany within the ORM? Any help would be greatly appreciated.
You can use SlugRelatedField on the serializer to indicate a field on a related model identified by a specific attribute.
class ClaimSerializer(serializers.HyperlinkedModelSerializer):
author = serializers.PrimaryKeyRelatedField(allow_null=True, queryset=get_user_model().objects.all())
claimcollaborator_set = serializers.SlugRelatedField(slug_field='access_project_only', read_only=True, many=True)
class Meta:
model = Claim
fields = ('url', 'permalink', 'claimcollaborator_set', 'author', 'deleted')

Django REST Framework relationship serialization

I've been banging my head against this issue and know I have to be missing something simple.
I'm trying to learn Django REST Framework and having issues setting the foreign keys of a new object to existing other objects when POSTing JSON to the server.
models.py
class Genre(models.Model):
name = models.CharField(max_length=200)
class Author(models.Model):
first_name = models.CharField(max_length=200)
last_name = models.CharField(max_length=200)
def full_name(self):
return self.first_name + ' ' + self.last_name
class Book(models.Model):
title = models.CharField(max_length=200)
genre = models.ForeignKey(Genre)
isbn = models.CharField(max_length=15, default='')
summary = models.CharField(max_length=500, null=True)
author = models.ForeignKey(Author)
serializers.py
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = ('id', 'first_name', 'last_name',)
class GenreSerializer(serializers.ModelSerializer):
class Meta:
model = Genre
fields = ('id', 'name',)
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
genre = GenreSerializer(read_only=True)
class Meta:
model = Book
fields = ('id','url', 'author', 'genre', 'title', 'isbn', 'summary',)
What I'm trying to is create a new book related to an existing Author and Genre. So, given some JSON like
{"title": "Title",
"author": {"id":1}
"genre" : {"id":2}
...
}
I want to create a new book and have its Genre and Author set to the appropriate entities that are already in the database.
I've tried to change the author and genre fields on BookSerializer to serializers.PrimaryKeyRelatedField() and scoured the docs and SO for answers, including this one. I've tried to change the fields in the JSON to "author": 1 or "genre_id": 2 but I can't seem to get it working. I keep getting django.db.utils.IntegrityError: books_book.genre_id may not be NULL.
I am using a DRF ModelViewSet for the views if that makes a difference.
What am I missing here?
You are getting Integrity error because it's expecting the author instance but you are sending the pk related to author. Try this
serializers.py
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
genre = GenreSerializer(read_only=True)
class Meta:
model = Book
fields = ('id','url', 'author', 'genre', 'title', 'isbn', 'summary',)
def create(self, validated_data):
author_id = self.initial_data.pop("author")
genre_id = self.initial_data.pop("genre")
author = Author.objects.get(id=author_id)
genre = Genre.objects.get(id=genre_id)
book = Book.objects.create(author=author, genre=genre, **validated_data)
return book

Categories

Resources