Return any data from a query using GraphQL, Graphene and Python - graphql

I am receiving the following error:
{
"errors": [
{
"message": "Unknown argument \"project_id\" on field" +
\"get_project_detail_summary\" of type \"Query\".",
"locations": [
{
"line": 2,
"column": 30
}
]
}
]
}
With the following query:
query GetProjectDetailSummary($project_id: Int) {
get_project_detail_summary(project_id: $project_id) {
comments {
... on ManagerCommentNode {
id
text
created
}
... on VendorCommentNode {
id
text
created
}
... on TenantCommentNode {
id
text
created
}
}
}
}
With the following backend code, How can I get to the breakpoint?, or how do I send back custom data given a number?:
class CommentsUnion(graphene.types.union.Union):
class Meta:
name = 'CommentsUnion'
types = (ManagerCommentNode, VendorCommentNode, TenantCommentNode, )
class ProjectSummaryInput(graphene.InputObjectType):
project_id = graphene.Int()
class ProjectSummaryNode(graphene.ObjectType):
Input = ProjectSummaryInput
project_id = graphene.Int()
comments = graphene.List(CommentsUnion)
#classmethod
def resolve_comments(self, *args, **kwargs):
import pdb;pdb.set_trace()
return ProjectSummary.select_related('comments').objects.filter(comments__created__lt=dt)
class Query(graphene.ObjectType):
get_project_detail_summary = Field(ProjectSummaryNode)

In regards to the error.
Be sure to add a kwarg ( e.g. project_id in this example, it is the reason for the "unknown argument on field" error ) to the graphene.Field for get_project_detail_summary.
Like so:
class Query(graphene.ObjectType):
get_project_detail_summary = Field(ProjectSummaryNode, # see below for example
project_id=graphene.Int() # kwarg here
)
def resolve_get_project_detail_summary(self, info, **kwargs):
return ProjectSummary.objects.get(id=kwargs.get('project_id'))
In regards to returning any data.
This is a way ( returning a graphene.String ), but it untypes the response by putting everything inside a String:
from django.core.serializers.json import DjangoJSONEncoder
class ProjectSummaryRecentUpdatesNode(graphene.ObjectType):
Input = ProjectSummaryInput
recent_updates = graphene.String()
def resolve_recent_updates(self, resolve, **kwargs):
instance = Project.objects.get(id=resolve.variable_values.get("project_id"))
things = instance.things.all()
# these are all from different models, and the list is a bit longer than this.
querysets = (
("scheduled", get_scheduled(things, resolve, **kwargs)),
("completed", get_completed(things, resolve, **kwargs)),
("invoices", get_invoices(things, resolve, **kwargs)),
("expenditures", get_expenditures(things, resolve, **kwargs)),
("comments", get_comments(things, resolve, **kwargs)),
("files", get_files(things, resolve, **kwargs)),
)
recent_updates = []
for update_type, qs in querysets:
for item in qs:
item.update(
{
"recent_update_type": update_type
}
)
recent_updates.append(item)
return json.dumps(recent_updates, cls=DjangoJSONEncoder)
And on the frontend, with an import { Query } from "react-apollo"; element we can JSON.parse the field ... which has "any" (json serialized) data inside of it:
<Query
query={GET_PROJECT_DETAIL_SUMMARY_RECENT_UPDATES}
fetchPolicy="network-only"
variables={{ project_id: this.props.projectId }}
>
{({ loading, error, data }: QueryResult) => {
if (
data &&
data.get_project_detail_summary_recent_updates &&
data.get_project_detail_summary_recent_updates.recent_updates
) {
console.log(JSON.parse(data.get_project_detail_summary_recent_updates.recent_updates))
}
}}
</Query>
Side note:
If there isn't a large list of data types create a Union like object which has all of the fields needed from different models, or an actual Union, which seems like the correct way, as then types are not lost:
class ProjectSummaryNode(graphene.ObjectType):
# from FileNode graphene.ObjectType class
file__id = graphene.Int()
file__brief_description = graphene.String()
file_count = graphene.Int()
last_file_created = graphene.String()
last_file = graphene.String()
# from UploaderNode graphene.ObjectType class
uploader__first_name = graphene.String()
uploader__last_name = graphene.String()
# from CommentNode graphene.ObjectType class
comment = graphene.String()
class Meta:
name = "ProjectSummaryNode"
# example of Union, the above fields would be commented out, as they are part of
# the different graphene.ObjectType classes, and this line below would be uncommented
# types = ( FileNode, UploaderNode, CommentNode, )
Still open to suggestions on a better way to do this.

Related

DRF: problem with writing nested serializer with unique field

I need to add some tags to a post when creating it. They have a many to many relationship, and tag has a unique name field. But I get an already exists error.
Here is my setup:
class Tag(models.Model):
name = models.SlugField(max_length=100, unique=True)
class Post(models.Model):
(...)
tags = models.ManyToManyField(Tag, related_name='posts')
class PostSerializer(serializers.HyperlinkedModelSerializer):
tags = TagSerializer(many=True)
def create(self, validated_data):
tags_data = validated_data.pop('tags')
post = Post.objects.create(**validated_data)
for tag_data in tags_data:
try:
tag = Tag.objects.get(name=tag_data['name'])
except Tag.DoesNotExist:
tag = Tag.objects.create(**tag_data)
post.tags.add(tag)
return post
class Meta:
model = Post
(...)
Now when I post the following data to create a Post:
{
(...),
"tags": [{"name": "someExistentTag"}, {"name": "someTag"}]
}
serializer.is_valid is called prior to create and I get the following response:
{
"tags": [
{
"name": [
"tag with this name already exists."
]
},
{}
]
}
What is your solution?
Here is the first thing I got working; get tags away from post and validate them manually (which I'm not sure is done right). Yet I'd like to see a better solution.
class PostSerializer(HyperlinkedModelSerializer):
tags = TagSerializer(many=True)
def __init__(self, instance=None, data=empty, **kwargs):
super().__init__(instance, data, **kwargs)
if hasattr(self, 'initial_data'):
self.tags = self.initial_data.get('tags', [])
if 'tags' in self.initial_data:
self.initial_data['tags'] = []
def create(self, validated_data):
tags_data = self.tags
existing_tags = []
new_tags_data = []
for tag_data in tags_data:
try:
tag = Tag.objects.get(name=tag_data['name'])
except KeyError:
raise ValidationError("Field 'name' for tag is required.")
except Tag.DoesNotExist:
new_tags_data.append(tag_data)
else:
existing_tags.append(tag)
new_tags_serializer = TagSerializer(data=new_tags_data, many=True)
new_tags_serializer.is_valid(raise_exception=True)
validated_data.pop('tags')
post = Post.objects.create(**validated_data)
for tag_data in new_tags_data:
tag = Tag.objects.create(**tag_data)
post.tags.add(tag)
for tag in existing_tags:
post.tags.add(tag)
return post

How to query nested object in GraphQL?

I am new to GraphQL and MongoDB and try to build a backend using both.
I have followed the sample on https://github.com/graphql-python/graphene-mongo/tree/master/examples/flask_mongoengine and somehow able to build one. But I am not able to query the nested object, I am not sure if it is the issue of my code or my query?
models.py
from mongoengine import connect, Document, EmbeddedDocument
from mongoengine.fields import (
StringField,
IntField,
ReferenceField,
ListField,
EmbeddedDocumentField,
ObjectIdField
)
class author(Document):
meta = {'db_alias': 'db', 'collection': 'author'}
acronym = StringField()
name = StringField()
notes = StringField()
class author_list(EmbeddedDocument):
sequence = IntField()
author = ReferenceField(author)
class book(Document):
meta = {'db_alias': 'db', 'collection': 'book'}
publish_date = StringField()
title = StringField()
author_list = ListField(EmbeddedDocumentField(author_list))
category = StringField()
schema.py
import graphene
from graphene.relay import Node
from graphene_mongo import MongoengineConnectionField, MongoengineObjectType
from models import author as AuthorModel
from models import author_list as AuthorListModel
from models import book as BookModel
class AuthorType(MongoengineObjectType):
class Meta:
model = AuthorModel
interfaces = (Node,)
class AuthorListType(MongoengineObjectType):
class Meta:
model = AuthorListModel
interfaces = (Node,)
class BookType(MongoengineObjectType):
class Meta:
model = BookModel
interfaces = (Node,)
class Query(graphene.ObjectType):
node = Node.Field()
books = MongoengineConnectionField(BookType)
authors = MongoengineConnectionField(AuthorType)
schema = graphene.Schema(query=Query, types=[BookType, AuthorListType, AuthorType])
Query
{
books {
edges {
node {
authorList{
edges {
node {
sequence
author(acronym: "CC") {
name
}
}
}
}
}
}
}
}
And I receive the following error:
{
"errors": [
{
"message": "Unknown argument \"acronym\" on field \"author\" of type \"AuthorListType\".",
"locations": [
{
"line": 10,
"column": 20
}
]
}
]
}

How to remove the nested input object in the Graphene Django mutation query (Relay)?

I want to create a Mutation in Relay. I'm using InputObjectType pattern to separate the input and make it reusable.
In mutation class I'm using Input class and there I'm passing the InputObjectType
In general it works but the final query at the client side is very ugly.
I need to pass arguments in this way
query( input : { input : { ...arguments } } )
and to be honest I don't like it. I think it looks ugly.
So the question is: Is it possible to avoid to use a lot of these input objects?
It's ok to use 1 input object, but the nested one is redundant and I'd like to avoid to use it.
Thanks for any help!
Here is the example
class FuelTypeInput(graphene.InputObjectType):
id = graphene.Int()
label = graphene.String()
class FuelSubtypeInput(graphene.InputObjectType):
id = graphene.ID()
label = graphene.String()
fuel_type = graphene.Field(FuelTypeInput)
class CreateFuelSubType(relay.ClientIDMutation):
class Input:
input = FuelSubtypeInput(required=True)
fuel_subtype = Field(FuelSubTypeNode)
ok = graphene.Boolean()
def mutate_and_get_payload(root, info, input):
label = input.label
fuel_type = FuelType.objects.get(pk=input.fuel_type.id)
fuel_subtype = FuelSubType(label=label, fuel_type=fuel_type)
ok = True
return CreateFuelSubType(fuel_subtype=fuel_subtype, ok=ok)
The mutation query is:
mutation MyMutations {
createFuelSubtype( input: { input : { label: "Mutation Subtype", fuelType: {
id: 3
}} } ) {
fuelSubtype {
label
}
ok
}
}
It works fine, here is the result. But I'd like to remove the nested input things
{
"data": {
"createFuelSubtype": {
"fuelSubtype": {
"label": "Mutation Subtype"
},
"ok": true
}
}
}
you can fix with this:
class FuelTypeInput(graphene.AbstractType):
id = graphene.Int()
label = graphene.String()
class CreateFuelSubType(relay.ClientIDMutation):
Input = FuelSubtypeInput
fuel_subtype = Field(FuelSubTypeNode)
ok = graphene.Boolean()
# Other Code ...

Nested serializer doesn't pick up correct ID

There are two models, they are defined this way:
class ShoppingList(models.Model):
id = models.CharField(max_length=40, primary_key=True)
name = models.CharField(max_length=40)
session_id = models.CharField(max_length=40)
config_file = models.FileField(upload_to=upload_config_file)
def __str__(self):
return self.id
class FetchedData(models.Model):
model_id = models.CharField(max_length=40)
config_id = models.ForeignKey(BillOfMaterial, on_delete=models.CASCADE, default=0)
config_name = models.CharField(max_length=40)
def __str__(self):
return self.model_id
And serialized like this:
class FetchedDataSerializer(serializers.ModelSerializer):
file_fields = serializers.SerializerMethodField()
class Meta:
model = FetchedData
fields = ('model_id', 'config_id', 'config_name', 'file_fields')
def get_file_fields(self, obj):
print(obj)
# queryset = ShoppingList.objects.filter(config_file = obj) ## (1)
queryset = BillOfMaterial.objects.all() ## (2)
return [ShoppingListSerializer(cf).data for cf in queryset]
I was advised* to implement the solution marked as (1) in the serializer above, but when it's on, I get responses with an empty array, for example:
[
{
"model_id": "6553",
"config_id": "2322",
"config_name": "Config No. 1",
"file_fields": []
}
]
Meanwhile, with option (2) turned on and option (1) commented out, I get all the instances displayed:
[
{
"model_id": "6553",
"config_id": "2322",
"config_name": "Config No. 1",
"file_fields": [
{
"id": "2322",
"name": "First Example",
"session_id": "9883",
"config_file": "/uploads/2322/eq-example_7DQDsJ4.json"
},
{
"id": "4544",
"name": "Another Example",
"session_id": "4376",
"config_file": "/uploads/4544/d-jay12.json"
}
]
}
]
The print(obj) method always gives a model_id value. And it should output file_fields.id, I guess.
How should I re-build this piece of code to be able to display only the file_field with id matching config_id of the parent?
*This is a follow-up of an issue described here: TypeError: 'FieldFile' object is not callable
In FetchedData model I added this method:
def config_link(self):
return self.config_id.config_file
(it binds config_file from ShoppingList model).
FetchedDataSerializer should then look like this:
class FetchedDataSerializer(serializers.ModelSerializer):
file_link = serializers.SerializerMethodField()
class Meta:
model = FetchedData
fields = ('model_id', 'config_id', 'config_name', 'file_link')
def get_file_link(self, obj):
return obj.config_link()

Getting rest history from Django simple History

I am using django-simple-history (1.8.1) and DRF (3.5.3). I want to get a rest service containing the history of each element. Let's take an example !
models.py
class Product(models.Model):
name = models.CharField(max_length=50)
price = models.IntegerField()
history = HistoricalRecords()
def __str__(self):
return self.name
So, what must be serializers.py ? I'd like to GET something like :
[
{
"id": 1,
"name": "Apple",
"price": 8,
"history": [
{
"history_id": 1,
"id": 1,
"name": "Apple",
"price": 0,
"history_date": "2016-11-22T08:02:08.739134Z",
"history_type": "+",
"history_user": 1
},
{
"history_id": 2,
"id": 1,
"name": "Apple",
"price": 10,
"history_date": "2016-11-22T08:03:50.845634Z",
"history_type": "~",
"history_user": 1
},
{
"history_id": 3,
"id": 1,
"name": "Apple",
"price": 8,
"history_date": "2016-11-22T08:03:58.243843Z",
"history_type": "~",
"history_user": 1
}
]
}
]
After searching whitout finding the solution, I finally found it by myself. But if someone have a better solution...
I know it's been a year, but anyway, maybe someone finds it useful. Here is my solution (it seems far easier to me):
A new serializer field:
class HistoricalRecordField(serializers.ListField):
child = serializers.DictField()
def to_representation(self, data):
return super().to_representation(data.values())
Now simply use it as a a field in your serializer:
history = HistoricalRecordField(read_only=True)
This makes use of DRF's built in list and dict serializers, only trick is to pass it the correct iterable, which is being done by calling .values() on the simple-history model manager class.
Here's my solution.
In serializers.py :
from rest_framework import serializers
from .models import Product
class sHistory(serializers.ModelSerializer):
def __init__(self, model, *args, fields='__all__', **kwargs):
self.Meta.model = model
self.Meta.fields = fields
super().__init__()
class Meta:
pass
class sProduct(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
history = serializers.SerializerMethodField()
def get_history(self, obj):
model = obj.history.__dict__['model']
fields = ['history_id', ]
serializer = sHistory(model, obj.history.all().order_by('history_date'), fields=fields, many=True)
serializer.is_valid()
return serializer.data
It works ! I'm quite proud about it ! any suggestions ?
There seems to be an even clearer and simpler way
class AnySerializer(serializers.ModelSerializer):
history = serializers.SerializerMethodField()
class Meta:
model = MyModel
fields = (....
....
'history',
)
read_only_fields = ('history',)
def get_history(self, obj):
# using slicing to exclude current field values
h = obj.history.all().values('field_name')[1:]
return h
You can create a serializer like this:
class ProductHistorySerializer(serializers.ModelSerializer):
class Meta:
model = Product.history.model
fields = '__all__'
Then in view, You can have the code below:
#...
logs = ProductHistorySerializer(Product.history.filter(price__gt=100), many=True)
return Response({'isSuccess': True, 'data': logs.data})

Resources