Comments (12)
Utilization of django forms is the only thing that keeps me using django-graphene. So I'm with @rossm6 on this feature request.
from strawberry-django-plus.
Since raising this enhancement request I've realised I could just do all the validaton at the model level. Still I think it would be good to support forms also.
from strawberry-django-plus.
Hey guys,
I can see how this can be useful!
The current DjangoInputMutation (and its create_mutation
, update_mutation
and delete_mutation
implementations) does some integration with django in a way that: It will change the return type to be a union of it with OperationInfo
, which will contain any errors raised by ValidationError
, ObjectDoesNotExist
and PermissionDenied
So, this could be done by probably extending on it and creating the input type automatically by introspecting the django form.
This is a low priority for me since I don't actually use django forms in my projects, but I welcome anyone to try to implement this and I'll gladly review it to be a part of strawberry-django-plus
:)
from strawberry-django-plus.
Since raising this enhancement request I've realised I could just do all the validaton at the model level. Still I think it would be good to support forms also.
Surely you can validate data on the model level, but forms allow you to enhance model schema with custom fields and neat validation for them that often gets useful.
As for me, when we're talking about building Graph QL server, forms are the best way to validate the mutation attributes, leaving for the resolver only business logic validation. Code becomes more clear and modular.
This is a low priority for me since I don't actually use django forms in my projects, but I welcome anyone to try to implement this and I'll gladly review it to be a part of strawberry-django-plus :)
Thanks! I didn't start with Strawberry yet, but at the moment I'm forced to lock my dependencies because of this issue. It's obvious that you would want to switch to something really maintained, which is Strawberry right now. Forms are the one thing that I'm missing right now, so at least I'll try to poke around and see how I could incorporate them into the existing suite.
from strawberry-django-plus.
@weareua Did you get anywhere with getting forms to work? I was thinking of taking a look soon but the source code is completely new to me so any pointers will be helpful.
from strawberry-django-plus.
@rossm6 sorry, I didn't even manage to start it yet.
from strawberry-django-plus.
Greetings. So finally I had a chance to try it out myself.
As far as I can see, it almost works out of the box for me.
I can apply django form validation by converting input data into django form and executing .is_valid
method on it.
Then using custom ErrorType
graph-ql type I can convert all gathered errors into the server response.
Things that are missing:
- automatically convert input data into the django model object if
id
was provided - generate graph ql types from the form. But as far as I see strawberry django doesn't even provide this type of generation even for models.
Both things are not crucial so in general I'm happy with how I can interact with strawberry django integration. Here is my code:
# models.py
from django.db import models
class Fruit(models.Model):
name = models.CharField(max_length=20)
color = models.ForeignKey(
'Color',
related_name='fruits',
on_delete=models.CASCADE
)
amount = models.IntegerField()
class Color(models.Model):
name = models.CharField(max_length=20)
# helpers.py
from django.utils.functional import Promise
from django.utils.encoding import force_str
from strawberry.utils.str_converters import to_camel_case
def isiterable(value):
try:
iter(value)
except TypeError:
return False
return True
def _camelize_django_str(s):
if isinstance(s, Promise):
s = force_str(s)
return to_camel_case(s) if isinstance(s, str) else s
def camelize(data):
if isinstance(data, dict):
return {_camelize_django_str(k): camelize(v) for k, v in data.items()}
if isiterable(data) and not isinstance(data, (str, Promise)):
return [camelize(d) for d in data]
return data
# types.py
from typing import List, Optional
import strawberry
import strawberry_django
from . import models
from .helpers import camelize
@strawberry.type
class ErrorType:
field: str
messages: List[str]
@classmethod
def from_errors(cls, errors):
data = camelize(errors)
return [cls(field=key, messages=value) for key, value in data.items()]
@strawberry_django.type(models.Fruit, pagination=True)
class FruitType:
id: strawberry.auto
name: strawberry.auto
color: "ColorType"
@strawberry_django.field
def upper_name(self) -> str:
return self.name.upper()
@strawberry.interface
class BaseMutationResponseType:
errors: Optional[List[ErrorType]]
@strawberry.type
class FruitMutationResponse(BaseMutationResponseType):
response: Optional[FruitType]
@strawberry_django.type(models.Color, pagination=True)
class ColorType:
id: strawberry.auto
name: strawberry.auto
fruits: List[FruitType]
# input types
@strawberry_django.input(models.Fruit)
class FruitInput:
id: strawberry.auto
name: strawberry.auto
color: strawberry.ID
@strawberry.input
class DeleteFruitInput:
fruit: strawberry.ID
# forms.py
from django import forms
from django.core.exceptions import ValidationError
from .models import Fruit, Color
class FruitModelForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
error_dict = {}
name = cleaned_data.get('name')
color = cleaned_data.get('color')
if name == 'Apricot':
error_dict['name'] = ValidationError('Apricots are not allowed!')
if color.name == 'White':
error_dict['color'] = ValidationError(
'White color is not allowed!')
if error_dict:
raise ValidationError(error_dict)
class Meta:
model = Fruit
fields = ('name', 'color')
class DeleteFruitModelForm(forms.Form):
fruit = forms.ModelChoiceField(queryset=Fruit.objects.all())
# mutations.py
from copy import deepcopy
from .types import (
FruitInput, DeleteFruitInput, ErrorType, FruitMutationResponse)
from .forms import (
FruitModelForm, DeleteFruitModelForm)
from .models import Fruit
def fruit_mutation(
self, info, data: FruitInput) -> FruitMutationResponse:
response = None
errors = []
form = FruitModelForm(data.__dict__)
if form.is_valid():
if not data.id:
fruit = form.save()
response = fruit
else:
try:
fruit = Fruit.objects.get(pk=data.id)
except Fruit.DoesNotExist:
errors.append(ErrorType(
field='id', messages=["Fruit doesn't exist"]))
else:
for key, value in form.cleaned_data.items():
setattr(fruit, key, value)
fruit.save()
response = fruit
else:
errors = ErrorType.from_errors(form.errors)
return FruitMutationResponse(response=response, errors=errors)
def delete_fruit_mutation(
self, info, data: DeleteFruitInput) -> FruitMutationResponse:
response = None
errors = []
form = DeleteFruitModelForm(data.__dict__)
if form.is_valid():
obj = form.cleaned_data.get('fruit')
deleted_obj = deepcopy(obj)
obj.delete()
response = deleted_obj
else:
errors = ErrorType.from_errors(form.errors)
return FruitMutationResponse(response=response, errors=errors)
# schema.py
from typing import List
import strawberry
import strawberry_django
from strawberry_django_plus import gql
from strawberry_django_plus.optimizer import DjangoOptimizerExtension
from app.models import Fruit as FruitModel
from .types import (
ColorType,
FruitType,
FruitMutationResponse,
)
from .mutations import (
fruit_mutation, delete_fruit_mutation)
@strawberry.type
class Query:
fruit: FruitType = strawberry_django.field()
fruits: List[FruitType] = strawberry_django.field()
color: ColorType = strawberry_django.field()
colors: List[ColorType] = strawberry_django.field()
@gql.type
class Mutation:
fruit: FruitMutationResponse = strawberry.mutation(
resolver=fruit_mutation
)
delete_fruit: FruitMutationResponse = strawberry.mutation(
resolver=delete_fruit_mutation
)
schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[
DjangoOptimizerExtension,
])
By the way, Query optimization extension just works, Thanks a lot!
from strawberry-django-plus.
Thank you very much for this - it will certainly save me some time.
@bellini666 In theory is it possible to create the input_type dynamically from a form (so taking the model and fields from the form) using the technologies already available in strawberry-django, and, or, strawberry-django-plus?
from strawberry-django-plus.
It took me some time to figure things out as well. Unfortunately, strawberry-django tutorials sort of force us to use generic mutation shortcuts. I think that's not the best idea because usually you would want to customize that behavior, and it's not easy to find the example of how we can bring our own mutation resolvers to play.
By the way, I updated the example above to show how we can DRY mutation response types using strawberry interfaces.
from strawberry-django-plus.
Here is an example of dynamically creating an input, response and mutation. So this means we can take a model form and create everything. I'll probably look at doing all of this on Friday. Hopefully will have a PR soon!
ExampleInput = strawberry.input(
make_dataclass(
"ExampleInput",
[
("something", str),
],
)
)
ExampleResponse = strawberry.type(make_dataclass("ExampleResponse", [("success", bool)]))
def example_mutation(info, data: ExampleInput) -> ExampleResponse:
return ExampleResponse(success=True)
example = strawberry.mutation(resolver=example_mutation)
Mutation = create_type("Mutation", [example])
from strawberry-django-plus.
Looking further into it, I found that we have to tweak the ModelForm a bit in order to get the existing model instance if there is the id
attribute in the payload. It's quite easy to implement by instantiating ModelForm and tweaking its __init__
method.
Please take a look at the example below:
class StrawberryModelForm(forms.ModelForm):
"""
Looks for the id in the input data and if it's provided
returns existing model instance instead of the new one.
"""
def __init__(self, *args, **kwargs):
pk = None
instance = None
for arg in args:
pk = arg.get('id', None)
# get the first pk and exit
if pk:
break
if pk:
instance = self._meta.model._default_manager.get(pk=pk)
super().__init__(*args, **kwargs)
if instance:
# restore the instance after form initiation
self.instance = instance
class FruitModelForm(StrawberryModelForm):
....
So basically all you need is to instantiate from StrawberryModelForm
instead of forms.ModelForm
and you'll be good to go
from strawberry-django-plus.
@rossm6
I've tried to enhance your idea with automatic types generation. Response types don't bother me as much as input types, especially when we're talking about dozens of Mutations that all are based on forms and which input types in theory could be generated from that forms.
I've done some digging and as results here is the prototype of automatic types generation from the forms.
This approach:
- works both with django.Form and django.ModelForm
- respects
fields
andexclude
Form Meta attributes - respects
required
attribute of the form field
# utils.py
import datetime
import decimal
import uuid
from typing import Optional
import strawberry
import django
from dataclasses import make_dataclass, field
form_field_type_map = {
django.forms.fields.BooleanField: bool,
django.forms.fields.CharField: str,
django.forms.fields.DateField: datetime.date,
django.forms.fields.DateTimeField: datetime.datetime,
django.forms.fields.DecimalField: decimal.Decimal,
django.forms.fields.EmailField: str,
django.forms.fields.FilePathField: str,
django.forms.fields.FloatField: float,
django.forms.fields.GenericIPAddressField: str,
django.forms.fields.IntegerField: int,
django.forms.fields.NullBooleanField: Optional[bool],
django.forms.fields.SlugField: str,
django.forms.fields.TimeField: datetime.time,
django.forms.fields.URLField: str,
django.forms.fields.UUIDField: uuid.UUID,
django.forms.models.ModelChoiceField: strawberry.ID,
}
def get_all_fields_from_form(form_class):
form_instance = form_class()
fields = []
items = form_instance.base_fields.items()
# push optional items to the end of the list.
# we will assign them "None" value, so they should be placed after
# attrs with no default values
sorted_items = sorted(items, key=lambda x: x[1].required, reverse=True)
for item in sorted_items:
item_name = item[0]
item_value = item[1]
field_tuple = (item_name, )
# set default value to None for Optional types
if not item_value.required:
field_tuple = field_tuple + (
Optional[form_field_type_map[type(item_value)]],
field(default=None),)
else:
field_tuple = field_tuple + (
form_field_type_map[type(item_value)],)
fields.append(field_tuple)
# ModelForm instances should have id attr even if it's not present
# in model form instance by default
if issubclass(form_class, django.forms.ModelForm):
fields.append(('id', Optional[strawberry.ID], field(default=None)))
return fields
def get_form_input(input_name, form_class):
datacls = make_dataclass(
input_name,
get_all_fields_from_form(form_class),
)
return strawberry.input(datacls)
# forms.py
from django import forms
from .models import Fruit
class DeleteFruitModelForm(forms.Form):
fruit = forms.ModelChoiceField(queryset=Fruit.objects.all())
class FruitModelForm(forms.ModelForm):
class Meta:
model = Fruit
fields = ('name', 'color')
# types.py
from .forms import FruitModelForm, DeleteFruitModelForm
from .utils import get_form_input
GeneratedFruitInput = get_form_input('GeneratedFruitInput', FruitModelForm)
GeneratedDeleteFruitInput = get_form_input(
'GeneratedDeleteFruitInput', DeleteFruitModelForm)
....
# mutations.py
from .types import (
GeneratedFruitInput, GeneratedDeleteFruitInput, FruitMutationResponse)
def fruit_mutation(
self, info, data: GeneratedFruitInput) -> FruitMutationResponse:
....
def delete_fruit_mutation(
self, info, data: GeneratedDeleteFruitInput) -> FruitMutationResponse:
....
from strawberry-django-plus.
Related Issues (20)
- Querying both sides of a OneToOneField at the same time is an error HOT 5
- Can't make a union of `gql.Connection | OperationMessage` HOT 1
- Headers are not passed to `self.client.post` in `TestClient.request` HOT 1
- Can't make a union of `gql.Connection | OperationMessage` (part 2) HOT 9
- clash with the novel strawberry.relay module (GlobalId)
- relay: compatibility with the new strawberry.relay module HOT 2
- more SQLs then expected HOT 6
- need for an async resolvers.update HOT 1
- NameError with lazy types in relay connection annotations
- input_mutation: TypeError: MutateContentInput fields cannot be resolved HOT 9
- filters not showing up HOT 1
- Optimise manually prefetched field HOT 1
- Got an unexpected keyword argument 'filters' HOT 2
- NodeExtension broken with Strawberry HOT 1
- class inheritance not being detected? HOT 8
- hard dependency on contenttypes framework in optimizer
- hard dependency on django.contrib.auth HOT 1
- Using input_mutation with a None return type throws an exception HOT 1
- Query resolution is really slow HOT 15
- This repo has been merged into strawberry-graphql-django and it is now deprecated HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from strawberry-django-plus.