New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Django form mutations #217
Merged
syrusakbary
merged 14 commits into
graphql-python:master
from
grantmcconnaughey:form_mutations
Jun 5, 2018
Merged
Changes from 9 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
f034946
Add Django form-based mutations
26a4d31
Use options correctly
80a06a0
Fix flake8 issue
4f904f4
Test mutation
5072464
Document form mutations
f5083cb
Change form valid method names
666ddb2
Merge form converter modules
463ce68
Change mutations to new 2.0 format
bf7ad7e
Test invalid forms
40610c6
Support instance kwarg
d6dbe2a
Default return_field_name is camcelcased
748dc4c
Add id input field to model form mutation
6d7a0d0
Make id field an ID type
c3938d1
Fix line length
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
Integration with Django forms | ||
============================= | ||
|
||
Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation. | ||
|
||
FormMutation | ||
------------ | ||
|
||
.. code:: python | ||
|
||
class MyForm(forms.Form): | ||
name = forms.CharField() | ||
|
||
class MyMutation(FormMutation): | ||
class Meta: | ||
form_class = MyForm | ||
|
||
``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string. | ||
|
||
ModelFormMutation | ||
----------------- | ||
|
||
``ModelFormMutation`` will pull the fields from a ``ModelForm``. | ||
|
||
.. code:: python | ||
|
||
class Pet(models.Model): | ||
name = models.CharField() | ||
|
||
class PetForm(forms.ModelForm): | ||
class Meta: | ||
model = Pet | ||
fields = ('name',) | ||
|
||
# This will get returned when the mutation completes successfully | ||
class PetType(DjangoObjectType): | ||
class Meta: | ||
model = Pet | ||
|
||
class PetMutation(ModelFormMutation): | ||
class Meta: | ||
form_class = PetForm | ||
|
||
``PetMutation`` will grab the fields from ``PetForm`` and turn them into inputs. If the form is valid then the mutation | ||
will lookup the ``DjangoObjectType`` for the ``Pet`` model and return that under the key ``pet``. Otherwise it will | ||
return a list of errors. | ||
|
||
You can change the input name (default is ``input``) and the return field name (default is the model name lowercase). | ||
|
||
.. code:: python | ||
|
||
class PetMutation(ModelFormMutation): | ||
class Meta: | ||
form_class = PetForm | ||
input_field_name = 'data' | ||
return_field_name = 'my_pet' | ||
|
||
Form validation | ||
--------------- | ||
|
||
Form mutations will call ``is_valid()`` on your forms. | ||
|
||
If the form is valid then ``form_valid(form, info)`` is called on the mutation. Override this method to change how | ||
the form is saved or to return a different Graphene object type. | ||
|
||
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string | ||
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,4 +12,5 @@ Contents: | |
authorization | ||
debug | ||
rest-framework | ||
form-mutations | ||
introspection |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField # noqa |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
from collections import OrderedDict | ||
|
||
import graphene | ||
from graphene import Field, InputField | ||
from graphene.relay.mutation import ClientIDMutation | ||
from graphene.types.mutation import MutationOptions | ||
from graphene.types.utils import yank_fields_from_attrs | ||
from graphene_django.registry import get_global_registry | ||
|
||
from .converter import convert_form_field | ||
from .types import ErrorType | ||
|
||
|
||
def fields_for_form(form, only_fields, exclude_fields): | ||
fields = OrderedDict() | ||
for name, field in form.fields.items(): | ||
is_not_in_only = only_fields and name not in only_fields | ||
is_excluded = ( | ||
name in exclude_fields # or | ||
# name in already_created_fields | ||
) | ||
|
||
if is_not_in_only or is_excluded: | ||
continue | ||
|
||
fields[name] = convert_form_field(field) | ||
return fields | ||
|
||
|
||
class BaseFormMutation(ClientIDMutation): | ||
class Meta: | ||
abstract = True | ||
|
||
@classmethod | ||
def mutate_and_get_payload(cls, root, info, **input): | ||
form = cls._meta.form_class(data=input) | ||
|
||
if form.is_valid(): | ||
return cls.perform_mutate(form, info) | ||
else: | ||
errors = [ | ||
ErrorType(field=key, messages=value) | ||
for key, value in form.errors.items() | ||
] | ||
|
||
return cls(errors=errors) | ||
|
||
|
||
class FormMutationOptions(MutationOptions): | ||
form_class = None | ||
|
||
|
||
class FormMutation(BaseFormMutation): | ||
class Meta: | ||
abstract = True | ||
|
||
errors = graphene.List(ErrorType) | ||
|
||
@classmethod | ||
def __init_subclass_with_meta__(cls, form_class=None, | ||
only_fields=(), exclude_fields=(), **options): | ||
|
||
if not form_class: | ||
raise Exception('form_class is required for FormMutation') | ||
|
||
form = form_class() | ||
input_fields = fields_for_form(form, only_fields, exclude_fields) | ||
output_fields = fields_for_form(form, only_fields, exclude_fields) | ||
|
||
_meta = FormMutationOptions(cls) | ||
_meta.form_class = form_class | ||
_meta.fields = yank_fields_from_attrs( | ||
output_fields, | ||
_as=Field, | ||
) | ||
|
||
input_fields = yank_fields_from_attrs( | ||
input_fields, | ||
_as=InputField, | ||
) | ||
super(FormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) | ||
|
||
@classmethod | ||
def perform_mutate(cls, form, info): | ||
form.save() | ||
return cls(errors=[]) | ||
|
||
|
||
class ModelFormMutationOptions(FormMutationOptions): | ||
model = None | ||
return_field_name = None | ||
|
||
|
||
class ModelFormMutation(BaseFormMutation): | ||
class Meta: | ||
abstract = True | ||
|
||
errors = graphene.List(ErrorType) | ||
|
||
@classmethod | ||
def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_name=None, | ||
only_fields=(), exclude_fields=(), **options): | ||
|
||
if not form_class: | ||
raise Exception('form_class is required for ModelFormMutation') | ||
|
||
if not model: | ||
model = form_class._meta.model | ||
|
||
if not model: | ||
raise Exception('model is required for ModelFormMutation') | ||
|
||
form = form_class() | ||
input_fields = fields_for_form(form, only_fields, exclude_fields) | ||
|
||
registry = get_global_registry() | ||
model_type = registry.get_type_for_model(model) | ||
return_field_name = return_field_name or model._meta.model_name | ||
output_fields = OrderedDict() | ||
output_fields[return_field_name] = graphene.Field(model_type) | ||
|
||
_meta = ModelFormMutationOptions(cls) | ||
_meta.form_class = form_class | ||
_meta.model = model | ||
_meta.return_field_name = return_field_name | ||
_meta.fields = yank_fields_from_attrs( | ||
output_fields, | ||
_as=Field, | ||
) | ||
|
||
input_fields = yank_fields_from_attrs( | ||
input_fields, | ||
_as=InputField, | ||
) | ||
super(ModelFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) | ||
|
||
@classmethod | ||
def perform_mutate(cls, form, info): | ||
obj = form.save() | ||
kwargs = {cls._meta.return_field_name: obj} | ||
return cls(errors=[], **kwargs) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on some of the other requests coming in on the
SerializerMutation
stuff, I have a few suggestions here for extensibility.get_form
classmethod that can be overridden to do any custom form logic.get_form_kwargs
classmethod for a simpler override use case where you just want to send some extra kwargs to the form class.