How to override related field in django? - python

Let's say we have such models.
class Product(models.Model):
name = models.CharField(max_length=100)
# ...
main_photo = models.ImageField(upload_to='photos/')
class ProductPhoto(models.Model):
product = models.ForeignKey(Product, related_name='photos', on_delete=models.CASCADE)
photo = models.ImageField(upload_to='photos/')
def __str__(self):
return self.photo.url
I have two views:
ProductsView. It provides list of products with general information about each one, including name, ..., main_photo only.
ProductDetailsView. It provides more detailed info, including all photos.
class ProductsView(ListAPIView):
serializer_class = ProductSerializer
class ProductDetailsView(RetrieveAPIView):
serializer_class = ProductDetailsSerializer
serializers:
class ProductSerializer(ModelSerializer):
class Meta:
model = Product
fields = ('id', 'name', 'main_photo')
class ProductDetailsSerializer(ModelSerializer):
photos = StringRelatedField(many=True)
class Meta:
model = Product
fields = ('id', 'name', 'main_photo', 'photos')
I want detailed view to provide all photos in flat array photos, like this [main_photo, ...rest_photos].
In other words,
In response to detailed view instead of this:
{
"id": 1,
"name": "name",
"main_photo": "/media/photos/main_photo.jpg",
"photos": [
"/media/photos/photo1.jpg",
"/media/photos/photo2.jpg",
"/media/photos/photo3.jpg"
],
}
I want to get this:
{
"id": 1,
"name": "name",
"photos": [
"/media/photos/main_photo.jpg",
"/media/photos/photo1.jpg",
"/media/photos/photo2.jpg",
"/media/photos/photo3.jpg"
],
}
How can I do this with django rest framework? On which level should this logic be implemented? Model, View, Serializer?
I think it should be somewhere here, but not quite sure how should it look.
class ProductDetailsView(RetrieveAPIView):
serializer_class = ProductDetailsSerializer
def get_queryset(self):
query_set = Product.objects.all()
# ...
return query_set

For url of the photos add a __str__ method in ProductPhoto which will return only url of the photo
class ProductPhoto(models.Model):
...
def __str__(self):
return self.photo.url
and change ProductDetailsSerializer like this
class ProductDetailsSerializer(ModelSerializer):
photo_list = serializers.SerializerMethodField()
def get_photo_list(self, obj):
db_photos = obj.photos.all()
result = []
if obj.main_photo:
result.append(obj.main_photo.url)
for p in db_photos:
result.append(p.photo.url)
return result
class Meta:
model = Product
fields = ('id', 'name', 'photo_list')
For more relation related documentation for DRF check this

Related

Is there a way to filter out items from RelatedManager in a ModelViewSet?

I'm using DRF for a simple API, and I was wondering if there's a way to achieve this behavior:
I've got two models similar to the following:
class Table(models.Model):
name = models.CharField(max_length=100)
...
class Column(models.Model):
original_name = models.CharField(max_length=100)
name = models.CharField(max_length=100, blank=True, null=True)
...
table = models.ForeignKey(Table, on_delete=models.CASCADE, related_name="columns")
And their serializers as follows:
class ColumnSerializer(serializers.HyperlinkedModelSerializer):
table = serializers.HyperlinkedRelatedField(
read_only=True, view_name="table-detail"
)
class Meta:
model = Column
fields = ["url", "name", "table"]
class TableSerializer(serializers.HyperlinkedModelSerializer):
dataset = serializers.HyperlinkedRelatedField(
read_only=True, view_name="dataset-detail"
)
tags = serializers.SlugRelatedField(
many=True, slug_field="name", queryset=Tag.objects.all()
)
columns = ColumnSerializer(many=True, read_only=True)
class Meta:
model = Table
fields = [
"url",
"name",
...
"columns",
]
This returns me an output similar to
{
...
"results": [
{
"url": "http://0.0.0.0:8001/api/tables/1/",
"name": "some-name",
"columns": [
{
"url": "http://0.0.0.0:8001/api/columns/1/",
"name": "id",
"table": "http://0.0.0.0:8001/api/tables/1/"
},
...
}
which is totally fine. But what I'd really want to do is, if a Column has name=None, it's filtered out from every API ViewSet. I've managed to do it on the ColumnViewSet by doing queryset = queryset.filter(name__isnull=False), but I can't do it for the TableViewSet or others that might show a Column list.
I've tried tinkering with the ColumnSerializer, but the best I could get from it was to show nulls on the Column list.
I wonder if there's a way of hiding those.
EDIT 1: Adding my ViewSets
class TableViewSet(viewsets.ModelViewSet):
serializer_class = TableSerializer
def get_queryset(self):
queryset = Table.objects.all().order_by("name")
# some query_params filtering
return queryset
class ColumnViewSet(viewsets.ModelViewSet):
serializer_class = ColumnSerializer
def get_queryset(self):
queryset = Column.objects.all().order_by("id")
queryset = queryset.filter(name__isnull=False)
# some query_params filtering
return queryset
You can work with a Prefetch object [Django-doc] to filter the related object collection, so:
from django.db.models import Prefetch
class TableViewSet(viewsets.ModelViewSet):
serializer_class = TableSerializer
def get_queryset(self):
queryset = Table.objects.prefetch_related(
Prefetch('columns', Column.objects.filter(name__isnull=False))
).order_by('name')
# some query_params filtering
return queryset

Nested Serializer for Many to Many

I am new to Python and Django. I am creating api using Django-Rest-Framework I want to serializer data that can accept json in below format:
{
"ingredients": ["Sugar","Egg"],
"name": "Cake",
"description": "Dinner Food",
"directions": "direction1"
}
However I am able to persist data in db with below format:
{
"ingredients": [{"name":"Cake"},{"name":"Egg"}],
"name": "Rice",
"description": "Dinner Food",
"directions": "direction1"
}
I am not sure how can I convert dictionary in to the set field. I am aware of List field and list serialiser but not sure how to use them.
Is it possible to do this using model serialiser?
Serializer.py
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = '__all__'
class RecipeSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(many=True)
class Meta:
model = Recipe
fields = '__all__'
def create(self, validated_data):
ingredients_data = validated_data.pop('ingredients')
print(ingredients_data)
recipe = Recipe.objects.create(**validated_data)
for ingredient in ingredients_data:
ingredient, created = Ingredient.objects.get_or_create(name=ingredient['name'])
recipe.ingredients.add(ingredient)
return recipe
Model.py
class Ingredient(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Recipe(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True, null=True)
directions = models.TextField()
ingredients = models.ManyToManyField(Ingredient)
def __str__(self):
return self.name
view.py
class RecipieView(viewsets.ModelViewSet):
queryset = Recipe.objects.all()
serializer_class = RecipeSerializer
class IngredientView(viewsets.ModelViewSet):
queryset = Ingredient.objects.all()
serializer_class = IngredientSerializer
I would recommend you to use two different serializers for creation purpose and others. See the below snippet,
views.py
class RecipieView(viewsets.ModelViewSet):
queryset = Recipe.objects.all()
serializer_class = RecipeMainSerializer
def get_serializer_class(self):
if self.action == 'create':
return RecipeCreateSerializer
return RecipeMainSerializer
serializer.py
class RecipeCreateSerializer(serializers.ModelSerializer):
ingredients = serializers.ListField(write_only=True)
class Meta:
model = Recipe
fields = '__all__'
def create(self, validated_data):
ingredients_data = validated_data.pop('ingredients')
recipe = Recipe.objects.create(**validated_data)
for ingredient in ingredients_data:
ingredient, created = Ingredient.objects.get_or_create(name=ingredient)
recipe.ingredients.add(ingredient)
return recipe
class RecipeMainSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(many=True)
class Meta:
model = Recipe
fields = '__all__'
We can refactor #JPG serializers in better way like this:
class RecipeMainSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(many=True)
class Meta:
model = Recipe
fields = '__all__'
class RecipeCreateSerializer(RecipeMainSerializer):
ingredients = serializers.ListField(write_only=True)
def create(self, validated_data):
ingredients_data = validated_data.pop('ingredients')
recipe = Recipe.objects.create(**validated_data)
for ingredient in ingredients_data:
ingredient, created = Ingredient.objects.get_or_create(name=ingredient)
recipe.ingredients.add(ingredient)
return recipe
This improve dry principle of our codes in more complicated serializers

Django Rest Create with a Writable Nested Serializer

I'm trying to perform a create in Django Rest Framework using a writable nested serializer.
With the code bellow I can create a ScriptQuestion but I can't add a RecordedInterview into it. Django says OrderedDict is None.
What am I doing wrong?
Thanks in advance
#models.py
class ScriptQuestion(models.Model):
interview = models.ManyToManyField(RecordedInterview)
...
class RecordedInterview(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
...
The serializers
#serializers.py
class InterviewTitleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = RecordedInterview
fields = ('id', 'title')
extra_kwargs = {
'title': { 'read_only': True }
}
class QuestionDetailSerializer(serializers.HyperlinkedModelSerializer):
interview = InterviewTitleSerializer(many=True)
class Meta:
model = ScriptQuestion
fields = ('id', 'title', 'prep_time', 'answer_time', 'interview')
depth = 1
def create(self, validated_data):
interview_data = validated_data.pop('interview')
question = ScriptQuestion.objects.create(**validated_data)
for item in interview_data:
item = interview_data['id']
question.interview.add(item)
return question
Here is my view
#views.py
class CreateQuestion(generics.CreateAPIView):
queryset = ScriptQuestion.objects.all()
serializer_class = QuestionDetailSerializer
And the json
{
"title": "Question Test Json",
"prep_time": "1",
"answer_time":"1",
"interview": [
{
"id": "a450aeb0-8446-47b0-95bd-5accbb8b4afa"
}
]
}
If I do manually, I can add the RecordedInterview into the ScriptQuestion:
#serializers.py
def create(self, validated_data):
interview_data = validated_data.pop('interview')
question = ScriptQuestion.objects.create(**validated_data)
item = 'a450aeb0-8446-47b0-95bd-5accbb8b4afa'
question.interview.add(item)
return question
I cannot add a comment because of low reputation. SO adding as an answer. I think you should use 'serializers.ModelSerializer' instead of 'serializers.HyperLinkedModelSerializer'
Oh, I could make it.
For someone in the future, just add "id = serializers.CharField()" in the Serializer
class InterviewTitleSerializer(serializers.ModelSerializer):
id = serializers.CharField()
class Meta:
model = RecordedInterview
fields = ('id', 'title')
extra_kwargs = {'title': { 'read_only': True }}

Django Rest Framework - related fields as nested objects for GET and pk for POST only

In my app which use DRF, I want to use model serializer with multiple related objects.
models.py:
class JobType(models.Model):
name = models.CharField(null=False, max_length=250)
class Offer(models.Model):
user = models.ForeignKey(User, null=False)
job_type = models.ForeignKey(JobType, null=False)
salary = models.DecimalField(null=False, max_digits=8,
decimal_places=2)
serializers.py:
class JobTypeSerializer(serializers.ModelSerializer):
class Meta:
model = JobType
fields = ('id', 'name')
class OfferSerializer(serializers.ModelSerializer):
job_type = JobTypeSerializer()
class Meta:
model = Offer
fields = (
'salary', 'job_type', 'user'
)
views.py:
class SalaryViewSet(viewsets.ModelViewSet):
queryset = Salary.objects.all()
serializer_class = SalaryFullSerializer
What I want to achieve:
when I do GET request on my api/offers I want to have something like:
[
{
"salary": 1000,
"user: 1,
"job_type": {
"id": 1,
"name": "Developer",
}
}
]
so, basically, when GET offers is made, I want to have nested related object with all it's properties.
On other hand, when POST offers is made, I want to limit JobType choices.
When I've removed job_type = JobTypeSerializer() from OfferSerializer I had nice dropdown with available choices (in DRF debug). But it caused that GET on offers returned only JobOffer's ID in results.
How can I achieve desired behavior?
You can use different serializer for POST and GET requests.
Override get_serializer_class
def get_serializer_class(self):
if self.request.method == 'POST':
return SalaryPostSerializer
return SalaryFullSerializer

Django-rest serializer returning code instead of list of objects

I have an endpoint in my Django-rest application in which I expect to receive the following get response:
{
"my_objects": [
{
"my_object_order": 1,
"related_topics": [{"title": "my_title", "subtitle": "my_subtitle"}, {"title": "my_title2", "subtitle": "my_subtitle2"}],
"collected_at": "2016-05-02T20:52:38.989Z",
}]
}
In order to achieve that, below you can observe my serializers.py
class TopicSerializer(serializers.ModelSerializer):
class Meta:
model = MyTopic
fields = ["title", "subtitle"]
class MyObjectSerializer(serializers.ModelSerializer):
related_topics = TopicSerializer(many=True)
class Meta:
model = MyObject
fields = ("my_object_order",
"related_topics")
def create(self, validated_data):
"""
Saving serialized data
"""
related_topics_list = validated_data.pop("related_topics", [])
obj = MyObject.objects.create(**validated_data)
for topics_data in related_topics_list:
MyTopic.objects.create(trend=trend, **topics_data)
return obj
As suggested, here you can see my models.py
class MyObject(models.Model):
my_object_order = models.IntegerField()
collected_at = models.DateTimeField(auto_now=True)
def __unicode__(self):
return self.story_title
class MyTopic(models.Model):
my_obj = models.ForeignKey(MyObject, related_name="related_topics")
title = models.CharField(max_length=50, blank=False, null=True)
subtitle = models.CharField(max_length=50, blank=True, null=True)
def __unicode__(self):
return self.title
Below you have the excerpt from my views.py
def get(self, request):
params = request.QUERY_PARAMS
# Filtering data
obj_list = my_fun(MyObject, params)
response = {"my_objects": obj_list.values("my_object_order",
"collected_at",
"related_topics")}
return Response(response)
I have looked on the documentation, however I am confused/not understanding fundamentally what I should do.
Your problem is in views.py, you are not using actually the serializer at all. You are just filter some data and return whatever values you get from database (hence the ids only).
I suggest you to check Generic Class Based Views
from myapp.models import MyObject
from myapp.serializers import MyObjectSerializer
from rest_framework import generics
class MyObjectListAPIView(generics.ListAPIView):
queryset = MyObject.objects.all()
serializer_class = MyObjectSerializer
Also if you need any filtering check documentation here. Basically you can filter by fields from model with this snippet
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('field1', 'field2')
PS: You can do the view as normal function, but you have to handle yourself filtering/serialization part, the code may not look as cleaner as you get with class based views.

Categories

Resources