Giter Site home page Giter Site logo

Comments (12)

weareua avatar weareua commented on August 16, 2024

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.

rossm6 avatar rossm6 commented on August 16, 2024

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.

bellini666 avatar bellini666 commented on August 16, 2024

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.

weareua avatar weareua commented on August 16, 2024

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.

rossm6 avatar rossm6 commented on August 16, 2024

@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.

weareua avatar weareua commented on August 16, 2024

@rossm6 sorry, I didn't even manage to start it yet.

from strawberry-django-plus.

weareua avatar weareua commented on August 16, 2024

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.

rossm6 avatar rossm6 commented on August 16, 2024

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.

weareua avatar weareua commented on August 16, 2024

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.

rossm6 avatar rossm6 commented on August 16, 2024

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.

weareua avatar weareua commented on August 16, 2024

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.

weareua avatar weareua commented on August 16, 2024

@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 and exclude 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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.