I'm building a simple CRUD interface with Python, GraphQL (graphene-django) and Django. The CREATE mutation for an Object (Ingredient) that includes Foreign Key relations to another Object (Category) won't work. I want to give GraphQL the id of the CategoryObject and not a whole category instance. Then in the backend it should draw the relation to the Category object.
In the Django model the Ingredient Object contains an instance of the Foreign key Category Object (see code below). Is the whole Category Object needed here to draw the relation and to use Ingredient.objects.select_related('category').all()?
The create mutation expects IngredientInput that includes all properties and an integer field for the foreign key relation. So the graphQL mutation itself currently works as I want it to.
My question is similar if not the same as this one but these answers don't help me.
models.py:
class Category(models.Model):
name = models.CharField(max_length=50, unique=True)
notes = models.TextField()
class Meta:
verbose_name = u"Category"
verbose_name_plural = u"Categories"
ordering = ("id",)
def __str__(self):
return self.name
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField()
category = models.ForeignKey(Category, on_delete=models.CASCADE)
class Meta:
verbose_name = u"Ingredient"
verbose_name_plural = u"Ingredients"
ordering = ("id",)
def __str__(self):
return self.name
schema.py:
class CategoryType(DjangoObjectType):
class Meta:
model = Category
class CategoryInput(graphene.InputObjectType):
name = graphene.String(required=True)
notes = graphene.String()
class IngredientType(DjangoObjectType):
class Meta:
model = Ingredient
class IngredientInput(graphene.InputObjectType):
name = graphene.String(required=True)
notes = graphene.String()
category = graphene.Int()
class CreateIngredient(graphene.Mutation):
class Arguments:
ingredientData = IngredientInput(required=True)
ingredient = graphene.Field(IngredientType)
#staticmethod
def mutate(root, info, ingredientData):
_ingredient = Ingredient.objects.create(**ingredientData)
return CreateIngredient(ingredient=_ingredient)
class Mutation(graphene.ObjectType):
create_category = CreateCategory.Field()
create_ingredient = CreateIngredient.Field()
graphql_query:
mutation createIngredient($ingredientData: IngredientInput!) {
createIngredient(ingredientData: $ingredientData) {
ingredient {
id
name
notes
category{name}
}
graphql-variables:
{
"ingredientData": {
"name": "milk",
"notes": "from cow",
"category": 8 # here I ant to insert the id of an existing category object
}
}
error-message after executoin the query:
{
"errors": [
{
"message": "Cannot assign \"8\": \"Ingredient.category\" must be a \"Category\" instance.",
"locations": [
{
"line": 38,
"column": 3
}
],
"path": [
"createIngredient"
]
}
],
"data": {
"createIngredient": null
}
}
I had this same problem today.
The Cannot assign \"8\": \"Ingredient.category\" must be a \"Category\" instance. error is a Django error that happens when you try to create an object using the foreign key integer directly instead of an object.
If you want to use the foreign key id directly you have to use the _id suffix.
For example, instead of using:
_ingredient = Ingredient.objects.create(name="milk", notes="from_cow", category=8)
You have to use either
category_obj = Category.objects.get(id=8)
_ingredient = Ingredient.objects.create(name="milk", notes="from_cow", category=category_obj)
or
_ingredient = Ingredient.objects.create(name="milk", notes="from_cow", category_id=8)
In the case of using GraphQL, you would have to set your InputObjectType field to <name>_id. In your case:
class IngredientInput(graphene.InputObjectType):
name = graphene.String(required=True)
notes = graphene.String()
category_id = graphene.Int()
This, however will make your field in the schema show up as categoryId. If you wish to keep the category name, you must change to:
category_id = graphene.Int(name="category")
Cheers!
Related
I have following 2 models:
class Student(BaseModel):
external_id = models.CharField(max_length=30)
name= models.CharField(max_length=30)
last_name= models.CharField(max_length=30)
class Book(BaseModel):
student = models.OneToOneField(
"student.Student",
related_name="books",
on_delete=models.CASCADE,
)
book_name = models.CharField(max_length=30)
book_type = models.CharField(max_length=30)
I want to create such serializer that will create a Book only if there is a Student with provided external_id.
class BooksSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = [
"student",
"book_name",
"book_type",
]
def create(self, validated_data):
student_external_id = validated_data.pop('student')
st = Student.objects.get(
external_id=student_external_id
)
book = Book.objects.create(
student=st,
book_name=validated_data["book_name"],
book_type=validated_data["book_type"],
)
return book
I have written this code but obviously it doesn't work. I used nested serializers to do similar things but I don't want to create Student this time, only check if exisit's and the use it as relation, otherwise throw error that such student doesn't exists.
Maybe it's bad approach first of all? Maybe I shouldn't try to create separete endpoint for books, and try to use patch method for Student and inside of it create object Book to given Student?
You can use PrimaryKeyRelatedField . It checks sended value exists, if not exists raises ValidationError. Try this:
class BooksSerializer(serializers.ModelSerializer):
student = serializers.PrimaryKeyRelatedField(queryset=Student.objects.all())
class Meta:
model = Book
fields = [
"student",
"book_name",
"book_type",
]
def create(self, validated_data):
book = Book.objects.create(
student=student,
book_name=validated_data["book_name"],
book_type=validated_data["book_type"],
)
return book
If you want to use different field from primary key, then you can use SlugRelatedField. You should add your lookup field with slug_field key.
class BooksSerializer(serializers.ModelSerializer):
student = serializers.SlugRelatedField(queryset=Student.objects.all(), slug_field='external_id')
class Meta:
model = Book
fields = [
"student",
"book_name",
"book_type",
]
def create(self, validated_data):
book = Book.objects.create(
student=validated_data["student"],
book_name=validated_data["book_name"],
book_type=validated_data["book_type"],
)
return book
models.py
class Sequence(models.Model):
category_id = models.ForeignKey(SequenceCategory, on_delete=models.CASCADE)
description = models.TextField()
code = models.CharField(max_length=50)
total_divisions = models.IntegerField()
status = models.BooleanField(default=True)
def __str__(self):
return self.code
class SequenceValue(models.Model):
TYPE_CHOICES =(
('N', 'Numeric'),
('A', 'Alphabet')
)
sequence_id = models.ForeignKey(Sequence, on_delete=models.CASCADE, blank=True, null=True, related_name='Sequence_details')
type = models.CharField(max_length=100, choices=TYPE_CHOICES)
value = models.CharField(max_length=50)
starting_value = models.CharField(max_length=50, null=True, blank=True)
increment_value = models.CharField(max_length=50, null=True, blank=True)
def __str__(self):
return self.value
serializers.py
class SequenceValueSerializer(serializers.ModelSerializer):
class Meta:
model = SequenceValue
# fields = '__all__'
exclude = ['sequence_id']
class SequenceSerializer(serializers.ModelSerializer):
Sequence_details = SequenceValueSerializer()
class Meta:
model = Sequence
fields = '__all__'
def create(self, validate_data):
Sequence_details_data = validate_data.pop('Sequence_details')
sequence_id = Sequence.objects.create(**validate_data)
SequenceValue.objects.create(sequence_id=sequence_id, **Sequence_details_data)
return sequence_id
views.py
class SequenceCreate(ListCreateAPIView):
queryset = Sequence.objects.all()
serializer_class = SequenceSerializer
Why do I get the error? When I refer to n number of articles I got a solution that put many=True something like this.
Sequence_details = SequenceValueSerializer(many=True)
But when I make changes on that then I can't get the Sequence details fields. How can I fix this issue? Can you give a solution, please?
Actual error-
Got AttributeError when attempting to get a value for field type on serializer SequenceValueSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the RelatedManager instance.
Original exception text was: 'RelatedManager' object has no attribute 'type'.
Data Passess
Data - {
"Sequence_details": [{
"type": "N",
"value": "123",
"starting_value": "1",
"increment_value": "1"
}],
"description": "asd",
"code": "qwe",
"total_divisions": 2,
"status": false,
"category_id": 3
}
Actual eroor screenshot
Adding many=True works because your request data passes Sequence_details as a list. In order to use it in your serializer, you can just iterate through the field and create your objects like this:
class SequenceSerializer(serializers.ModelSerializer):
Sequence_details = SequenceValueSerializer(many=True)
class Meta:
model = Sequence
fields = '__all__'
def create(self, validate_data):
Sequence_details_data = validate_data.pop('Sequence_details')
sequence_id = Sequence.objects.create(**validate_data)
for details in Sequence_details_data
SequenceValue.objects.create(sequence_id=sequence_id, **details)
return sequence_id
But if you don't want to do this, you can remove many=True and keep your serializer as is, but ensure that Sequence_details in your request data is not a list:
{
"Sequence_details": {
"type": "N",
"value": "123",
"starting_value": "1",
"increment_value": "1"
},
"description": "asd",
"code": "qwe",
"total_divisions": 2,
"status": false,
"category_id": 3
}
I can't comment so I will just mention it here :). type is actually a function in python which tells you about the class type of argument. I am not sure if this the issue but the can you try by renaming type to _type or something of your choice.
I am using Django rest framework and I am trying to do a JOIN on a GET command.
I have the following view:
class CharacterView(APIView):
permission_classes = (AllowAny,)
def get(self, request, user_id):
character_saves = Char.objects.select_related('character').filter(
user_id=user_id)
serializer = CharacterSerializer(character_saves, many=True)
return Response({"characters": serializer.data})
And the following serializer:
class CharacterSerializer(serializers.Serializer):
character_id = serializers.IntegerField()
user_id = serializers.IntegerField()
active = serializers.BooleanField()
character = serializers.CharField()
def create(self, validated_data):
return Char.objects.update_or_create(
user_id=validated_data.pop('user_id'),
character_id=validated_data.pop('character_id'),
defaults=validated_data
)
Yet, I am getting the following data:
"characters": [
{
"character_id": 256,
"user_id": 1,
"active": true,
"character": "Dictionary object (256)"
},
{
"character_id": 260,
"user_id": 1,
"active": true,
"character": "Dictionary object (260)"
}
]
Instead of giving me the actual item I want, it gives me a dictionary object. This is almost correct, however I am guessing I am configuring something wrong in my query or serializer, preventing me from getting the raw value.
How do I get the actual value here, rather than "Dictionary object (260)"?
Char model:
class Char(models.Model):
character = models.ForeignKey(
Dictionary, on_delete=models.CASCADE)
user = models.ForeignKey(
User, on_delete=models.CASCADE)
active = models.BooleanField()
Dictionary model:
class Dictionary(models.Model):
traditional = models.CharField(max_length=50)
simplified = models.CharField(max_length=50)
pinyin_numbers = models.CharField(max_length=50)
pinyin_marks = models.CharField(max_length=50)
translation = models.TextField()
level = models.IntegerField()
class Meta:
db_table = 'dictionary'
tredzko's answer is correct, but you have asked in the comment box that you like to put several fields from dictionary into the serialized output, so for that part you can do like this,
Class DictionarySerializer(models.Model):
class Meta:
model = Dictionary
fields = (....) # Your specified fields goes here
class CharacterSerializer(serializers.Serializer):
character_id = serializers.IntegerField()
user_id = serializers.IntegerField()
active = serializers.BooleanField()
character = DictionarySerializer() # simply add this
EDIT: No need to override the create method.
Because you specified a CharField for the Dictionary relation, it's trying to get the string representation of your Dictionary model. One way you could fix it is to set the str representation on the model:
class Dictionary(models.Model):
...
def __str__(self):
return self.traditional
Alternatively, you should be able to alter the source of the character field on your serializer to instead pull from a property from the relation.
class CharacterSerializer(serializers.Serializer):
...
character = serializers.CharField(source="character.traditional")
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!
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__"