Handling ForeignKey in GraphQL (graphene) - python

I'm using Django-Graphene to build and API for my webapp. I've built this simple structure where I have Product that belong to a category:
from graphene_django import DjangoObjectType
import graphene
from .models import ProductModel, CategoryModel
class CategoryType(DjangoObjectType):
class Meta:
model = CategoryModel
class ProductType(DjangoObjectType):
category = graphene.Field(CategoryType)
class Meta:
model = ProductModel
class CategoryInput(graphene.InputObjectType):
title = graphene.String()
class ProductInput(graphene.InputObjectType):
title = graphene.String()
category = graphene.Field(CategoryInput)
class Query(graphene.ObjectType):
products = graphene.List(ProductType)
categories = graphene.List(CategoryType)
def resolve_products(self, info):
return ProductModel.objects.all()
def resolve_categories(self, info):
return CategoryModel.objects.all()
class CreateProduct(graphene.Mutation):
class Arguments:
product_data = ProductInput(required=True)
product = graphene.Field(ProductType)
#staticmethod
def mutate(self, info, product_data):
product = ProductModel.objects.create(**product_data)
product.save()
return CreateProduct(product=product)
class CreateCategory(graphene.Mutation):
class Arguments:
title = graphene.String()
category = graphene.Field(CategoryType)
def mutate(self, info, title):
category = CategoryModel(title=title)
category.save()
return CreateCategory(category=category)
class Mutations(graphene.ObjectType):
create_product = CreateProduct.Field()
create_category = CreateCategory.Field()
schema = graphene.Schema(query=Query, mutation=Mutations)
But when I try to mutate, I need to pass an instance of the CategoryModel, which I can't figure out how to do:
mutation X {
createProduct(productData: {title: "title", category: {}}) {
product {
id
}
}
}
How can I create products which require a title and a category?

I once had similar problem yesterday, but seems I haven't gotten a better grasp of GRAPHENE. But I wrote some funny codes which worked, tho there could be a more graphene way of solving it.
class ProductInput(graphene.InputObjectType):
name = graphene.String()
class ProductPayload(graphene.Mutation):
product = graphene.Field(ProductType)
class Meta:
input = ProductInput(required=True)
category_name = graphene.String(required=True)
def mutate(self, info, input=None, category_name=None):
cat = None
if category_name is not None:
cat = Category.objects.get(name=category_name)
product = Product.objects.create(name=input.name, category=cat)
return ProductPayload(product=product)
class ProductMutation(graphene.ObjectType):
create_product = ProductPayload.Field()
schema = graphene.Schema(mutation=ProductMutation
To make a mutation in your graphiql,
mutation {
createProduct(input: {name: "Your product name"}, category_name: "your category name") {
product {
name,
category {
name
}
}
}
}

Related

Nested Serializers with different queryset

How to create a nested serializer with it's own queryset?
In the following example I would like to replace an '#api_view' function with a class based view with serializers.
Simplified, I have the following code:
models.py
class Klass(models.Model):
name = models.TextField()
class Pupils(models.Model):
name = models.TextField()
klass = models.ForeignKey(Klass)
class Chapter(models.Model):
"""A Chapter in a school book."""
name = models.TextField()
class TestResult(models.Model):
"""TestResults for a Chapter."""
pupil = models.ForeignKey(Pupil)
chapter = models.ForeignKey(Chapter)
score = models.IntegerField()
view.py
#api_view
def testresults(request, klass_id, chapter_id):
pupils = Pupils.objects.filter(klas__id=klass_id)
tests = Tests.objects.filter(chapter__id=chapter_id)
ret_val = []
for pupil in pupils:
pu = {
"name": pupil.name,
"tests": [{"score": test.score,
"chapter": test.chapter.name} for test in tests.filter(pupil=pupil)]
}
ret_val.append(pu)
return Response({"pupils": ret_val})
url
/api/testresult/<klass_id>/<chapter_id>/

DRF Create new object and link to (but not create) it's related items (nested serializer)?

I'm trying to link multiple items (subdomains) to an item being created (evidence).
My form submits okay - but I'm trying to figure out the 'best' way to go about this.
According to the docs I have to override the create method - but their example shows them 'creating' the related objects.
I don't want to do that. I want to just just add those related items to the piece of evidence I am creating (to create the relationship)
Here are my serializers:
class SubdomainSerializer(serializers.ModelSerializer):
class Meta:
model = Subdomain
fields = [
"id",
"domain",
"short_description",
"long_description",
"character_code",
]
class EvidenceSerializer(serializers.ModelSerializer):
"""
"""
created_by = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
updated_by = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
absolute_url = serializers.SerializerMethodField()
created_by_name = serializers.SerializerMethodField()
updated_by_name = serializers.SerializerMethodField()
subdomains = SubdomainSerializer(many=True)
class Meta:
model = Evidence
fields = "__all__"
extra_kwargs = {
"subdomains": {
"error_messages": {
"null": "This field is required.",
"blank": "This field is required.",
}
},
}
def get_absolute_url(self, obj):
return obj.get_absolute_url()
def get_created_by_name(self, obj):
return obj.created_by.full_name
def get_updated_by_name(self, obj):
return obj.updated_by.full_name
def create(self, validated_data):
subdomains_data = validated_data.pop("subdomains")
evidence = Evidence.objects.create(**validated_data)
for subdomain in subdomains_data:
# This is where I want to add the subdomains to the evidence - not create ones - how best to accomplish that?
Subdomain.objects.create(**subdomain)
return evidence
def update(self, instance, validated_data):
# Add the requestor as the updater in a PATCH request
request = self.context["request"]
validated_data["updated_by"] = request.user
return super().update(instance, validated_data)
Is there a better way to setup my serializers? Should I be doing something different to make this more achievable?
Edited to add my models:
class Subdomain(CreateUpdateMixin):
"""
"""
domain = models.ForeignKey(Domain, on_delete=models.PROTECT)
short_description = models.CharField(max_length=100)
long_description = models.CharField(max_length=250)
character_code = models.CharField(max_length=5)
proficiency_levels = models.ManyToManyField(SubdomainProficiencyLevel)
class Meta:
verbose_name = "Subdomain"
verbose_name_plural = "Subdomains"
def __str__(self):
"""Unicode representation of Subdomain."""
return f"{self.character_code}"
class Evidence(CreateUpdateMixin, CreateUpdateUserMixin, SoftDeletionModel):
"""
"""
subdomains = models.ManyToManyField(Subdomain, related_name="evidences")
evaluation = models.ForeignKey(
Evaluation, related_name="evidences", on_delete=models.PROTECT
)
published = models.BooleanField(default=False)
comments = models.CharField(max_length=500)
class Meta:
ordering = ["-created_at"]
verbose_name = "Evidence"
verbose_name_plural = "Evidence"
def __str__(self):
"""Unicode representation of Evidence."""
return f"{self.subdomain} : {self.comments}"
def get_absolute_url(self):
"""Return absolute url for Evidence."""
return reverse("evidence-detail", args=[str(self.id)])
Here you want to set the subdomain ForeignKey to the new created evidence.
For that, you just have to retrieve the subdomain and update his evidence field like this :
def create(self, validated_data):
subdomains_data = validated_data.pop("subdomains")
evidence = Evidence.objects.create(**validated_data)
for subdomain in subdomains_data:
# Retrieve the subdomain and update the evidence attribute
sub_id = subdomain['id']
sub = Subdomain.objects.get(id=sub_id)
sub.evidence = evidence
sub.save()
return evidence
NB : I use the default inverse ForeignKey relationship : sub.evidence

GraphQL Mutation in Graphene for Object with Foreign Key Relation

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!

Django queryset to JSON object nested by primary key

I'm trying to parse a queryset into a JSON object, so that each key is the primary key of the model, and each value is a JSON object containing all other fields.
myjson = {
apple: { color: "r", calories: 10},
banana: { color: "w", calories: 50}
​ }
Here is the model and Django view collecting the data (based on this blog post):
class Fruit(models.Model):
fruit_id = models.CharField(primary_key=True)
color = models.CharField(max_length=50)
calories = models.IntegerField()
def get_FruitsTableDjango(request):
fruits_table = Fruit.objects.all().values()
fruits_table_list = list(fruits_table ) # important: convert the QuerySet to a list object
return JsonResponse(fruits_table_list , safe=False)
But on the client's side (via AJAX), this returns an array of objects:
mydata = [
0: { fruit_id: "apple", color: "r", calories: 10},
1: { fruit_id: "banana", color: "w", calories: 50}
]
I found also here how I can rework this array as expected :
//Restructure JSON by fruit_id name
fruits= {},
mydata.forEach(function (a) {
var temp = {};
Object.keys(a).forEach(function (k) {
if (k === 'fruit_id') {
fruits[a[k]] = temp; //gets fruit_id
return;
}
temp[k] = a[k]; //fill the temp variable with elements
});
});
mydata=fruits; //overwrite initial array with nicely-rearranged-by-fruitId object
I have basically two questions:
Is there a more direct way to obtain the desired JSON (nested by primary keys)?
If not, where is objectively the best place to perform the object-parsing logic: on the client's side in Javascript (like above), or on the server side, e.g. in the Django view?
​​
In DRF,
Let's say I have model.py as below
from django.db import models
class OrganisationalUnitGrouper(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
#property
def unit(self):
return self.organisationalunit_set.all()
class OrganisationalUnit(models.Model):
unit_id = models.CharField(max_length=255)
name = models.CharField(max_length=255)
display = models.CharField(max_length=255)
description = models.CharField(max_length=255)
path = models.CharField(max_length=255)
ou_group = models.ForeignKey(OrganisationalUnitGrouper, on_delete=models.PROTECT)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = "OU Unit"
Serializer you can define like
from rest_framework import serializers
from .models import OrganisationalUnitGrouper, OrganisationalUnit
class OUSerializer(serializers.ModelSerializer):
# id = serializers.IntegerField(required=False)
class Meta:
model = OrganisationalUnit
fields = ("unit_id", "name", "display", "description", "path")
class OUGroupSerializer(serializers.ModelSerializer):
unit = OUSerializer(many=True)
class Meta:
model = OrganisationalUnitGrouper
fields = ("name", "unit")
And viewset could be
from rest_framework import viewsets
from .serializers import OUSerializer, OUGroupSerializer
from .models import OrganisationalUnitGrouper, OrganisationalUnit
class OUGroupViewSet(viewsets.ModelViewSet):
queryset = OrganisationalUnitGrouper.objects.all()
serializer_class = OUGroupSerializer
class OUViewSet(viewsets.ModelViewSet):
queryset = OrganisationalUnit.objects.all()
serializer_class = OUSerializer
This will give you nested API.

Custom create method to prevent duplicates

I would like to add some logic to my serializer.py.
Currently it creates duplicate tags (giving a new ID to the item, but often it will match a tag name already).
In plain english
if exists:
# Find the PK that matches the "name" field
# "link" the key with Movie Class item
else:
# Create the "name" inside of Tag class
# "link" the key with Movie Class item
The data being posted looks like this:
{
"title": "Test",
"tag": [
{
"name": "a",
"taglevel": 1
}
],
"info": [
]
}
Models.py
class Tag(models.Model):
name = models.CharField("Name", max_length=5000, blank=True)
taglevel = models.IntegerField("Tag level", blank=True)
def __str__(self):
return self.name
class Movie(models.Model):
title = models.CharField("Whats happening?", max_length=100, blank=True)
tag = models.ManyToManyField('Tag', blank=True)
def __str__(self):
return self.title
Serializers
class MovieSerializer(serializers.ModelSerializer):
tag = TagSerializer(many=True, read_only=False)
class Meta:
model = Movie
fields = ('title', 'tag', 'info')
def create(self, validated_data):
tags_data = validated_data.pop('tag')
movie = Movie.objects.create(**validated_data)
for tag_data in tags_data:
movie.tag.create(**tag_data)
return movie
This will probably solve your issue:
tag = Tag.objects.get_or_create(**tag_data)[0]
movie.tag.add(tag)
get_or_create function returns tuple (instance, created), so you have to get instance with [0].
So the full code is:
def create(self, validated_data):
tags_data = validated_data.pop('tag')
movie = Movie.objects.create(**validated_data)
for tag_data in tags_data:
tag = Tag.objects.get_or_create(**tag_data)[0]
movie.tag.add(tag)
return movie
To make function case insensitive, you have to do "get or create" manually (the main part is to use __iexact in filter):
tag_qs = Tag.objects.filter(name__iexact=tag_data['name'])
if tag_qs.exists():
tag = tag_qs.first()
else:
tag = Tag.objects.create(**tag_data)
movie.tag.add(tag)

Categories