How to handle multiple related objects (nesting within objects) - django

Due to the way my database is designed, images are not stored with the project.
This is because there is no set amount of images per product. Some may have 1 image, others may have 10.
I would like my API to return content nested within itself. Currently, my code simply repeats the entire object when additional images exist for the item.
I am using Django Rest Framework:
class ProductDetailView(APIView):
renderer_classes = (JSONRenderer, )
def get(self, request, *args, **kwargs):
filters = {}
for key, value in request.GET.items():
key = key.lower()
if key in productdetailmatch:
lookup, val = productdetailmatch[key](value.lower())
filters[lookup] = val
qset = (
Product.objects
.filter(**filters)
.values('pk', 'brand')
.annotate(
image=F('variation__image__image'),
price=F('variation__price__price'),
name=F('variation__name'),
)
)
return Response(qset)
Currently, an item with 3 images pointing to it will look like this:
[{
"name": "Amplitiue jet black",
"brand": "Allup",
"price": "$1248",
"vari": "917439",
"image": "url1",
},
{
"name": "Amplitiue jet black",
"brand": "Allup",
"price": "$1248",
"vari": "917439",
"image": "url",
},
{
"name": "Amplitiue jet black",
"brand": "Allup",
"price": "$1248",
"vari": "917439",
"image": "url",
},
]
Ideally, it should look like this, combining all the images within an array:
{
"name": "Amplitiue jet black",
"brand": "Allup",
"price": "$1248",
"vari": "917439",
"images": [
"url1",
"url2"
"url3"
],
}

You should use a ListApiView together with a ModelSerializer. Don't put the filtering in the get method, the Django class based view way is to use get_queryset for that.
from rest_framework import serializers, generics
class ImageSerializer(serializers.ModelSerializer):
class Meta:
model = Image
fields = ("url",)
class ProductSerializer(serializers.ModelSerializer):
images = ImageSerializer(many=True)
class Meta:
model = Product
fields = ("name", "brand", "price", "vari", "images")
class ProductListView(generics.ListAPIView): # it is not a details view
serializer_class = ProductSerializer
def get_queryset(self):
filters = {}
for key, value in self.request.GET.items():
key = key.lower()
if key in productdetailmatch:
lookup, val = productdetailmatch[key](value.lower())
filters[lookup] = val
return Product.objects.prefetch_related("images").filter(**filters)
The image list in the JSON will be objects with one "url" element instead of just a list of urls, but this is more consistent with REST standards anyway.

Related

Django can i only pass "id" in POST request, despite displaying nested fields?

in my post requests to OrderProduct model, i want to only have to pass order.id and product.id and it works... untill i add a serializer to retrieve product.name. It might be because i didnt understand documentation about nested requests, but im unable to advance further into my project :(
[
{
"id": 2,
"order": 1,
"product": 1,
}
]
^ here's how it looks without nested serializer, and thats the data that i wanna have to input
[
{
"id": 2,
"order": 1,
"product": {
"id": 1,
"name": "gloomhaven",
},
},
^ here's how it looks after i add an additional serializer. I pretty much want these nested fields to be read only, with me still being able to send simple post requests
here are my serializers
class OrderProductSerializer(serializers.ModelSerializer):
product = Product()
class Meta:
model = OrderProduct
fields = [
"id",
"order",
"product"]
class Product(serializers.ModelSerializer):
class Meta:
model = Product
fields = (
"id",
"name")
Is there any way for me to accomplish this? Thank you for trying to help!
Just overwrite to_representation method of the serializer
def to_representation(self, instance):
response = super().to_representation(instance)
response['other_field'] = instance.id# also response['other_field'] = otherSerializer(instance.model)
return response
This can solve your problem
I think you are missing many=True
class OrderProductSerializer(serializers.ModelSerializer):
product = Product(many=True)
class Meta:
model = OrderProduct
fields = [
"id",
"order",
"product"]

Prefetching indirectly related items using Django ORM

I'm trying to optimize the queries for my moderation system, build with Django and DRF.
I'm currently stuck with the duplicates retrieval: currently, I have something like
class AdminSerializer(ModelSerializer):
duplicates = SerializerMethodField()
def get_duplicates(self, item):
if item.allowed:
qs = []
else:
qs = Item.objects.filter(
allowed=True,
related_stuff__language=item.related_stuff.language
).annotate(
similarity=TrigramSimilarity('name', item.name)
).filter(similarity__gt=0.2).order_by('-similarity')[:10]
return AdminMinimalSerializer(qs, many=True).data
which works fine, but does at least one additional query for each item to display. In addition, if there are duplicates, I'll do additional queries to fill the AdminMinimalSerializer, which contains fields and related objects of the duplicated item. I can probably reduce the overhead by using a prefetch_related inside the serializer, but that doesn't prevent me from making several queries per item (assuming I have only one related item to prefetch in AdminMinimalSerializer, I'd still have ~2N + 1 queries: 1 for the items, N for the duplicates, N for the related items of the duplicates).
I've already looked at Subquery, but I can't retrieve an object, only an id, and this is not enough in my case. I tried to use it in both a Prefetch object and a .annotate.
I also tried something like Item.filter(allowed=False).prefetch(Prefetch("related_stuff__language__related_stuff_set__items", queryset=Items.filter..., to_attr="duplicates")), but the duplicates property is added to "related_stuff__language__related_stuff_set", so I can't really use it...
I'll welcome any idea ;)
Edit: the real code lives here. Toy example below:
# models.py
from django.db.models import Model, CharField, ForeignKey, CASCADE, BooleanField
class Book(Model):
title = CharField(max_length=250)
serie = ForeignKey(Serie, on_delete=CASCADE, related_name="books")
allowed = BooleanField(default=False)
class Serie(Model):
title = CharField(max_length=250)
language = ForeignKey(Language, on_delete=CASCADE, related_name="series")
class Language(Model):
name = CharField(max_length=100)
# serializers.py
from django.contrib.postgres.search import TrigramSimilarity
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from .models import Book, Language, Serie
class BookAdminSerializer(ModelSerializer):
class Meta:
model = Book
fields = ("id", "title", "serie", "duplicates", )
serie = SerieAdminAuxSerializer()
duplicates = SerializerMethodField()
def get_duplicates(self, book):
"""Retrieve duplicates for book"""
if book.allowed:
qs = []
else:
qs = (
Book.objects.filter(
allowed=True, serie__language=book.serie.language)
.annotate(similarity=TrigramSimilarity("title", book.title))
.filter(similarity__gt=0.2)
.order_by("-similarity")[:10]
)
return BookAdminMinimalSerializer(qs, many=True).data
class BookAdminMinimalSerializer(ModelSerializer):
class Meta:
model = Book
fields = ("id", "title", "serie")
serie = SerieAdminAuxSerializer()
class SerieAdminAuxSerializer(ModelSerializer):
class Meta:
model = Serie
fields = ("id", "language", "title")
language = LanguageSerializer()
class LanguageSerializer(ModelSerializer):
class Meta:
model = Language
fields = ('id', 'name')
I'm trying to find a way to prefetch related objects and duplicates so that I can get rid of the get_duplicates method in BookSerializer, with the N+1 queries it causes, and have only a duplicates field in my BookSerializer.
Regarding data, here would be an expected output:
[
{
"id": 2,
"title": "test2",
"serie": {
"id": 2,
"language": {
"id": 1,
"name": "English"
},
"title": "series title"
},
"duplicates": [
{
"id": 1,
"title": "test",
"serie": {
"id": 1,
"language": {
"id": 1,
"name": "English"
},
"title": "first series title"
}
}
]
},
{
"id": 3,
"title": "random",
"serie": {
"id": 3,
"language": {
"id": 1,
"name": "English"
},
"title": "random series title"
},
"duplicates": []
}
]

Specifying field names in serializer class acting as another serializer class's field

Suppose for below ModelSerializer class
class UserSongSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
song_likes = serializers.ReadOnlyField() # This is model's property field
song_shares = serializers.ReadOnlyField()
song_plays = serializers.ReadOnlyField()
song_price = serializers.ReadOnlyField()
genre = GenreSerializer(many=True,required=False,context={'key':5})
language = LanguageSerializer(many=True, required=False)
Passing specific context kwarg like below
genre = GenreSerializer(many=True,required=False,context={'fields':['name']})
Since I want to retrieve only name field in Genre model class in some specific cases, I overrided GenreSerializer class's get_fields_name method so that I can mention specific fields only when required via context
class GenreSerializer(serializers.ModelSerializer):
def get_field_names(self, *args, **kwargs):
"""
Overriding ModelSerializer get_field_names method for getting only specific fields in serializer when mentioned in SerializerClass arguments
"""
field_names = self.context.get('fields', None)
if field_names:
return field_names
return super(GenreSerializer, self).get_field_names(*args, **kwargs)
class Meta:
model = Genre
fields = '__all__'
However, I am unable to get any 'fields' (getting None) key inside overrided get_fields_name method. I know of other ways as well like using StringRelatedField but that would change the output representation to
"genre":[
"Pop",
"Rock"
]
Whereas, I want to stick to my original representation
"genre": [
{
"id": 3,
"name": "Pop",
"created_date": "2018-09-05T17:05:59.705422+05:30",
"updated_date": "2018-09-20T14:43:02.062107+05:30",
"status": false
},
{
"id": 4,
"name": "Rock",
"created_date": "2018-09-05T17:06:06.889047+05:30",
"updated_date": "2018-09-17T16:45:22.684044+05:30",
"status": true
},
{
"id": 5,
"name": "Classical",
"created_date": "2018-09-05T17:06:14.216260+05:30",
"updated_date": "2018-09-05T17:06:14.275082+05:30",
"status": true
}
]
UPDATE - What I want is like this
"genre": [
{
"name": "Pop"
},
{
"name": "Rock"
},
{
"name": "Classical"
}
]
Contexts are meant to be set to the root serializer only.
Whenever UserSongSerializer will be instantiated it'll override the nested genre context.
If you are using generic views, you'll want to override the view's get_serializer_context and add your own context there. It's documented at the bottom of the methods section
PS: context are "shared" to serializers, fields, validators.
PPS: Don't alter context after it's been set you it's going to be sort of undefined behavior.

Django-REST-Framework "GroupBy" ModelSerializer

I have the following situation
class MyModel(models.Model):
key = models.CharField(max_length=255)
value = models.TextField(max_length=255)
category = models.CharField(max_length=4)
mode = models.CharField(max_length=4)
the fields key, category and mode are unique together. I have the following objects:
m1 = MyModel(key='MODEL_KEY', value='1', category='CAT_1' mode='MODE_1')
m2 = MyModel(key='MODEL_KEY', value='2', category='CAT_1' mode='MODE_2')
m3 = MyModel(key='MODEL_KEY', value='1', category='CAT_2' mode='MODE_1')
m4 = MyModel(key='MODEL_KEY', value='2', category='CAT_2' mode='MODE_2')
I want to expose an API that will group by key and category so the serialized data will look something like this:
{
"key": "MODEL_KEY",
"category": "CAT_1"
"MODE_1": { "id": 1, "value": "1" }
"MODE_2": { "id": 2, "value": "2" }
},
{
"key": "MODEL_KEY",
"category": "CAT_2"
"MODE_1": { "id": 3, "value": "1" }
"MODE_2": { "id": 4, "value": "2" }
}
Is there any way of doing this in django rest framework with ModelSerializer.
There is module that allows you to group Django models and still work with a QuerySet in the result: https://github.com/kako-nawao/django-group-by
Using the above to form your queryset:
# Postgres specific!
from django.contrib.postgres.aggregates.general import ArrayAgg
qs = MyModel.objects.group_by('key', 'category').annotate(
mode_list=ArrayAgg('mode')).order_by(
'key', 'category').distinct()
You can then access the properties key, category and mode_list on the resulting QuerySet items as attributes like qs[0].mode_list. Therefore, in your serializer you can simply name them as fields.
The model_list field might require a SerializerMethodField with some custom code to transform the list.
Note that you need an aggregation if you don't want to group by mode, as well.

Django fixtures primary key error, need natural keys solution

So I have a Film model that holds a list of Actors model in a many to many field:
class Person(models.Model):
full = models.TextField()
short = models.TextField()
num = models.CharField(max_length=5)
class Film(models.Model):
name = models.TextField()
year = models.SmallIntegerField(blank=True)
actors = models.ManyToManyField('Person')
I'm trying to load some initial data from json fixtures, however the problem I have is loading the many to many actors field.
For example I get the error:
DeserializationError: [u"'Anna-Varney' value must be an integer."]
with these fixtures:
{
"pk": 1,
"model": "data.Film",
"fields": {
"actors": [
"Anna-Varney"
],
"name": "Like a Corpse Standing in Desperation (2005) (V)",
"year": "2005"
}
while my actors fixture looks like this:
{
"pk": 1,
"model": "data.Person",
"fields": {
"full": "Anna-Varney",
"num": "I",
"short": "Anna-Varney"
}
}
So the many to many fields must use the pk integer, but the problem is that the data isn't sorted and for a long list of actors I don't think its practical to manually look up the pk of each one. I've been looking for solutions and it seems I have to use natural keys, but I'm not exactly sure how to apply those for my models.
EDIT: I've changed my models to be:
class PersonManager(models.Manager):
def get_by_natural_key(self, full):
return self.get(full=full)
class Person(models.Model):
objects = PersonManager()
full = models.TextField()
short = models.TextField()
num = models.CharField(max_length=5)
def natural_key(self):
return self.full
But I'm still getting the same error
There's a problem with both the input and the natural_key method.
Documentation: Serializing Django objects - natural keys states:
A natural key is a tuple of values that can be used to uniquely
identify an object instance without using the primary key value.
The Person natural_key method should return a tuple
def natural_key(self):
return (self.full,)
The serialised input should also contain tuples/lists for the natural keys.
{
"pk": 1,
"model": "data.film",
"fields": {
"actors": [
[
"Matt Damon"
],
[
"Jodie Foster"
]
],
"name": "Elysium",
"year": 2013
}
}