DRF 3 - Creating Many-to-Many update/create serializer with though table - python

I am trying to create a reference app in DRF 3 to demonstrate a nested serializer that can create/update models. The sample code below bombs with "*create() argument after ** must be a mapping, not list*" when trying to create the nested models. It is also no clear to me how I'd handle the .update() as in some cases I just want to be establish additional relationships (Persons).
The sample models:
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=128)
class Group(models.Model):
name = models.CharField(max_length=128)
persons = models.ManyToManyField(Person, through='Membership')
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
And the serializers and viewsets:
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from app.models import Group, Person
class PersonSerializer(ModelSerializer):
class Meta:
model = Person
class GroupSerializer(ModelSerializer):
persons = PersonSerializer(many=True)
def create(self, validated_data):
persons = validated_data.pop('persons')
group = Group.objects.create(**validated_data)
if persons: # Bombs without this check
Person.objects.create(group=group, **persons) # Errors here
return group
class Meta:
model = Group
class PersonModelViewSet(ModelViewSet):
serializer_class = PersonSerializer
queryset = Person.objects.all()
class GroupModelViewSet(ModelViewSet):
serializer_class = GroupSerializer
queryset = Group.objects.all()
I am trying to POST some JSON that inserts a Group with two (related) Persons:
{
"persons": [
{ "name" : "name 1" },
{ "name" : "name 2" }
],
"name": "group name 1"
}

I have no clue if there is an easier way, but the only way I managed to get this to work is to reference the 'through' model "memberships" in the Group serializer and write custom code for .create() and .update(). This seems like a lot of work to just set M2M FK's. If someone knows a better way I'd love to hear it.
class GroupMembershipSerializer(ModelSerializer):
class Meta:
model = Membership
fields = ('person',)
class GroupCreateSerializer(ModelSerializer):
memberships = GroupMembershipSerializer(many=True, required=False)
def create(self, validated_data):
person_data = validated_data.pop('memberships')
group = Group.objects.create(**validated_data)
for person in person_data:
d=dict(person)
Membership.objects.create(group=group, person=d['person'])
return group
def update(self, instance, validated_data):
person_data = validated_data.pop('memberships')
for item in validated_data:
if Group._meta.get_field(item):
setattr(instance, item, validated_data[item])
Membership.objects.filter(group=instance).delete()
for person in person_data:
d=dict(person)
Membership.objects.create(group=instance, person=d['person'])
instance.save()
return instance
class Meta:
model = Group
class GroupCreateModelViewSet(ModelViewSet):
serializer_class = GroupCreateSerializer
queryset = Group.objects.all()
So you can create a new Group with related Person(s) using:
{
"name" : "Group 1",
"memberships" : [
{ "person" : 1 },
{ "person" : 2 }
]
}

Use PrimaryKeyRelatedField shown here:
http://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield
class GroupSerializer(serializers.ModelSerializer):
persons = serializers.PrimaryKeyRelatedField(
many=True, queryset=Person.objects.all())
class Meta:
model = Group
fields = ('name', 'persons')
Create each person first, for example. Person with ID 1, Name = "Bob". Person with ID 2, Name = "Tim". Then post them to the REST Endpoint using their primary keys So:
# Group create() REST endpoint data to POST
{'name': 'my group', 'persons': [1, 2]}
Now the people that you had created prior, are part of that Group.

Related

Id instead of String when displaying foreign key field in DRF

I'm trying to return the name of the pricing field but all I get is its foreign key id instead. What am I doing wrong here? I looked at some similiar issues on here but I didn't find anything that resembled my situation.
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = (
"assignedteams",
"agent",
"facility",
"organisor",
"avatar",
)
class UserSubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = (
"user",
"pricing",
"status",
)
class UserSerializer(UserDetailsSerializer):
profile = UserProfileSerializer(source="userprofile")
subscription = UserSubscriptionSerializer(source="usersubscription")
class Meta(UserDetailsSerializer.Meta):
fields = UserDetailsSerializer.Meta.fields + ('profile', 'subscription',)
def update(self, instance, validated_data):
userprofile_serializer = self.fields['profile']
userprofile_instance = instance.userprofile
userprofile_data = validated_data.pop('userprofile', {})
usersubscription_serializer = self.fields['subscription']
usersubscription_instance = instance.usersubscription
usersubscription_data = validated_data.pop('usersubscription', {})
# update the userprofile fields
userprofile_serializer.update(userprofile_instance, userprofile_data)
usersubscription_serializer.update(usersubscription_instance, usersubscription_data)
instance = super().update(instance, validated_data)
return instance
You have 2 options to solve this problem.
option1:
If you want to return only the name of your pricing model you can use SlugRelatedField to do it.
Example:
class UserSubscriptionSerializer(serializers.ModelSerializer):
pricing = serializers.SlugRelatedField('name', readonly=True)
class Meta:
model = Subscription
fields = (
"user",
"pricing",
"status",
)
Option2:
If you want to return the Pricing object you can create a new ModelSerializer for your Pricing model and use it.
Example:
class PricingSerializer(serializers.ModelSerializer):
class Meta:
model = Pricing
fields = ["id","name"]
class UserSubscriptionSerializer(serializers.ModelSerializer):
pricing = PricingSerializer(readonly=True)
class Meta:
model = Subscription
fields = (
"user",
"pricing",
"status",
)
There are some other options that can you use but you must explain more about your problem can I will help you with.
you can easily add a new field representation or override the pricing field when want to represent data
so in your serializer add the following code
class UserSubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = (
"user",
"pricing",
"status",
)
def to_representation(self, instance):
data = super().to_representation(instance)
data['pricing_name'] = instance.pricing.name # or replace the name with your pricing name field
return data
As you are saying pricing returned FK id, so i assume pricing column inside Subscription model is a FK to another model, let's assume it Pricing model.
You can create a serializer for Pricing and use it on UserSubscriptionSerializer,
like the way you created UserProfileSerializer and UserSubscriptionSerializer for UserSerializer
But, using directly a nested serializer will give you problem while doing write operation since as far i can understand you are accepting pricing as FK value when creating or updating
To solve this issue you can do some if/else on get_fields() method
class UserSubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = (
"user",
"pricing",
"status",
)
def get_fields(self):
fields = super().get_fields()
# make sure request is passed through context
if self.context['request'] and self.context['request'].method == 'GET':
fields['pricing']=PricingSerializer()
return fields
Now coming back to the question, since you only need the pricing name which i assume name is a column on Pricing model
simply rewrite the previous code as
def get_fields(self):
fields = super().get_fields()
# make sure request is passed through context
if self.context['request'] and self.context['request'].method == 'GET':
fields['pricing'] = serializers.CharField(source='pricing.name', read_only=True)
return fields
P.S: I haven't tested this code on my computer

How to design ArrayField in django rest framework?

I am making API for cook book with Django Rest Framework. I don't know how to design ingredients model to make data to be like this:
{
"id": 1,
"name": "spaghetti",
"recipe": "recipe",
"ingredients": [
[{name:'pasta',amount:100},{name:'tomato',amount:200},{...}]
],
}
My model:
class Meal(models.Model):
name = models.TextField()
recipe = models.TextField()
ingredients = ?
Also how to serialize this field?
You can create a separate model for ingredient.
Many to many relation will be the best for me, because of one meal can have many ingredients and in the opposite way one ingredient can be used to make many meals.
According to django docs, in your case:
models.py
from django.db import models
class Ingredient(models.Model):
name = models.CharField(max_length=90)
amount = models.FloatField()
class Meta:
ordering = ['name']
def __str__(self):
return self.name
class Meal(models.Model):
name = models.CharField(max_length=100)
recipe = models.CharField(max_length=100)
ingredients = models.ManyToManyField(Ingredient)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
serializers.py
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = '__all__'
class MealSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(read_only=True, many=True)
class Meta:
model = Meal
fields = '__all__'
I believe what you are looking for is JsonBField
from django.contrib.postgres.fields.jsonb import JSONField as JSONBField
ingredients = JSONBField(default=list,null=True,blank=True)
this should do what you expect, have a nice day
edit: thanks for the update as #Çağatay Barın mentioned below
FYI, it is deprecated, Use django.db.models.JSONField instead.see the Doc
from django-3.1, Django comes with JSONField
class Meal(models.Model):
name = models.TextField()
recipe = models.TextField()
ingredients = models.JSONField()
There are two approaches to have this outcome [2] having another model:
using WritableNestedModelSerializer.
Overwriting the create() method of your serializer.
1st example, using WritableNestedModelSerializer:
# models.py --------------------------
class Ingredient(models.Model):
# model that will be related to Meal.
name = models.CharField(max_lenght=128)
amount = models.IntergerField()
def __str__(self):
return str(self.name)
class Meal(models.Model):
# meal model related to Ingredient.
name = models.TextField()
recipe = models.TextField()
ingredients = models.ForeigKey(Ingredient)
def __str__(self):
return str(self.name)
# serializers.py ----------------------
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = '__all__'
class MealSerializer(WritableNestedModelSerializer, serializers.ModelSerializer):
ingredients_set = IngredientSerializer(required=False, many=True)
class Meta:
model = Ingredient
fields = ["id", "name","recipe", "ingredients_set"]
2nd example rewriting the create() method:
# models.py --------------------------
# Follow the same approach as the first example....
# serializers.py ----------------------
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = '__all__'
class MealSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(required=False, many=True)
class Meta:
model = Meal
fields = ["id", "name","recipe", "ingredients"]
def create(self, validated_data):
# 1st step.
ingredients = validated_data('ingredients')
# 2nd step.
actual_instance = Meal.objects.create(**validated_data)
# 3rd step.
for ingredient in ingredients:
ing_objects = Ingredients.object.create(**ingredient)
actual_instance.ingredients.add(ing_objects.id)
actua_instance.save()
return actual_instance
What was done in the second example?
1st step: since you create a one-2-many relationship the endpoint will wait for a payload like this:
{
"name": null,
"recipe": null,
"ingredients": [],
}
ex of validated_data/ the data you sent:
{
"id": 1,
"name": "spaghetti",
"recipe": "recipe",
"ingredients": [
{name:'pasta',amount:100},{name:'tomato',amount:200},{...}
],
}
Therefore, since you are sending a payload with many ingredientes inside the ingredient array you will get this value from the validated_data.
For instance, if you make a print of the 'ingredients'(from the inside of the create() method) this is what you will get in your terminal:
[{name:'pasta',amount:100},{name:'tomato',amount:200},{...}]
2nd step: Alright, since you get the ingredients from validate_data it is time to create a Meal instance (where will be without the 'ingredients').
3rd step: You will loop all the ingredients objects from the 1st step you have done above and add them into the Meal.ingredients relationship saving the Meal instance.
-- about the extra model --
[2] Bear in mind that having a JSONField() allows anything to be added there even extra fields. Having a Meal model might be a better option if you want have a better control.

Django: Failed to link nested child to parent

I have an issue when trying to create nested objects, more specifically creating a parent and its child at the same time.
The child's model has the parent's id as foreign key as can be seen below.
Here is my parent_model:
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
from PIL import Image
class Work(models.Model):
user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
name = models.CharField(max_length=200)
length = models.IntegerField(null=True)
width = models.IntegerField(null=True)
def __str__(self):
return "{}".format(self.id)
My child_model:
from django.db import models
from .model_work import *
from .model_taxes import *
from djmoney.models.fields import MoneyField
class Price(models.Model):
work = models.OneToOneField(Work, on_delete=models.CASCADE, related_name='price')
price = MoneyField(max_digits=19, decimal_places=4, default_currency='USD', null=True)
total = models.IntegerField(null=True)
def __str__(self):
return "{}".format(self.price)
Here is my ParentCreateSerializer:
class WorkCreateSerializer(serializers.ModelSerializer):
"""
Serializer to create a new Work model in DB
"""
price = PriceCreateSerializer()
class Meta:
model = Work
fields = [
'user',
'price',
'name',
'length',
'width'
]
def create(self, validated_data):
price_data = validated_data.pop('price')
work = Work.objects.create(**validated_data)
price = Price.objects.create(**price_data)
return work
My ChildCreateSerializer:
class PriceCreateSerializer(serializers.ModelSerializer):
"""
Serializer to create a new Price when new Work model is created in DB
"""
# work = WorkDetailsSerializer()
class Meta:
model = Price
fields = [
'work',
'price',
'price_currency',
'total'
]
def create(self, validated_data):
work_data = validated_data.pop('work')
work = Work.objects.create(**work_data)
price = Price.objects.create(**validated_data)
return price
When I POST an object as shown below, both the parent and child objects are created but I can't manage giving the child the parent's id as foreign key, so they are not linked.
I have tried linking the child's create serializer to the parent's detail serializer (the commented line in my ChildCreateSerializer) but that creates an error work = WorkDetailsSerializer() NameError: name 'WorkDetailsSerializer' is not defined.
Due to the serializers initialisation, because it seems this creates an infinite loop as explained in this Django: Creating Nested Objects with Reverse Relationship
post.
{
"user":2,
"price":
{
"price":20,
"price_currency":"EUR",
"total":32
},
"name":"work 42",
"length":"50",
"width":"60",
}
Here is the result:
{
"id": 33,
"user": {
"id": 2,
"username": "Max",
"password": "pbkdf2_sha256$180000$WXTaxmhOOTZF$oTx2i/HoZk+lCxHWsRYGVVZcw3/Sy8Micc4YOfaDRaM="
},
"price": null,
"name": "work 42",
"length": 50,
"width": 60
}
I've noticed that I don't enter the "create()" method of the child's serializer.
Does anyone know how to pass to the child the parent's id as foreign key?
Is that done in the "create()" method, and if yes then how can I access it?
This is because you need to pass the newly created Work instance to your Price serializer. This is done through the "create()" method in your WorkCreateSerializer.
class WorkCreateSerializer(serializers.ModelSerializer):
"""
Serializer to create a new Work model in DB
"""
price = PriceCreateSerializer()
class Meta:
model = Work
fields = [
'user',
'price',
'name',
'length',
'width',
'height',
'depth',
'weight',
'creation_year',
'description',
'unit_system'
]
def create(self, validated_data):
price_data = validated_data.pop('price')
work = Work.objects.create(**validated_data)
Price.objects.create(work=work, **price_data)
return art_piece
As you can see in the line below, you create a new Price object to which you pass to its field "work" (from the Price model) the newly created "work" instance from the line above.
This other post explains it well too: create() argument after ** must be a mapping, not unicode
Concerning your issue with accessing the "create()" method from the PriceCreateSerializer, I do not know why you don't access it.
Hope this helps!

Filtering results of querying a model based on ids of related ManyToMany another Model ids

I have 2 models
class Tag(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Question(models.Model):
ques_id = models.IntegerField(default=0)
name = models.CharField(max_length=255)
Tag_name = models.ManyToManyField(Tag)
class Meta:
ordering = ['ques_id']
def __str__(self):
return self.name
searlizers.py
class TagSerializers(serializers.ModelSerializer):
class Meta:
model = Tag
fields = '__all__'
class QuestionSerializers(serializers.ModelSerializer):
class Meta:
model = Question
fields = '__all__'
This is my searilzers class.
I want the response like
{
"id": 1,
"name": "QUES 1",
"tags": [{
"id": 1,
"name": "Abcd"
}]
}
what will be query to get Fetch 10 questions, given some input tag ids
e.g Tag_id = 1 or 2 or 3.
You need to add tags field as another serializer data to your QuestionSerializer.
Your QuestionSerializer code should look like that:
class QuestionSerializers(serializers.ModelSerializer):
Tag_name = TagSerializer(many=True)
class Meta:
model = Question
fields = '__all__'
If you want exactly tags name in response, you can specify SerializerMethodField like that:
class QuestionSerializer(serializers.ModelSerializer):
tags = serializers.SerializerMethodField()
get_tags(self, instance):
return TagSerializer(instance.Tag_name, many=True).data
class Meta:
model = Question
fields = ('ques_id', 'name', 'tags')
First: I would suggest that you refactor your Question Model, since it has a ques_id, and I think it is considered a duplicate (since Django already creates an id field by default)
Then You need to change your ManyToManyField's name to tags, and makemigrations, then migrate
class Question(models.Model):
name = models.CharField(max_length=255)
tags = models.ManyToManyField(Tag)
class Meta:
ordering = ['id']
def __str__(self):
return self.name
# run make migrations
python manage.py makemigrations <<<YOUR QUESTIONS APP NAME>>>
# It will prompt you to check if you change the many to many relationship say yes
Did you rename question.Tag_name to question.tags (a ManyToManyField)? [y/N] y
# Then Run migrate
python manage.py migrate
Second: Update your QuestionSerializers to make the tags field serialize the relation
class QuestionSerializers(serializers.ModelSerializer):
tags = TagSerializers(many=True)
class Meta:
model = Question
fields = '__all__'
This way you make your code cleaner. And you are good to go.
Answer Updated (Filtering, and Pagination)
Now if you wanted to filter questions based on provided tag ids.
You need to use PageNumberPagination for your view, and for filtering use DjangoFilterBackend.
I recommend you to make them the default of DRF settings.
Make sure you have django-filter installed.
# In your settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
),
'PAGE_SIZE': 10
}
Now create your custom filter
# Inside filters.py
import re
import django_filters
from questions.models import Question
class QuestionsFilterSet(django_filters.FilterSet):
tags = django_filters.CharFilter(field_name='tags__id', method='tags_ids_in')
def tags_ids_in(self, qs, name, value):
ids_list = list(map(lambda id: int(id), filter(lambda x: x.isdigit(), re.split(r'\s*,\s*', value))))
return qs.filter(**{f"{name}__in": ids_list}).distinct()
class Meta:
model = Question
fields = ('tags',)
Now use ListAPIView to list your questions
class QuestionsListAPIView(ListAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializers
filter_class = QuestionsFilterSet
Now if you hit
http://<PATH_TO_VIEW>?tags=1,2,3
You will receive all Questions that have tag id 1, 2, or 3.
and all of them will be paginated 10 results at a time.
You just need to write a field tags like this in your QuestionSerializers
class QuestionSerializers(serializers.ModelSerializer):
tags = TagSerializers(source='Tag_name', many=True)
class Meta:
model = Question
fields = ('id', 'name', 'tags')
It will give you response like this
{
"id": 1,
"name": "QUES 1",
"tags": [{
"id": 1,
"name": "Abcd"
}]
}
Your query to fetch on the basis of tags will be like this
Question.objects.filter(Tag_name__in=[1, 2, 4])

Nested serializer "Through model" in Django Rest Framework

I am having difficulty serializing an intermediary "pivot" model and attach to each item in an Many-to-many relation in Django Rest Framework.
Example:
models.py:
class Member(models.Model):
name = models.CharField(max_length = 20)
groups = models.ManyToManyField('Group', through='Membership')
class Group(models.Model):
name = models.CharField(max_length = 20)
class Membership(models.Model):
member = models.ForeignKey('Member')
group = models.ForeignKey('Group')
join_date = models.DateTimeField()
serializers.py:
class MemberSerializer(ModelSerializer):
class Meta:
model = Member
class GroupSerializer(ModelSerializer):
class Meta:
model = Group
class MembershipSerializer(ModelSerializer):
class Meta:
model = Membership
I tried to follow the answers:
Include intermediary (through model) in responses in Django Rest Framework
But it's not exactly what I need
I need to generate the following output
{
"id": 1,
"name": "Paul McCartney",
"groups": [
{
"id": 3,
"name": "Beatles",
"membership": {
"id": 2,
"member_id": 1,
"group_id": 3,
"join_date": "2018-08-08T13:43:45-0300"
}
}
]
}
In this output I'm returning the related "Through Model" for each item in groups.
How can I generate serialize models in this way?
Based on the way you would like to show your output, I suggest you change your models to:
class Group(models.Model):
name = models.CharField(max_length=20)
members = models.ManyToManyField(
'Membership',
related_name='groups',
related_query_name='groups',
)
class Member(models.Model):
name = models.CharField(max_length=20)
class Membership(models.Model):
group = models.ForeignKey(
'Group',
related_name='membership',
related_query_name='memberships',
)
join_date = models.DateTimeField()
How Group model and Member model are ManytoMany, does not have a problem you let the relationship in Group model. It will be the easiest to output it in serialize. related_name and related_query_name are used to make the serialization and point the nested relation.
And finally, your serialize could be like this (I exemplified it with a create method):
class MembershipSerializer(ModelSerializer):
class Meta:
fields = ("id", "join_date",)
class GroupSerializer(ModelSerializer):
memberships = MembershipSerializer(many=True)
class Meta:
model = Group
fields = ("id", "name", "memberships",)
class MemberSerializer(ModelSerializer):
groups = GroupSerializer(many=True)
class Meta:
model = Member
fields = ("id", "name", "groups")
def create(self):
groups_data = validated_data.pop('groups')
member = Member.objects.create(**validated_data)
for group in groups_data:
memberships_data = group.pop('memberships')
Group.objects.create(member=member, **group)
for memberhip in memberships:
Membership.objects.create(group=group, **memberships)
The output will be:
{
"id": 1,
"name": "Paul McCartney",
"groups": [
{
"id": 3,
"name": "Beatles",
"memberships": [
{
"id": 2,
"join_date": "2018-08-08T13:43:45-0300"
}
]
}
]
}
In this output, I am not "nesting" the parent id but you can make it too, just declare into fields attributes.
By looking at your output it seems you want to show membership inside groups and groups inside member. I would recommend editing the serializer to something like this.
class MemberSerializer(ModelSerializer):
groups = GroupSerializer(many=True)
class Meta:
model = Member
fields = ("id","name","groups")
class GroupSerializer(ModelSerializer):
membership = MembershipSerializer()
class Meta:
model = Group
fields = ("id","name","membership")
class MembershipSerializer(ModelSerializer):
class Meta:
model = Membership
fields = "__all__"

Categories