From 4bdcf054ebdb29e84ea190bb6becc8dc27ca286d Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Fri, 28 Jul 2017 16:46:39 +0200 Subject: [PATCH 001/140] Pass context object to FilterSet instance to support request-baed filtering (fixes #203). --- docs/filtering.rst | 20 ++++++++++ graphene_django/filter/fields.py | 3 +- graphene_django/filter/tests/test_fields.py | 42 +++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/docs/filtering.rst b/docs/filtering.rst index f6ad882..2e6b87f 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -126,3 +126,23 @@ create your own ``Filterset`` as follows: # We specify our custom AnimalFilter using the filterset_class param all_animals = DjangoFilterConnectionField(AnimalNode, filterset_class=AnimalFilter) + +The context argument is passed on as the `request argument `__ +in a ``django_filters.FilterSet`` instance. You can use this to customize your +filters to be context-dependent. We could modify the ``AnimalFilter`` above to +pre-filter animals owned by the authenticated user (set in ``context.user``). + +.. code:: python + + class AnimalFilter(django_filters.FilterSet): + # Do case-insensitive lookups on 'name' + name = django_filters.CharFilter(lookup_type='iexact') + + class Meta: + model = Animal + fields = ['name', 'genus', 'is_domesticated'] + + @property + def qs(self): + # The query context can be found in self.request. + return super(AnimalFilter, self).filter(owner=self.request.user) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index fc414bf..68a9072 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -73,7 +73,8 @@ class DjangoFilterConnectionField(DjangoConnectionField): filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} qs = filterset_class( data=filter_kwargs, - queryset=default_manager.get_queryset() + queryset=default_manager.get_queryset(), + request=context ).qs return super(DjangoFilterConnectionField, cls).connection_resolver( diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 1b24ff2..565e4f2 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -136,6 +136,48 @@ def test_filter_shortcut_filterset_extra_meta(): assert 'headline' not in field.filterset_class.get_fields() +def test_filter_shortcut_filterset_context(): + class ArticleContextFilter(django_filters.FilterSet): + + class Meta: + model = Article + exclude = set() + + @property + def qs(self): + qs = super(ArticleContextFilter, self).qs + return qs.filter(reporter=self.request.reporter) + + class Query(ObjectType): + context_articles = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleContextFilter) + + r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com') + r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com') + Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1, editor=r1) + Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2, editor=r2) + + class context(object): + reporter = r2 + + query = ''' + query { + contextArticles { + edges { + node { + headline + } + } + } + } + ''' + schema = Schema(query=Query) + result = schema.execute(query, context_value=context()) + assert not result.errors + + assert len(result.data['contextArticles']['edges']) == 1 + assert result.data['contextArticles']['edges'][0]['node']['headline'] == 'a2' + + def test_filter_filterset_information_on_meta(): class ReporterFilterNode(DjangoObjectType): From be20450a663073add7c7ace437cccaf2832cf4f3 Mon Sep 17 00:00:00 2001 From: Abram Booth Date: Fri, 21 Apr 2017 13:25:30 -0400 Subject: [PATCH 002/140] Don't break on inherited choices fields. Store converted Django fields in the registry, so choices enums are not created multiple times when inherited by child models. --- graphene_django/converter.py | 12 +++++++++-- graphene_django/registry.py | 8 +++++++- graphene_django/tests/test_query.py | 32 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index b1a8837..d7965fc 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -41,6 +41,10 @@ def get_choices(choices): def convert_django_field_with_choices(field, registry=None): + if registry: + converted = registry.get_converted_field(field) + if converted: + return converted choices = getattr(field, 'choices', None) if choices: meta = field.model._meta @@ -56,8 +60,12 @@ def convert_django_field_with_choices(field, registry=None): return named_choices_descriptions[self.name] enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType) - return enum(description=field.help_text, required=not field.null) - return convert_django_field(field, registry) + converted = enum(description=field.help_text, required=not field.null) + else: + converted = convert_django_field(field, registry) + if registry: + registry.register_converted_field(field, converted) + return converted @singledispatch diff --git a/graphene_django/registry.py b/graphene_django/registry.py index 21fed12..da1ee85 100644 --- a/graphene_django/registry.py +++ b/graphene_django/registry.py @@ -2,7 +2,7 @@ class Registry(object): def __init__(self): self._registry = {} - self._registry_models = {} + self._field_registry = {} def register(self, cls): from .types import DjangoObjectType @@ -19,6 +19,12 @@ class Registry(object): def get_type_for_model(self, model): return self._registry.get(model) + def register_converted_field(self, field, converted): + self._field_registry[field] = converted + + def get_converted_field(self, field): + return self._field_registry.get(field) + registry = None diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 3ecd8ea..f1f5fdb 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -747,3 +747,35 @@ def test_should_query_dataloader_fields(): result = schema.execute(query) assert not result.errors assert result.data == expected + + +def test_should_handle_inherited_choices(): + class BaseModel(models.Model): + choice_field = models.IntegerField(choices=((0, 'zero'), (1, 'one'))) + + class ChildModel(BaseModel): + class Meta: + proxy = True + + class BaseType(DjangoObjectType): + class Meta: + model = BaseModel + + class ChildType(DjangoObjectType): + class Meta: + model = ChildModel + + class Query(graphene.ObjectType): + base = graphene.Field(BaseType) + child = graphene.Field(ChildType) + + schema = graphene.Schema(query=Query) + query = ''' + query { + child { + choiceField + } + } + ''' + result = schema.execute(query) + assert not result.errors From 73905547c8391c009228f3ecfda7750af784bbea Mon Sep 17 00:00:00 2001 From: Miguel Date: Sat, 9 Sep 2017 15:21:34 -0400 Subject: [PATCH 003/140] Fix tutorial-plain.rst typo in " Getting single objects" code example --- docs/tutorial-plain.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 6aa4294..93e55a9 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -445,8 +445,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return Ingredient.objects.all() def resolve_category(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Category.objects.get(pk=id) @@ -457,8 +457,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return None def resolve_ingredient(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Ingredient.objects.get(pk=id) From 459d7df42b7804d3427218bf8dc1ffe6f6f5ac39 Mon Sep 17 00:00:00 2001 From: Ivan Belokobylskiy Date: Wed, 27 Sep 2017 18:46:32 +0300 Subject: [PATCH 004/140] Access to batch endpoint without parameters fixed ValueError: max() arg is an empty sequence exception fixed --- graphene_django/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index d7b8795..b2b5f91 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -106,7 +106,7 @@ class GraphQLView(View): if self.batch: responses = [self.get_response(request, entry) for entry in data] result = '[{}]'.format(','.join([response[0] for response in responses])) - status_code = max(responses, key=lambda response: response[1])[1] + status_code = responses and max(responses, key=lambda response: response[1])[1] or 200 else: result, status_code = self.get_response(request, data, show_graphiql) From f034946cda62f5a0e2477c02684664a97a54b1bc Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 10:15:25 -0500 Subject: [PATCH 005/140] Add Django form-based mutations --- graphene_django/forms/__init__.py | 1 + graphene_django/forms/converter.py | 90 +++++++++++ graphene_django/{ => forms}/forms.py | 0 graphene_django/forms/mutation.py | 157 +++++++++++++++++++ graphene_django/forms/tests/__init__.py | 0 graphene_django/forms/tests/test_coverter.py | 98 ++++++++++++ graphene_django/forms/tests/test_mutation.py | 49 ++++++ graphene_django/forms/types.py | 6 + 8 files changed, 401 insertions(+) create mode 100644 graphene_django/forms/__init__.py create mode 100644 graphene_django/forms/converter.py rename graphene_django/{ => forms}/forms.py (100%) create mode 100644 graphene_django/forms/mutation.py create mode 100644 graphene_django/forms/tests/__init__.py create mode 100644 graphene_django/forms/tests/test_coverter.py create mode 100644 graphene_django/forms/tests/test_mutation.py create mode 100644 graphene_django/forms/types.py diff --git a/graphene_django/forms/__init__.py b/graphene_django/forms/__init__.py new file mode 100644 index 0000000..9559be4 --- /dev/null +++ b/graphene_django/forms/__init__.py @@ -0,0 +1 @@ +from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py new file mode 100644 index 0000000..9d7b242 --- /dev/null +++ b/graphene_django/forms/converter.py @@ -0,0 +1,90 @@ +from django import forms +from django.core.exceptions import ImproperlyConfigured +from graphene_django.utils import import_single_dispatch +import graphene + + +singledispatch = import_single_dispatch() + + +def convert_form_to_input_type(form_class): + form = form_class() + + items = { + name: convert_form_field(field) + for name, field in form.fields.items() + } + + return type( + '{}Input'.format(form.__class__.__name__), + (graphene.InputObjectType, ), + items + ) + + +@singledispatch +def get_graphene_type_from_form_field(field): + raise ImproperlyConfigured( + "Don't know how to convert the form field %s (%s) " + "to Graphene type" % (field, field.__class__) + ) + + +def convert_form_field(field, is_input=True): + """ + Converts a Django form field to a graphql field and marks the field as + required if we are creating an input type and the field itself is required + """ + + graphql_type = get_graphene_type_from_form_field(field) + + kwargs = { + 'description': field.help_text, + 'required': is_input and field.required, + } + + # if it is a tuple or a list it means that we are returning + # the graphql type and the child type + if isinstance(graphql_type, (list, tuple)): + kwargs['of_type'] = graphql_type[1] + graphql_type = graphql_type[0] + + return graphql_type(**kwargs) + + +@get_graphene_type_from_form_field.register(forms.CharField) +@get_graphene_type_from_form_field.register(forms.ChoiceField) +def convert_form_field_to_string(field): + return graphene.String + + +@get_graphene_type_from_form_field.register(forms.IntegerField) +def convert_form_field_to_int(field): + return graphene.Int + + +@get_graphene_type_from_form_field.register(forms.BooleanField) +def convert_form_field_to_bool(field): + return graphene.Boolean + + +@get_graphene_type_from_form_field.register(forms.FloatField) +@get_graphene_type_from_form_field.register(forms.DecimalField) +def convert_form_field_to_float(field): + return graphene.Float + + +@get_graphene_type_from_form_field.register(forms.DateField) +@get_graphene_type_from_form_field.register(forms.DateTimeField) +def convert_form_field_to_datetime(field): + return graphene.types.datetime.DateTime + + +@get_graphene_type_from_form_field.register(forms.TimeField) +def convert_form_field_to_time(field): + return graphene.types.datetime.Time + + +@get_graphene_type_from_form_field.register(forms.MultipleChoiceField) +def convert_form_field_to_list_of_string(field): + return (graphene.List, graphene.String) diff --git a/graphene_django/forms.py b/graphene_django/forms/forms.py similarity index 100% rename from graphene_django/forms.py rename to graphene_django/forms/forms.py diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py new file mode 100644 index 0000000..d9e27aa --- /dev/null +++ b/graphene_django/forms/mutation.py @@ -0,0 +1,157 @@ +from functools import partial + +import six +import graphene +from graphene import Field, Argument +from graphene.types.mutation import MutationMeta +from graphene.types.objecttype import ObjectTypeMeta +from graphene.types.options import Options +from graphene.types.utils import get_field_as, merge +from graphene.utils.is_base_type import is_base_type +from graphene_django.registry import get_global_registry + +from .converter import convert_form_to_input_type +from .types import ErrorType + + +class FormMutationMeta(MutationMeta): + def __new__(cls, name, bases, attrs): + if not is_base_type(bases, FormMutationMeta): + return type.__new__(cls, name, bases, attrs) + + options = Options( + attrs.pop('Meta', None), + name=name, + description=attrs.pop('__doc__', None), + form_class=None, + input_field_name='input', + local_fields=None, + only_fields=(), + exclude_fields=(), + interfaces=(), + registry=None + ) + + if not options.form_class: + raise Exception('Missing form_class') + + cls = ObjectTypeMeta.__new__( + cls, name, bases, dict(attrs, _meta=options) + ) + + options.fields = merge( + options.interface_fields, options.base_fields, options.local_fields, + {'errors': get_field_as(cls.errors, Field)} + ) + + cls.Input = convert_form_to_input_type(options.form_class) + + field_kwargs = {cls.options.input_field_name: Argument(cls.Input, required=True)} + cls.Field = partial( + Field, + cls, + resolver=cls.mutate, + **field_kwargs + ) + + return cls + + +class BaseFormMutation(graphene.Mutation): + + @classmethod + def mutate(cls, root, args, context, info): + form = cls.get_form(root, args, context, info) + + 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) + + @classmethod + def perform_mutate(cls, form, info): + form.save() + return cls(errors=[]) + + @classmethod + def get_form(cls, root, args, context, info): + form_data = args.get(cls._meta.input_field_name) + kwargs = cls.get_form_kwargs(root, args, context, info) + return cls._meta.form_class(data=form_data, **kwargs) + + @classmethod + def get_form_kwargs(cls, root, args, context, info): + return {} + + +class FormMutation(six.with_metaclass(FormMutationMeta, BaseFormMutation)): + + errors = graphene.List(ErrorType) + + +class ModelFormMutationMeta(MutationMeta): + def __new__(cls, name, bases, attrs): + if not is_base_type(bases, ModelFormMutationMeta): + return type.__new__(cls, name, bases, attrs) + + options = Options( + attrs.pop('Meta', None), + name=name, + description=attrs.pop('__doc__', None), + form_class=None, + input_field_name='input', + return_field_name=None, + model=None, + local_fields=None, + only_fields=(), + exclude_fields=(), + interfaces=(), + registry=None + ) + + if not options.form_class: + raise Exception('Missing form_class') + + cls = ObjectTypeMeta.__new__( + cls, name, bases, dict(attrs, _meta=options) + ) + + options.fields = merge( + options.interface_fields, options.base_fields, options.local_fields, + {'errors': get_field_as(cls.errors, Field)} + ) + + cls.Input = convert_form_to_input_type(options.form_class) + + field_kwargs = {cls.options.input_field_name: Argument(cls.Input, required=True)} + cls.Field = partial( + Field, + cls, + resolver=cls.mutate, + **field_kwargs + ) + + cls.model = options.model or options.form_class.Meta.model + cls.return_field_name = cls._meta.return_field_name or cls.model._meta.model_name + + registry = get_global_registry() + model_type = registry.get_type_for_model(cls.model) + + options.fields[cls.return_field_name] = graphene.Field(model_type) + + return cls + + +class ModelFormMutation(six.with_metaclass(ModelFormMutationMeta, BaseFormMutation)): + + errors = graphene.List(ErrorType) + + @classmethod + def perform_mutate(cls, form, info): + obj = form.save() + kwargs = {cls.return_field_name: obj} + return cls(errors=[], **kwargs) diff --git a/graphene_django/forms/tests/__init__.py b/graphene_django/forms/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graphene_django/forms/tests/test_coverter.py b/graphene_django/forms/tests/test_coverter.py new file mode 100644 index 0000000..e4a686b --- /dev/null +++ b/graphene_django/forms/tests/test_coverter.py @@ -0,0 +1,98 @@ +import copy + +from django import forms +from py.test import raises + +import graphene + +from ..converter import convert_form_field + + +def _get_type(form_field, **kwargs): + # prevents the following error: + # AssertionError: The `source` argument is not meaningful when applied to a `child=` field. + # Remove `source=` from the field declaration. + # since we are reusing the same child in when testing the required attribute + + if 'child' in kwargs: + kwargs['child'] = copy.deepcopy(kwargs['child']) + + field = form_field(**kwargs) + + return convert_form_field(field) + + +def assert_conversion(form_field, graphene_field, **kwargs): + graphene_type = _get_type(form_field, help_text='Custom Help Text', **kwargs) + assert isinstance(graphene_type, graphene_field) + + graphene_type_required = _get_type( + form_field, help_text='Custom Help Text', required=True, **kwargs + ) + assert isinstance(graphene_type_required, graphene_field) + + return graphene_type + + +def test_should_unknown_form_field_raise_exception(): + with raises(Exception) as excinfo: + convert_form_field(None) + assert 'Don\'t know how to convert the form field' in str(excinfo.value) + + +def test_should_charfield_convert_string(): + assert_conversion(forms.CharField, graphene.String) + + +def test_should_timefield_convert_time(): + assert_conversion(forms.TimeField, graphene.types.datetime.Time) + + +def test_should_email_convert_string(): + assert_conversion(forms.EmailField, graphene.String) + + +def test_should_slug_convert_string(): + assert_conversion(forms.SlugField, graphene.String) + + +def test_should_url_convert_string(): + assert_conversion(forms.URLField, graphene.String) + + +def test_should_choicefield_convert_string(): + assert_conversion(forms.ChoiceField, graphene.String, choices=[]) + + +def test_should_regexfield_convert_string(): + assert_conversion(forms.RegexField, graphene.String, regex='[0-9]+') + + +def test_should_uuidfield_convert_string(): + assert_conversion(forms.UUIDField, graphene.String) + + +def test_should_integer_convert_int(): + assert_conversion(forms.IntegerField, graphene.Int) + + +def test_should_boolean_convert_boolean(): + assert_conversion(forms.BooleanField, graphene.Boolean) + + +def test_should_float_convert_float(): + assert_conversion(forms.FloatField, graphene.Float) + + +def test_should_decimal_convert_float(): + assert_conversion(forms.DecimalField, graphene.Float, max_digits=4, decimal_places=2) + + +def test_should_filepath_convert_string(): + assert_conversion(forms.FilePathField, graphene.String, path='/') + + +def test_should_multiplechoicefield_convert_to_list_of_string(): + field = assert_conversion(forms.MultipleChoiceField, graphene.List, choices=[1, 2, 3]) + + assert field.of_type == graphene.String diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py new file mode 100644 index 0000000..004f5d3 --- /dev/null +++ b/graphene_django/forms/tests/test_mutation.py @@ -0,0 +1,49 @@ +from django import forms +from py.test import raises + +from graphene_django.tests.models import Pet +from ..mutation import FormMutation, ModelFormMutation + + +class MyForm(forms.Form): + text = forms.CharField() + + +class PetForm(forms.ModelForm): + + class Meta: + model = Pet + fields = ('name',) + + +def test_needs_form_class(): + with raises(Exception) as exc: + class MyMutation(FormMutation): + pass + + assert exc.value.args[0] == 'Missing form_class' + + +def test_has_fields(): + class MyMutation(FormMutation): + class Meta: + form_class = MyForm + + assert 'errors' in MyMutation._meta.fields + + +def test_has_input_fields(): + class MyMutation(FormMutation): + class Meta: + form_class = MyForm + + assert 'text' in MyMutation.Input._meta.fields + + +def test_model_form(): + class PetMutation(ModelFormMutation): + class Meta: + form_class = PetForm + + assert PetMutation.model == Pet + assert PetMutation.return_field_name == 'pet' diff --git a/graphene_django/forms/types.py b/graphene_django/forms/types.py new file mode 100644 index 0000000..1fe33f3 --- /dev/null +++ b/graphene_django/forms/types.py @@ -0,0 +1,6 @@ +import graphene + + +class ErrorType(graphene.ObjectType): + field = graphene.String() + messages = graphene.List(graphene.String) From 26a4d315c5960fc3e04ab59dff735caac3d7b221 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 10:31:17 -0500 Subject: [PATCH 006/140] Use options correctly --- graphene_django/forms/mutation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index d9e27aa..291a7af 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -46,7 +46,7 @@ class FormMutationMeta(MutationMeta): cls.Input = convert_form_to_input_type(options.form_class) - field_kwargs = {cls.options.input_field_name: Argument(cls.Input, required=True)} + field_kwargs = {options.input_field_name: Argument(cls.Input, required=True)} cls.Field = partial( Field, cls, @@ -127,7 +127,7 @@ class ModelFormMutationMeta(MutationMeta): cls.Input = convert_form_to_input_type(options.form_class) - field_kwargs = {cls.options.input_field_name: Argument(cls.Input, required=True)} + field_kwargs = {options.input_field_name: Argument(cls.Input, required=True)} cls.Field = partial( Field, cls, From 80a06a035488b43f709373672aef5e53840f01d3 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 10:37:08 -0500 Subject: [PATCH 007/140] Fix flake8 issue --- graphene_django/forms/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/forms/__init__.py b/graphene_django/forms/__init__.py index 9559be4..066eec4 100644 --- a/graphene_django/forms/__init__.py +++ b/graphene_django/forms/__init__.py @@ -1 +1 @@ -from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField +from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField # noqa From 4f904f470088bbafbebe8d5158852a36d9111aa7 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 10:48:09 -0500 Subject: [PATCH 008/140] Test mutation --- graphene_django/forms/tests/test_mutation.py | 26 +++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 004f5d3..9874ae6 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -1,4 +1,5 @@ from django import forms +from django.test import TestCase from py.test import raises from graphene_django.tests.models import Pet @@ -40,10 +41,23 @@ def test_has_input_fields(): assert 'text' in MyMutation.Input._meta.fields -def test_model_form(): - class PetMutation(ModelFormMutation): - class Meta: - form_class = PetForm +class ModelFormMutationTests(TestCase): - assert PetMutation.model == Pet - assert PetMutation.return_field_name == 'pet' + def test_model_form_mutation(self): + class PetMutation(ModelFormMutation): + class Meta: + form_class = PetForm + + self.assertEqual(PetMutation.model, Pet) + self.assertEqual(PetMutation.return_field_name, 'pet') + + def test_model_form_mutation_mutate(self): + class PetMutation(ModelFormMutation): + class Meta: + form_class = PetForm + + PetMutation.mutate(None, {'input': {'name': 'Fluffy'}}, None, None) + + self.assertEqual(Pet.objects.count(), 1) + pet = Pet.objects.get() + self.assertEqual(pet.name, 'Fluffy') From 507246468b07bddaaac63f4bf2f72013bdadbc23 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 11:18:58 -0500 Subject: [PATCH 009/140] Document form mutations --- docs/form-mutations.rst | 67 +++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 68 insertions(+) create mode 100644 docs/form-mutations.rst diff --git a/docs/form-mutations.rst b/docs/form-mutations.rst new file mode 100644 index 0000000..f010d8a --- /dev/null +++ b/docs/form-mutations.rst @@ -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 ``perform_mutate(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. diff --git a/docs/index.rst b/docs/index.rst index 256da68..7c64ae7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,4 +12,5 @@ Contents: authorization debug rest-framework + form-mutations introspection From f5083cb1901596b4a41ab6484ec3c89013d6c245 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Tue, 18 Jul 2017 11:20:59 -0500 Subject: [PATCH 010/140] Change form valid method names --- docs/form-mutations.rst | 2 +- graphene_django/forms/mutation.py | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/form-mutations.rst b/docs/form-mutations.rst index f010d8a..a498b56 100644 --- a/docs/form-mutations.rst +++ b/docs/form-mutations.rst @@ -60,7 +60,7 @@ Form validation Form mutations will call ``is_valid()`` on your forms. -If the form is valid then ``perform_mutate(form, info)`` is called on the mutation. Override this method to change how +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 diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 291a7af..43726d4 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -64,19 +64,23 @@ class BaseFormMutation(graphene.Mutation): form = cls.get_form(root, args, context, info) if form.is_valid(): - return cls.perform_mutate(form, info) + return cls.form_valid(form, info) else: - errors = [ - ErrorType(field=key, messages=value) - for key, value in form.errors.items() - ] - return cls(errors=errors) + return cls.form_invalid(form, info) @classmethod - def perform_mutate(cls, form, info): + def form_valid(cls, form, info): form.save() return cls(errors=[]) + @classmethod + def form_invalid(cls, form, info): + errors = [ + ErrorType(field=key, messages=value) + for key, value in form.errors.items() + ] + return cls(errors=errors) + @classmethod def get_form(cls, root, args, context, info): form_data = args.get(cls._meta.input_field_name) @@ -151,7 +155,7 @@ class ModelFormMutation(six.with_metaclass(ModelFormMutationMeta, BaseFormMutati errors = graphene.List(ErrorType) @classmethod - def perform_mutate(cls, form, info): + def form_valid(cls, form, info): obj = form.save() kwargs = {cls.return_field_name: obj} return cls(errors=[], **kwargs) From 666ddb2ff38eae171ed20ef8b467cd2cb0eaa8e0 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Mon, 2 Oct 2017 09:42:57 -0500 Subject: [PATCH 011/140] Merge form converter modules --- graphene_django/filter/utils.py | 2 +- graphene_django/form_converter.py | 75 ------------- graphene_django/forms/converter.py | 106 ++++++++++-------- .../tests/test_converter.py} | 2 +- 4 files changed, 61 insertions(+), 124 deletions(-) delete mode 100644 graphene_django/form_converter.py rename graphene_django/{tests/test_form_converter.py => forms/tests/test_converter.py} (98%) diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py index 6b938ce..cfa5621 100644 --- a/graphene_django/filter/utils.py +++ b/graphene_django/filter/utils.py @@ -8,7 +8,7 @@ def get_filtering_args_from_filterset(filterset_class, type): a Graphene Field. These arguments will be available to filter against in the GraphQL """ - from ..form_converter import convert_form_field + from ..forms.converter import convert_form_field args = {} for name, filter_field in six.iteritems(filterset_class.base_filters): diff --git a/graphene_django/form_converter.py b/graphene_django/form_converter.py deleted file mode 100644 index 46a38b3..0000000 --- a/graphene_django/form_converter.py +++ /dev/null @@ -1,75 +0,0 @@ -from django import forms -from django.forms.fields import BaseTemporalField - -from graphene import ID, Boolean, Float, Int, List, String, UUID - -from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField -from .utils import import_single_dispatch - -singledispatch = import_single_dispatch() - -try: - UUIDField = forms.UUIDField -except AttributeError: - class UUIDField(object): - pass - - -@singledispatch -def convert_form_field(field): - raise Exception( - "Don't know how to convert the Django form field %s (%s) " - "to Graphene type" % - (field, field.__class__) - ) - - -@convert_form_field.register(BaseTemporalField) -@convert_form_field.register(forms.CharField) -@convert_form_field.register(forms.EmailField) -@convert_form_field.register(forms.SlugField) -@convert_form_field.register(forms.URLField) -@convert_form_field.register(forms.ChoiceField) -@convert_form_field.register(forms.RegexField) -@convert_form_field.register(forms.Field) -def convert_form_field_to_string(field): - return String(description=field.help_text, required=field.required) - - -@convert_form_field.register(UUIDField) -def convert_form_field_to_uuid(field): - return UUID(description=field.help_text, required=field.required) - - -@convert_form_field.register(forms.IntegerField) -@convert_form_field.register(forms.NumberInput) -def convert_form_field_to_int(field): - return Int(description=field.help_text, required=field.required) - - -@convert_form_field.register(forms.BooleanField) -def convert_form_field_to_boolean(field): - return Boolean(description=field.help_text, required=True) - - -@convert_form_field.register(forms.NullBooleanField) -def convert_form_field_to_nullboolean(field): - return Boolean(description=field.help_text) - - -@convert_form_field.register(forms.DecimalField) -@convert_form_field.register(forms.FloatField) -def convert_form_field_to_float(field): - return Float(description=field.help_text, required=field.required) - - -@convert_form_field.register(forms.ModelMultipleChoiceField) -@convert_form_field.register(GlobalIDMultipleChoiceField) -def convert_form_field_to_list(field): - return List(ID, required=field.required) - - -@convert_form_field.register(forms.ModelChoiceField) -@convert_form_field.register(GlobalIDFormField) -def convert_form_field_to_id(field): - return ID(required=field.required) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 9d7b242..7e816d6 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -1,8 +1,17 @@ from django import forms from django.core.exceptions import ImproperlyConfigured -from graphene_django.utils import import_single_dispatch + import graphene +from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField +from .utils import import_single_dispatch + +try: + UUIDField = forms.UUIDField +except AttributeError: + class UUIDField(object): + pass + singledispatch = import_single_dispatch() @@ -23,68 +32,71 @@ def convert_form_to_input_type(form_class): @singledispatch -def get_graphene_type_from_form_field(field): +def convert_form_field(field): raise ImproperlyConfigured( - "Don't know how to convert the form field %s (%s) " - "to Graphene type" % (field, field.__class__) + "Don't know how to convert the Django form field %s (%s) " + "to Graphene type" % + (field, field.__class__) ) -def convert_form_field(field, is_input=True): - """ - Converts a Django form field to a graphql field and marks the field as - required if we are creating an input type and the field itself is required - """ - - graphql_type = get_graphene_type_from_form_field(field) - - kwargs = { - 'description': field.help_text, - 'required': is_input and field.required, - } - - # if it is a tuple or a list it means that we are returning - # the graphql type and the child type - if isinstance(graphql_type, (list, tuple)): - kwargs['of_type'] = graphql_type[1] - graphql_type = graphql_type[0] - - return graphql_type(**kwargs) - - -@get_graphene_type_from_form_field.register(forms.CharField) -@get_graphene_type_from_form_field.register(forms.ChoiceField) +@convert_form_field.register(forms.BaseTemporalField) +@convert_form_field.register(forms.CharField) +@convert_form_field.register(forms.EmailField) +@convert_form_field.register(forms.SlugField) +@convert_form_field.register(forms.URLField) +@convert_form_field.register(forms.ChoiceField) +@convert_form_field.register(forms.RegexField) +@convert_form_field.register(forms.Field) def convert_form_field_to_string(field): - return graphene.String + return graphene.String(description=field.help_text, required=field.required) -@get_graphene_type_from_form_field.register(forms.IntegerField) +@convert_form_field.register(UUIDField) +def convert_form_field_to_uuid(field): + return graphene.UUID(description=field.help_text, required=field.required) + + +@convert_form_field.register(forms.IntegerField) +@convert_form_field.register(forms.NumberInput) def convert_form_field_to_int(field): - return graphene.Int + return graphene.Int(description=field.help_text, required=field.required) -@get_graphene_type_from_form_field.register(forms.BooleanField) -def convert_form_field_to_bool(field): - return graphene.Boolean +@convert_form_field.register(forms.BooleanField) +def convert_form_field_to_boolean(field): + return graphene.Boolean(description=field.help_text, required=True) -@get_graphene_type_from_form_field.register(forms.FloatField) -@get_graphene_type_from_form_field.register(forms.DecimalField) +@convert_form_field.register(forms.NullBooleanField) +def convert_form_field_to_nullboolean(field): + return graphene.Boolean(description=field.help_text) + + +@convert_form_field.register(forms.DecimalField) +@convert_form_field.register(forms.FloatField) def convert_form_field_to_float(field): - return graphene.Float + return graphene.Float(description=field.help_text, required=field.required) -@get_graphene_type_from_form_field.register(forms.DateField) -@get_graphene_type_from_form_field.register(forms.DateTimeField) +@convert_form_field.register(forms.ModelMultipleChoiceField) +@convert_form_field.register(GlobalIDMultipleChoiceField) +def convert_form_field_to_list(field): + return graphene.List(graphene.ID, required=field.required) + + +@convert_form_field.register(forms.ModelChoiceField) +@convert_form_field.register(GlobalIDFormField) +def convert_form_field_to_id(field): + return graphene.ID(required=field.required) + + +@convert_form_field.register(forms.DateField) +@convert_form_field.register(forms.DateTimeField) def convert_form_field_to_datetime(field): - return graphene.types.datetime.DateTime + return graphene.types.datetime.DateTime(description=field.help_text, required=field.required) -@get_graphene_type_from_form_field.register(forms.TimeField) +@convert_form_field.register(forms.TimeField) def convert_form_field_to_time(field): - return graphene.types.datetime.Time - - -@get_graphene_type_from_form_field.register(forms.MultipleChoiceField) -def convert_form_field_to_list_of_string(field): - return (graphene.List, graphene.String) + return graphene.types.datetime.Time(description=field.help_text, required=field.required) diff --git a/graphene_django/tests/test_form_converter.py b/graphene_django/forms/tests/test_converter.py similarity index 98% rename from graphene_django/tests/test_form_converter.py rename to graphene_django/forms/tests/test_converter.py index 5a13554..ec66f8f 100644 --- a/graphene_django/tests/test_form_converter.py +++ b/graphene_django/forms/tests/test_converter.py @@ -4,7 +4,7 @@ from py.test import raises import graphene from graphene import ID, List, NonNull -from ..form_converter import convert_form_field +from ..converter import convert_form_field from .models import Reporter From 463ce68b16b070c0a49637dc71755a33acdf9d49 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Mon, 2 Oct 2017 13:03:20 -0500 Subject: [PATCH 012/140] Change mutations to new 2.0 format --- graphene_django/forms/converter.py | 4 +- graphene_django/forms/mutation.py | 224 ++++++++---------- graphene_django/forms/tests/test_converter.py | 11 +- graphene_django/forms/tests/test_coverter.py | 98 -------- graphene_django/forms/tests/test_mutation.py | 26 +- 5 files changed, 128 insertions(+), 235 deletions(-) delete mode 100644 graphene_django/forms/tests/test_coverter.py diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 7e816d6..220edc4 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -4,7 +4,7 @@ from django.core.exceptions import ImproperlyConfigured import graphene from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField -from .utils import import_single_dispatch +from ..utils import import_single_dispatch try: UUIDField = forms.UUIDField @@ -40,7 +40,7 @@ def convert_form_field(field): ) -@convert_form_field.register(forms.BaseTemporalField) +@convert_form_field.register(forms.fields.BaseTemporalField) @convert_form_field.register(forms.CharField) @convert_form_field.register(forms.EmailField) @convert_form_field.register(forms.SlugField) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 43726d4..bbe5f80 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -1,161 +1,141 @@ -from functools import partial +from collections import OrderedDict -import six import graphene -from graphene import Field, Argument -from graphene.types.mutation import MutationMeta -from graphene.types.objecttype import ObjectTypeMeta -from graphene.types.options import Options -from graphene.types.utils import get_field_as, merge -from graphene.utils.is_base_type import is_base_type +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_to_input_type +from .converter import convert_form_field from .types import ErrorType -class FormMutationMeta(MutationMeta): - def __new__(cls, name, bases, attrs): - if not is_base_type(bases, FormMutationMeta): - return type.__new__(cls, name, bases, attrs) - - options = Options( - attrs.pop('Meta', None), - name=name, - description=attrs.pop('__doc__', None), - form_class=None, - input_field_name='input', - local_fields=None, - only_fields=(), - exclude_fields=(), - interfaces=(), - registry=None +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 not options.form_class: - raise Exception('Missing form_class') + if is_not_in_only or is_excluded: + continue - cls = ObjectTypeMeta.__new__( - cls, name, bases, dict(attrs, _meta=options) - ) - - options.fields = merge( - options.interface_fields, options.base_fields, options.local_fields, - {'errors': get_field_as(cls.errors, Field)} - ) - - cls.Input = convert_form_to_input_type(options.form_class) - - field_kwargs = {options.input_field_name: Argument(cls.Input, required=True)} - cls.Field = partial( - Field, - cls, - resolver=cls.mutate, - **field_kwargs - ) - - return cls + fields[name] = convert_form_field(field) + return fields -class BaseFormMutation(graphene.Mutation): +class BaseFormMutation(ClientIDMutation): + class Meta: + abstract = True @classmethod - def mutate(cls, root, args, context, info): - form = cls.get_form(root, args, context, info) + def mutate_and_get_payload(cls, root, info, **input): + form = cls._meta.form_class(data=input) if form.is_valid(): - return cls.form_valid(form, info) + return cls.perform_mutate(form, info) else: - return cls.form_invalid(form, info) + errors = [ + ErrorType(field=key, messages=value) + for key, value in form.errors.items() + ] - @classmethod - def form_valid(cls, form, info): - form.save() - return cls(errors=[]) - - @classmethod - def form_invalid(cls, form, info): - errors = [ - ErrorType(field=key, messages=value) - for key, value in form.errors.items() - ] - return cls(errors=errors) - - @classmethod - def get_form(cls, root, args, context, info): - form_data = args.get(cls._meta.input_field_name) - kwargs = cls.get_form_kwargs(root, args, context, info) - return cls._meta.form_class(data=form_data, **kwargs) - - @classmethod - def get_form_kwargs(cls, root, args, context, info): - return {} + return cls(errors=errors) -class FormMutation(six.with_metaclass(FormMutationMeta, BaseFormMutation)): +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): -class ModelFormMutationMeta(MutationMeta): - def __new__(cls, name, bases, attrs): - if not is_base_type(bases, ModelFormMutationMeta): - return type.__new__(cls, name, bases, attrs) + if not form_class: + raise Exception('form_class is required for FormMutation') - options = Options( - attrs.pop('Meta', None), - name=name, - description=attrs.pop('__doc__', None), - form_class=None, - input_field_name='input', - return_field_name=None, - model=None, - local_fields=None, - only_fields=(), - exclude_fields=(), - interfaces=(), - registry=None + 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, ) - if not options.form_class: - raise Exception('Missing form_class') - - cls = ObjectTypeMeta.__new__( - cls, name, bases, dict(attrs, _meta=options) + input_fields = yank_fields_from_attrs( + input_fields, + _as=InputField, ) + super(FormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) - options.fields = merge( - options.interface_fields, options.base_fields, options.local_fields, - {'errors': get_field_as(cls.errors, Field)} - ) + @classmethod + def perform_mutate(cls, form, info): + form.save() + return cls(errors=None) - cls.Input = convert_form_to_input_type(options.form_class) - field_kwargs = {options.input_field_name: Argument(cls.Input, required=True)} - cls.Field = partial( - Field, - cls, - resolver=cls.mutate, - **field_kwargs - ) +class ModelFormMutationOptions(FormMutationOptions): + model = None + return_field_name = None - cls.model = options.model or options.form_class.Meta.model - cls.return_field_name = cls._meta.return_field_name or cls.model._meta.model_name + +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(cls.model) + 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) - options.fields[cls.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, + ) - return cls - - -class ModelFormMutation(six.with_metaclass(ModelFormMutationMeta, BaseFormMutation)): - - errors = graphene.List(ErrorType) + 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 form_valid(cls, form, info): + def perform_mutate(cls, form, info): obj = form.save() - kwargs = {cls.return_field_name: obj} - return cls(errors=[], **kwargs) + kwargs = {cls._meta.return_field_name: obj} + return cls(errors=None, **kwargs) diff --git a/graphene_django/forms/tests/test_converter.py b/graphene_django/forms/tests/test_converter.py index ec66f8f..f002031 100644 --- a/graphene_django/forms/tests/test_converter.py +++ b/graphene_django/forms/tests/test_converter.py @@ -5,7 +5,6 @@ import graphene from graphene import ID, List, NonNull from ..converter import convert_form_field -from .models import Reporter def assert_conversion(django_field, graphene_field, *args): @@ -24,15 +23,15 @@ def test_should_unknown_django_field_raise_exception(): def test_should_date_convert_string(): - assert_conversion(forms.DateField, graphene.String) + assert_conversion(forms.DateField, graphene.types.datetime.DateTime) def test_should_time_convert_string(): - assert_conversion(forms.TimeField, graphene.String) + assert_conversion(forms.TimeField, graphene.types.datetime.Time) def test_should_date_time_convert_string(): - assert_conversion(forms.DateTimeField, graphene.String) + assert_conversion(forms.DateTimeField, graphene.types.datetime.DateTime) def test_should_char_convert_string(): @@ -91,13 +90,13 @@ def test_should_decimal_convert_float(): def test_should_multiple_choice_convert_connectionorlist(): - field = forms.ModelMultipleChoiceField(Reporter.objects.all()) + field = forms.ModelMultipleChoiceField(queryset=None) graphene_type = convert_form_field(field) assert isinstance(graphene_type, List) assert graphene_type.of_type == ID def test_should_manytoone_convert_connectionorlist(): - field = forms.ModelChoiceField(Reporter.objects.all()) + field = forms.ModelChoiceField(queryset=None) graphene_type = convert_form_field(field) assert isinstance(graphene_type, graphene.ID) diff --git a/graphene_django/forms/tests/test_coverter.py b/graphene_django/forms/tests/test_coverter.py deleted file mode 100644 index e4a686b..0000000 --- a/graphene_django/forms/tests/test_coverter.py +++ /dev/null @@ -1,98 +0,0 @@ -import copy - -from django import forms -from py.test import raises - -import graphene - -from ..converter import convert_form_field - - -def _get_type(form_field, **kwargs): - # prevents the following error: - # AssertionError: The `source` argument is not meaningful when applied to a `child=` field. - # Remove `source=` from the field declaration. - # since we are reusing the same child in when testing the required attribute - - if 'child' in kwargs: - kwargs['child'] = copy.deepcopy(kwargs['child']) - - field = form_field(**kwargs) - - return convert_form_field(field) - - -def assert_conversion(form_field, graphene_field, **kwargs): - graphene_type = _get_type(form_field, help_text='Custom Help Text', **kwargs) - assert isinstance(graphene_type, graphene_field) - - graphene_type_required = _get_type( - form_field, help_text='Custom Help Text', required=True, **kwargs - ) - assert isinstance(graphene_type_required, graphene_field) - - return graphene_type - - -def test_should_unknown_form_field_raise_exception(): - with raises(Exception) as excinfo: - convert_form_field(None) - assert 'Don\'t know how to convert the form field' in str(excinfo.value) - - -def test_should_charfield_convert_string(): - assert_conversion(forms.CharField, graphene.String) - - -def test_should_timefield_convert_time(): - assert_conversion(forms.TimeField, graphene.types.datetime.Time) - - -def test_should_email_convert_string(): - assert_conversion(forms.EmailField, graphene.String) - - -def test_should_slug_convert_string(): - assert_conversion(forms.SlugField, graphene.String) - - -def test_should_url_convert_string(): - assert_conversion(forms.URLField, graphene.String) - - -def test_should_choicefield_convert_string(): - assert_conversion(forms.ChoiceField, graphene.String, choices=[]) - - -def test_should_regexfield_convert_string(): - assert_conversion(forms.RegexField, graphene.String, regex='[0-9]+') - - -def test_should_uuidfield_convert_string(): - assert_conversion(forms.UUIDField, graphene.String) - - -def test_should_integer_convert_int(): - assert_conversion(forms.IntegerField, graphene.Int) - - -def test_should_boolean_convert_boolean(): - assert_conversion(forms.BooleanField, graphene.Boolean) - - -def test_should_float_convert_float(): - assert_conversion(forms.FloatField, graphene.Float) - - -def test_should_decimal_convert_float(): - assert_conversion(forms.DecimalField, graphene.Float, max_digits=4, decimal_places=2) - - -def test_should_filepath_convert_string(): - assert_conversion(forms.FilePathField, graphene.String, path='/') - - -def test_should_multiplechoicefield_convert_to_list_of_string(): - field = assert_conversion(forms.MultipleChoiceField, graphene.List, choices=[1, 2, 3]) - - assert field.of_type == graphene.String diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 9874ae6..5f1ef4a 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -2,7 +2,7 @@ from django import forms from django.test import TestCase from py.test import raises -from graphene_django.tests.models import Pet +from graphene_django.tests.models import Pet, Film from ..mutation import FormMutation, ModelFormMutation @@ -22,10 +22,10 @@ def test_needs_form_class(): class MyMutation(FormMutation): pass - assert exc.value.args[0] == 'Missing form_class' + assert exc.value.args[0] == 'form_class is required for FormMutation' -def test_has_fields(): +def test_has_output_fields(): class MyMutation(FormMutation): class Meta: form_class = MyForm @@ -43,20 +43,32 @@ def test_has_input_fields(): class ModelFormMutationTests(TestCase): - def test_model_form_mutation(self): + def test_default_meta_fields(self): class PetMutation(ModelFormMutation): class Meta: form_class = PetForm - self.assertEqual(PetMutation.model, Pet) - self.assertEqual(PetMutation.return_field_name, 'pet') + self.assertEqual(PetMutation._meta.model, Pet) + self.assertEqual(PetMutation._meta.return_field_name, 'pet') + self.assertIn('pet', PetMutation._meta.fields) + + def test_custom_return_field_name(self): + class PetMutation(ModelFormMutation): + class Meta: + form_class = PetForm + model = Film + return_field_name = 'animal' + + self.assertEqual(PetMutation._meta.model, Film) + self.assertEqual(PetMutation._meta.return_field_name, 'animal') + self.assertIn('animal', PetMutation._meta.fields) def test_model_form_mutation_mutate(self): class PetMutation(ModelFormMutation): class Meta: form_class = PetForm - PetMutation.mutate(None, {'input': {'name': 'Fluffy'}}, None, None) + PetMutation.mutate_and_get_payload(None, None, name='Fluffy') self.assertEqual(Pet.objects.count(), 1) pet = Pet.objects.get() From bf7ad7eeda4c493d2b5954ee69232ec052eeaea6 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Mon, 2 Oct 2017 13:15:29 -0500 Subject: [PATCH 013/140] Test invalid forms --- graphene_django/forms/converter.py | 15 --------------- graphene_django/forms/mutation.py | 4 ++-- graphene_django/forms/tests/test_mutation.py | 17 ++++++++++++++++- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 220edc4..9d87811 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -16,21 +16,6 @@ except AttributeError: singledispatch = import_single_dispatch() -def convert_form_to_input_type(form_class): - form = form_class() - - items = { - name: convert_form_field(field) - for name, field in form.fields.items() - } - - return type( - '{}Input'.format(form.__class__.__name__), - (graphene.InputObjectType, ), - items - ) - - @singledispatch def convert_form_field(field): raise ImproperlyConfigured( diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index bbe5f80..49fabb8 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -83,7 +83,7 @@ class FormMutation(BaseFormMutation): @classmethod def perform_mutate(cls, form, info): form.save() - return cls(errors=None) + return cls(errors=[]) class ModelFormMutationOptions(FormMutationOptions): @@ -138,4 +138,4 @@ class ModelFormMutation(BaseFormMutation): def perform_mutate(cls, form, info): obj = form.save() kwargs = {cls._meta.return_field_name: obj} - return cls(errors=None, **kwargs) + return cls(errors=[], **kwargs) diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 5f1ef4a..084b8b0 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -68,8 +68,23 @@ class ModelFormMutationTests(TestCase): class Meta: form_class = PetForm - PetMutation.mutate_and_get_payload(None, None, name='Fluffy') + result = PetMutation.mutate_and_get_payload(None, None, name='Fluffy') self.assertEqual(Pet.objects.count(), 1) pet = Pet.objects.get() self.assertEqual(pet.name, 'Fluffy') + self.assertEqual(result.errors, []) + + def test_model_form_mutation_mutate_invalid_form(self): + class PetMutation(ModelFormMutation): + class Meta: + form_class = PetForm + + result = PetMutation.mutate_and_get_payload(None, None) + + # A pet was not created + self.assertEqual(Pet.objects.count(), 0) + + self.assertEqual(len(result.errors), 1) + self.assertEqual(result.errors[0].field, 'name') + self.assertEqual(result.errors[0].messages, ['This field is required.']) From 69cec060d8d5b9bc5e6403e608079eadf77d1655 Mon Sep 17 00:00:00 2001 From: Cameron Dawson Date: Fri, 21 Jul 2017 11:23:16 -0700 Subject: [PATCH 014/140] Fix select_related with filtering --- graphene_django/filter/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index a80d8d7..b93977b 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -61,7 +61,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): low = default_queryset.query.low_mark or queryset.query.low_mark high = default_queryset.query.high_mark or queryset.query.high_mark default_queryset.query.clear_limits() - queryset = default_queryset & queryset + queryset = queryset & default_queryset queryset.query.set_limits(low, high) return queryset From 1d76db8164a2a3723e41141209a8923bad5044bd Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 9 Oct 2017 23:00:09 -0500 Subject: [PATCH 015/140] Use super for base queryset logic --- graphene_django/filter/fields.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index b93977b..62c50d9 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -43,8 +43,8 @@ class DjangoFilterConnectionField(DjangoConnectionField): def filtering_args(self): return get_filtering_args_from_filterset(self.filterset_class, self.node_type) - @staticmethod - def merge_querysets(default_queryset, queryset): + @classmethod + def merge_querysets(cls, default_queryset, queryset): # There could be the case where the default queryset (returned from the filterclass) # and the resolver queryset have some limits on it. # We only would be able to apply one of those, but not both @@ -61,7 +61,9 @@ class DjangoFilterConnectionField(DjangoConnectionField): low = default_queryset.query.low_mark or queryset.query.low_mark high = default_queryset.query.high_mark or queryset.query.high_mark default_queryset.query.clear_limits() - queryset = queryset & default_queryset + + queryset = super(cls, cls).merge_querysets(default_queryset, queryset) + queryset.query.set_limits(low, high) return queryset From 2600f0f0416fba8bd3ee89253a74dd49f60143e7 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 26 Oct 2017 00:21:11 -0700 Subject: [PATCH 016/140] Point to stable version of Graphene 2.0 --- README.md | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62a36f0..6f6d90a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra For instaling graphene, just run this command in your shell ```bash -pip install "graphene-django>=2.0.dev" +pip install "graphene-django>=2.0" ``` ### Settings diff --git a/README.rst b/README.rst index 27cbdc0..c19b802 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ For instaling graphene, just run this command in your shell .. code:: bash - pip install "graphene-django>=2.0.dev" + pip install "graphene-django>=2.0" Settings ~~~~~~~~ From 00b5a176d3e591cb5f007a30e8f43e0654b3f62a Mon Sep 17 00:00:00 2001 From: Vincent Poulailleau Date: Mon, 30 Oct 2017 09:54:09 +0100 Subject: [PATCH 017/140] typo in authorization.rst fix a small typo error in the documentation --- docs/authorization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authorization.rst b/docs/authorization.rst index 1e2ec81..707dbf6 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -34,7 +34,7 @@ This is easy, simply use the ``only_fields`` meta attribute. only_fields = ('title', 'content') interfaces = (relay.Node, ) -conversely you can use ``exclude_fields`` meta atrribute. +conversely you can use ``exclude_fields`` meta attribute. .. code:: python From 2a39f5d8eaba3f7772c63b012a974bb9a841fb9f Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Mon, 30 Oct 2017 14:35:29 -0700 Subject: [PATCH 018/140] Allow abstract Connection Class to DjangoObjectType referred to as connection_class, it will instantiate the connection from the provided class or default to graphene.Connection if not supplied --- graphene_django/tests/test_types.py | 15 ++++++++++++++- graphene_django/types.py | 8 ++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index f0185d4..83d9b40 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -1,6 +1,6 @@ from mock import patch -from graphene import Interface, ObjectType, Schema +from graphene import Interface, ObjectType, Schema, Connection, String from graphene.relay import Node from .. import registry @@ -17,11 +17,23 @@ class Reporter(DjangoObjectType): model = ReporterModel +class ArticleConnection(Connection): + '''Article Connection''' + test = String() + + def resolve_test(): + return 'test' + + class Meta: + abstract = True + + class Article(DjangoObjectType): '''Article description''' class Meta: model = ArticleModel interfaces = (Node, ) + connection_class = ArticleConnection class RootQuery(ObjectType): @@ -74,6 +86,7 @@ type Article implements Node { type ArticleConnection { pageInfo: PageInfo! edges: [ArticleEdge]! + test: String } type ArticleEdge { diff --git a/graphene_django/types.py b/graphene_django/types.py index aeef7a6..684863a 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -45,7 +45,7 @@ class DjangoObjectType(ObjectType): @classmethod def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False, only_fields=(), exclude_fields=(), filter_fields=None, connection=None, - use_connection=None, interfaces=(), **options): + connection_class=None, use_connection=None, interfaces=(), **options): assert is_valid_django_model(model), ( 'You need to pass a valid Django Model in {}.Meta, received "{}".' ).format(cls.__name__, model) @@ -71,7 +71,11 @@ class DjangoObjectType(ObjectType): if use_connection and not connection: # We create the connection automatically - connection = Connection.create_type('{}Connection'.format(cls.__name__), node=cls) + if not connection_class: + connection_class = Connection + + connection = connection_class.create_type( + '{}Connection'.format(cls.__name__), node=cls) if connection is not None: assert issubclass(connection, Connection), ( From b5e7614b053f52f55cc9ce54b6d61246dadec683 Mon Sep 17 00:00:00 2001 From: Abram Booth Date: Tue, 31 Oct 2017 09:33:16 -0400 Subject: [PATCH 019/140] More specific None check. --- graphene_django/converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index d7965fc..4ab16ab 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -41,7 +41,7 @@ def get_choices(choices): def convert_django_field_with_choices(field, registry=None): - if registry: + if registry is not None: converted = registry.get_converted_field(field) if converted: return converted @@ -63,7 +63,7 @@ def convert_django_field_with_choices(field, registry=None): converted = enum(description=field.help_text, required=not field.null) else: converted = convert_django_field(field, registry) - if registry: + if registry is not None: registry.register_converted_field(field, converted) return converted From bbcd69967c93d90853e024da9f4ab2073afa5cea Mon Sep 17 00:00:00 2001 From: Justin Tervay <7595639+tervay@users.noreply.github.com> Date: Wed, 1 Nov 2017 13:56:28 -0700 Subject: [PATCH 020/140] Fix typos --- docs/tutorial-plain.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 592f244..d5045aa 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -445,8 +445,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return Ingredient.objects.all() def resolve_category(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Category.objects.get(pk=id) @@ -457,8 +457,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return None def resolve_ingredient(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Ingredient.objects.get(pk=id) From 5491e2cb0094e23ae6f63c4b11f483d4ad33df84 Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Fri, 3 Nov 2017 12:25:22 -0700 Subject: [PATCH 021/140] add test to show .reverse() not being perserved --- graphene_django/filter/tests/test_fields.py | 81 ++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 9a0ba21..1844f4b 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -2,7 +2,7 @@ from datetime import datetime import pytest -from graphene import Field, ObjectType, Schema, Argument, Float +from graphene import Field, ObjectType, Schema, Argument, Float, Boolean from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.forms import (GlobalIDFormField, @@ -534,3 +534,82 @@ def test_should_query_filter_node_double_limit_raises(): assert str(result.errors[0]) == ( 'Received two sliced querysets (high mark) in the connection, please slice only in one.' ) + +def test_order_by_is_perserved(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node, ) + filter_fields = () + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterType, reverse_order=Boolean()) + + def resolve_all_reporters(self, info, reverse_order=False, **args): + reporters = Reporter.objects.order_by('first_name') + + if reverse_order: + return reporters.reverse() + + return reporters + + Reporter.objects.create( + first_name='b', + ) + r = Reporter.objects.create( + first_name='a', + ) + + schema = Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters(first: 1) { + edges { + node { + firstName + } + } + } + } + ''' + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'firstName': 'a', + } + }] + } + } + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + + reverse_query = ''' + query NodeFilteringQuery { + allReporters(first: 1, reverseOrder: true) { + edges { + node { + firstName + } + } + } + } + ''' + + reverse_expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'firstName': 'b', + } + }] + } + } + + reverse_result = schema.execute(reverse_query) + + assert not reverse_result.errors + assert reverse_result.data == reverse_expected From 6d0837e7cbf001992a599d54a5f9ecc0b1601dd6 Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Fri, 3 Nov 2017 12:26:33 -0700 Subject: [PATCH 022/140] add test to show annotation not being perservered --- graphene_django/filter/tests/test_fields.py | 59 ++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 1844f4b..ef09cfa 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -2,7 +2,7 @@ from datetime import datetime import pytest -from graphene import Field, ObjectType, Schema, Argument, Float, Boolean +from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.forms import (GlobalIDFormField, @@ -10,6 +10,10 @@ from graphene_django.forms import (GlobalIDFormField, from graphene_django.tests.models import Article, Pet, Reporter from graphene_django.utils import DJANGO_FILTER_INSTALLED +# for annotation test +from django.db.models import TextField, Value +from django.db.models.functions import Concat + pytestmark = [] if DJANGO_FILTER_INSTALLED: @@ -613,3 +617,56 @@ def test_order_by_is_perserved(): assert not reverse_result.errors assert reverse_result.data == reverse_expected + +def test_annotation_is_perserved(): + class ReporterType(DjangoObjectType): + full_name = String() + + def resolve_full_name(instance, info, **args): + return instance.full_name + + class Meta: + model = Reporter + interfaces = (Node, ) + filter_fields = () + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterType) + + def resolve_all_reporters(self, info, **args): + return Reporter.objects.annotate( + full_name=Concat('first_name', Value(' '), 'last_name', output_field=TextField()) + ) + + Reporter.objects.create( + first_name='John', + last_name='Doe', + ) + + schema = Schema(query=Query) + + query = ''' + query NodeFilteringQuery { + allReporters(first: 1) { + edges { + node { + fullName + } + } + } + } + ''' + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'fullName': 'John Doe', + } + }] + } + } + + result = schema.execute(query) + + assert not result.errors + assert result.data == expected From 4013f78ecb012dd5aad32dfc0718bce59ac35fea Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Fri, 3 Nov 2017 12:26:49 -0700 Subject: [PATCH 023/140] fix default_queryset overriding queryset when merging queries --- graphene_django/filter/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index a80d8d7..b93977b 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -61,7 +61,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): low = default_queryset.query.low_mark or queryset.query.low_mark high = default_queryset.query.high_mark or queryset.query.high_mark default_queryset.query.clear_limits() - queryset = default_queryset & queryset + queryset = queryset & default_queryset queryset.query.set_limits(low, high) return queryset From a3f3d90ab76cab8301f63e7f6377f153f470ead9 Mon Sep 17 00:00:00 2001 From: mekhami Date: Sat, 4 Nov 2017 12:29:58 -0500 Subject: [PATCH 024/140] Update README to reflect that resolve_only_args is deprecated As resolve_only_args is deprecated, let's remove it from the README. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 6f6d90a..1dd8301 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,6 @@ class User(DjangoObjectType): class Query(graphene.ObjectType): users = graphene.List(User) - @graphene.resolve_only_args def resolve_users(self): return UserModel.objects.all() From e7391937dd39f0a7f105e5814476bec4c91e0716 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Fri, 10 Nov 2017 21:49:48 +0000 Subject: [PATCH 025/140] fixes #322, fixed incorrect serializer instance usage --- graphene_django/rest_framework/mutation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 94d1e4b..58c6b9c 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -84,4 +84,9 @@ class SerializerMutation(ClientIDMutation): @classmethod def perform_mutate(cls, serializer, info): obj = serializer.save() - return cls(errors=None, **obj) + + kwargs = {} + for f, field in serializer.fields.items(): + kwargs[f] = field.get_attribute(obj) + + return cls(errors=None, **kwargs) From e05f41af405283a32052b2f04335488d3e42c0c3 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Sun, 12 Nov 2017 23:10:29 +0000 Subject: [PATCH 026/140] fixed indentation --- graphene_django/rest_framework/mutation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 58c6b9c..a776eab 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -87,6 +87,6 @@ class SerializerMutation(ClientIDMutation): kwargs = {} for f, field in serializer.fields.items(): - kwargs[f] = field.get_attribute(obj) + kwargs[f] = field.get_attribute(obj) return cls(errors=None, **kwargs) From 75e11a8195daf42b507c3d1fd8608f4c3269fa50 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Mon, 13 Nov 2017 16:06:07 +0000 Subject: [PATCH 027/140] add created field to test --- graphene_django/rest_framework/tests/test_mutation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index 852265d..bb3ba34 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -10,6 +10,7 @@ from ..mutation import SerializerMutation class MyFakeModel(models.Model): cool_name = models.CharField(max_length=50) + created = models.DateTimeField(auto_now_add=True) class MyModelSerializer(serializers.ModelSerializer): @@ -71,6 +72,7 @@ def test_nested_model(): model_input_type = model_input._type.of_type assert issubclass(model_input_type, InputObjectType) assert 'cool_name' in model_input_type._meta.fields + assert 'created' in model_input_type._meta.fields def test_mutate_and_get_payload_success(): From 1db1cbdd9ca9d3dc62158e8a5ecd03b7fc8d1e51 Mon Sep 17 00:00:00 2001 From: Samuel Cormier-Iijima Date: Mon, 13 Nov 2017 12:47:55 -0500 Subject: [PATCH 028/140] Fix passing request context to filterset creation Currently this code throws an error about `context` not being defined. --- graphene_django/filter/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index ae6f6a6..cf53ea3 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -73,7 +73,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): qs = filterset_class( data=filter_kwargs, queryset=default_manager.get_queryset(), - request=context + request=info.context ).qs return super(DjangoFilterConnectionField, cls).connection_resolver( From 3f08ed629ccd1ddb0d8e7630fc8fe3a2a8f085a6 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 13 Nov 2017 14:56:25 -0500 Subject: [PATCH 029/140] Fix infinite recursion when sublassing DjangoFilterConnectionField and merging querysets --- graphene_django/filter/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index ae6f6a6..f9297b6 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -61,7 +61,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): low = default_queryset.query.low_mark or queryset.query.low_mark high = default_queryset.query.high_mark or queryset.query.high_mark default_queryset.query.clear_limits() - queryset = super(cls, cls).merge_querysets(default_queryset, queryset) + queryset = super(DjangoFilterConnectionField, cls).merge_querysets(default_queryset, queryset) queryset.query.set_limits(low, high) return queryset From c72e7e55ebe4ff7fb3e4063c7fdc71deb406dd32 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Mon, 13 Nov 2017 20:33:12 +0000 Subject: [PATCH 030/140] added ModelSerializer tests --- django_test_settings.py | 1 + .../rest_framework/tests/test_mutation.py | 35 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/django_test_settings.py b/django_test_settings.py index 2e08272..9279a73 100644 --- a/django_test_settings.py +++ b/django_test_settings.py @@ -8,6 +8,7 @@ SECRET_KEY = 1 INSTALLED_APPS = [ 'graphene_django', + 'graphene_django.rest_framework', 'graphene_django.tests', 'starwars', ] diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index bb3ba34..c34a971 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -1,18 +1,16 @@ -from django.db import models +import datetime + from graphene import Field from graphene.types.inputobjecttype import InputObjectType from py.test import raises +from py.test import mark from rest_framework import serializers from ...types import DjangoObjectType +from ..models import MyFakeModel from ..mutation import SerializerMutation -class MyFakeModel(models.Model): - cool_name = models.CharField(max_length=50) - created = models.DateTimeField(auto_now_add=True) - - class MyModelSerializer(serializers.ModelSerializer): class Meta: model = MyFakeModel @@ -90,6 +88,19 @@ def test_mutate_and_get_payload_success(): assert result.errors is None +@mark.django_db +def test_model_mutate_and_get_payload_success(): + class MyMutation(SerializerMutation): + class Meta: + serializer_class = MyModelSerializer + + result = MyMutation.mutate_and_get_payload(None, None, **{ + 'cool_name': 'Narf', + }) + assert result.errors is None + assert result.cool_name == 'Narf' + assert isinstance(result.created, datetime.datetime) + def test_mutate_and_get_payload_error(): class MyMutation(SerializerMutation): @@ -98,4 +109,14 @@ def test_mutate_and_get_payload_error(): # missing required fields result = MyMutation.mutate_and_get_payload(None, None, **{}) - assert len(result.errors) > 0 \ No newline at end of file + assert len(result.errors) > 0 + +def test_model_mutate_and_get_payload_error(): + + class MyMutation(SerializerMutation): + class Meta: + serializer_class = MyModelSerializer + + # missing required fields + result = MyMutation.mutate_and_get_payload(None, None, **{}) + assert len(result.errors) > 0 From 345fe877c4c73e650a5fb9dd7e7aa3c918bca760 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Mon, 13 Nov 2017 20:35:00 +0000 Subject: [PATCH 031/140] added ModelSerializer tests --- graphene_django/rest_framework/models.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 graphene_django/rest_framework/models.py diff --git a/graphene_django/rest_framework/models.py b/graphene_django/rest_framework/models.py new file mode 100644 index 0000000..27ac2f4 --- /dev/null +++ b/graphene_django/rest_framework/models.py @@ -0,0 +1,6 @@ +from django.db import models + + +class MyFakeModel(models.Model): + cool_name = models.CharField(max_length=50) + created = models.DateTimeField(auto_now_add=True) \ No newline at end of file From 6cfd5b28540dbffcf96e6968ebe251cc3ecd6792 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Tue, 14 Nov 2017 22:10:26 +0000 Subject: [PATCH 032/140] added line ending --- graphene_django/rest_framework/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/rest_framework/models.py b/graphene_django/rest_framework/models.py index 27ac2f4..848837b 100644 --- a/graphene_django/rest_framework/models.py +++ b/graphene_django/rest_framework/models.py @@ -3,4 +3,4 @@ from django.db import models class MyFakeModel(models.Model): cool_name = models.CharField(max_length=50) - created = models.DateTimeField(auto_now_add=True) \ No newline at end of file + created = models.DateTimeField(auto_now_add=True) From 70a6c727514f9c13915faf3ec00a07c591fa5c0c Mon Sep 17 00:00:00 2001 From: Kees Kluskens Date: Sat, 18 Nov 2017 22:10:39 +0100 Subject: [PATCH 033/140] Docs: fix usage of deprecated `AbstractType` A tutorial still used `graphene.AbstractType` instead of `object`, which leads to a [deprecation warning](https://github.com/graphql-python/graphene/blob/master/UPGRADE-v2.0.md#abstracttype-deprecated). --- docs/tutorial-plain.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index d5045aa..eca7904 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -153,7 +153,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following: model = Ingredient - class Query(graphene.AbstractType): + class Query(object): all_categories = graphene.List(CategoryType) all_ingredients = graphene.List(IngredientType) @@ -426,7 +426,7 @@ We can update our schema to support that, by adding new query for ``ingredient`` model = Ingredient - class Query(graphene.AbstractType): + class Query(object): category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) From d7c160c3b012a12adb3124ecab582e3b52fa0644 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 20 Nov 2017 21:02:00 -0800 Subject: [PATCH 034/140] Pin current Graphene-Django version to Graphene~2 To avoid compatibility issues it's better to pin dependencies. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 232afd9..beee83c 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphene>=2.0', + 'graphene>=2.0,<3', 'Django>=1.8.0', 'iso8601', 'singledispatch>=3.4.0.3', From 90d84e8be99d25e9efddfe482f758d92fffc7d6b Mon Sep 17 00:00:00 2001 From: Pi Delport Date: Tue, 21 Nov 2017 15:10:52 +0200 Subject: [PATCH 035/140] README: Update resolve_users() example signature --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1dd8301..16fed50 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ class User(DjangoObjectType): class Query(graphene.ObjectType): users = graphene.List(User) - def resolve_users(self): + def resolve_users(self, info, **kwargs): return UserModel.objects.all() schema = graphene.Schema(query=Query) From d8a4b4a0fd7d2124a50d82f9ba74e72db9c8f99e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 22 Nov 2017 13:47:18 -0800 Subject: [PATCH 036/140] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16fed50..639d70e 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ class User(DjangoObjectType): class Query(graphene.ObjectType): users = graphene.List(User) - def resolve_users(self, info, **kwargs): + def resolve_users(self, info): return UserModel.objects.all() schema = graphene.Schema(query=Query) From 8a3ba7a321e81ad6cb0b907e918ca1725a6cfcd0 Mon Sep 17 00:00:00 2001 From: Michael Tom Date: Tue, 28 Nov 2017 21:02:53 -0500 Subject: [PATCH 037/140] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 639d70e..4e0b01d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra ## Installation -For instaling graphene, just run this command in your shell +For installing graphene, just run this command in your shell ```bash pip install "graphene-django>=2.0" From 3aeee3af3ddeb90f90061c7ff5fc0d4f8dfca133 Mon Sep 17 00:00:00 2001 From: Michael Tom Date: Tue, 28 Nov 2017 21:04:08 -0500 Subject: [PATCH 038/140] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c19b802..a96e60f 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ A `Django `__ integration for Installation ------------ -For instaling graphene, just run this command in your shell +For installing graphene, just run this command in your shell .. code:: bash From 40610c64a3be003719d88db439e442085ed18072 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Wed, 29 Nov 2017 14:12:02 -0600 Subject: [PATCH 039/140] Support instance kwarg --- graphene_django/forms/mutation.py | 40 ++++++++++++++------ graphene_django/forms/tests/test_mutation.py | 34 ++++++++++++----- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 49fabb8..876e76a 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -27,13 +27,13 @@ def fields_for_form(form, only_fields, exclude_fields): return fields -class BaseFormMutation(ClientIDMutation): +class BaseDjangoFormMutation(ClientIDMutation): class Meta: abstract = True @classmethod def mutate_and_get_payload(cls, root, info, **input): - form = cls._meta.form_class(data=input) + form = cls.get_form(root, info, **input) if form.is_valid(): return cls.perform_mutate(form, info) @@ -45,12 +45,28 @@ class BaseFormMutation(ClientIDMutation): return cls(errors=errors) + @classmethod + def get_form(cls, root, info, **input): + form_kwargs = cls.get_form_kwargs(root, info, **input) + return cls._meta.form_class(**form_kwargs) -class FormMutationOptions(MutationOptions): + @classmethod + def get_form_kwargs(cls, root, info, **input): + kwargs = {'data': input} + + pk = input.pop('id', None) + if pk: + instance = cls._meta.model._default_manager.get(pk=pk) + kwargs['instance'] = instance + + return kwargs + + +class DjangoFormMutationOptions(MutationOptions): form_class = None -class FormMutation(BaseFormMutation): +class DjangoFormMutation(BaseDjangoFormMutation): class Meta: abstract = True @@ -67,7 +83,7 @@ class FormMutation(BaseFormMutation): input_fields = fields_for_form(form, only_fields, exclude_fields) output_fields = fields_for_form(form, only_fields, exclude_fields) - _meta = FormMutationOptions(cls) + _meta = DjangoFormMutationOptions(cls) _meta.form_class = form_class _meta.fields = yank_fields_from_attrs( output_fields, @@ -78,7 +94,7 @@ class FormMutation(BaseFormMutation): input_fields, _as=InputField, ) - super(FormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) + super(DjangoFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) @classmethod def perform_mutate(cls, form, info): @@ -86,12 +102,12 @@ class FormMutation(BaseFormMutation): return cls(errors=[]) -class ModelFormMutationOptions(FormMutationOptions): +class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions): model = None return_field_name = None -class ModelFormMutation(BaseFormMutation): +class DjangoModelFormMutation(BaseDjangoFormMutation): class Meta: abstract = True @@ -102,13 +118,13 @@ class ModelFormMutation(BaseFormMutation): only_fields=(), exclude_fields=(), **options): if not form_class: - raise Exception('form_class is required for ModelFormMutation') + raise Exception('form_class is required for DjangoModelFormMutation') if not model: model = form_class._meta.model if not model: - raise Exception('model is required for ModelFormMutation') + raise Exception('model is required for DjangoModelFormMutation') form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) @@ -119,7 +135,7 @@ class ModelFormMutation(BaseFormMutation): output_fields = OrderedDict() output_fields[return_field_name] = graphene.Field(model_type) - _meta = ModelFormMutationOptions(cls) + _meta = DjangoModelDjangoFormMutationOptions(cls) _meta.form_class = form_class _meta.model = model _meta.return_field_name = return_field_name @@ -132,7 +148,7 @@ class ModelFormMutation(BaseFormMutation): input_fields, _as=InputField, ) - super(ModelFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) + super(DjangoModelFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) @classmethod def perform_mutate(cls, form, info): diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 084b8b0..5876405 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -3,7 +3,7 @@ from django.test import TestCase from py.test import raises from graphene_django.tests.models import Pet, Film -from ..mutation import FormMutation, ModelFormMutation +from ..mutation import DjangoFormMutation, DjangoModelFormMutation class MyForm(forms.Form): @@ -19,14 +19,14 @@ class PetForm(forms.ModelForm): def test_needs_form_class(): with raises(Exception) as exc: - class MyMutation(FormMutation): + class MyMutation(DjangoFormMutation): pass assert exc.value.args[0] == 'form_class is required for FormMutation' def test_has_output_fields(): - class MyMutation(FormMutation): + class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm @@ -34,7 +34,7 @@ def test_has_output_fields(): def test_has_input_fields(): - class MyMutation(FormMutation): + class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm @@ -44,7 +44,7 @@ def test_has_input_fields(): class ModelFormMutationTests(TestCase): def test_default_meta_fields(self): - class PetMutation(ModelFormMutation): + class PetMutation(DjangoModelFormMutation): class Meta: form_class = PetForm @@ -53,7 +53,7 @@ class ModelFormMutationTests(TestCase): self.assertIn('pet', PetMutation._meta.fields) def test_custom_return_field_name(self): - class PetMutation(ModelFormMutation): + class PetMutation(DjangoModelFormMutation): class Meta: form_class = PetForm model = Film @@ -64,19 +64,33 @@ class ModelFormMutationTests(TestCase): self.assertIn('animal', PetMutation._meta.fields) def test_model_form_mutation_mutate(self): - class PetMutation(ModelFormMutation): + class PetMutation(DjangoModelFormMutation): class Meta: form_class = PetForm - result = PetMutation.mutate_and_get_payload(None, None, name='Fluffy') + pet = Pet.objects.create(name='Axel') + + result = PetMutation.mutate_and_get_payload(None, None, id=pet.pk, name='Mia') + + self.assertEqual(Pet.objects.count(), 1) + pet.refresh_from_db() + self.assertEqual(pet.name, 'Mia') + self.assertEqual(result.errors, []) + + def test_model_form_mutation_updates_existing_(self): + class PetMutation(DjangoModelFormMutation): + class Meta: + form_class = PetForm + + result = PetMutation.mutate_and_get_payload(None, None, name='Mia') self.assertEqual(Pet.objects.count(), 1) pet = Pet.objects.get() - self.assertEqual(pet.name, 'Fluffy') + self.assertEqual(pet.name, 'Mia') self.assertEqual(result.errors, []) def test_model_form_mutation_mutate_invalid_form(self): - class PetMutation(ModelFormMutation): + class PetMutation(DjangoModelFormMutation): class Meta: form_class = PetForm From d6dbe2a4a81dd069d12bfb75f8e997f8105d7335 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Wed, 29 Nov 2017 14:20:56 -0600 Subject: [PATCH 040/140] Default return_field_name is camcelcased --- graphene_django/forms/mutation.py | 6 +++++- graphene_django/forms/tests/test_mutation.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 876e76a..58e0075 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -131,7 +131,11 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): registry = get_global_registry() model_type = registry.get_type_for_model(model) - return_field_name = return_field_name or model._meta.model_name + return_field_name = return_field_name + if not return_field_name: + model_name = model.__name__ + return_field_name = model_name[:1].lower() + model_name[1:] + output_fields = OrderedDict() output_fields[return_field_name] = graphene.Field(model_type) diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 5876405..3bfb883 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -2,7 +2,7 @@ from django import forms from django.test import TestCase from py.test import raises -from graphene_django.tests.models import Pet, Film +from graphene_django.tests.models import Pet, Film, FilmDetails from ..mutation import DjangoFormMutation, DjangoModelFormMutation @@ -52,6 +52,15 @@ class ModelFormMutationTests(TestCase): self.assertEqual(PetMutation._meta.return_field_name, 'pet') self.assertIn('pet', PetMutation._meta.fields) + def test_return_field_name_is_camelcased(self): + class PetMutation(DjangoModelFormMutation): + class Meta: + form_class = PetForm + model = FilmDetails + + self.assertEqual(PetMutation._meta.model, FilmDetails) + self.assertEqual(PetMutation._meta.return_field_name, 'filmDetails') + def test_custom_return_field_name(self): class PetMutation(DjangoModelFormMutation): class Meta: From 748dc4c50994c99301143aaff36035ac513309f6 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Wed, 29 Nov 2017 15:15:16 -0600 Subject: [PATCH 041/140] Add id input field to model form mutation --- graphene_django/forms/mutation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 58e0075..e8e76a2 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -77,7 +77,7 @@ class DjangoFormMutation(BaseDjangoFormMutation): only_fields=(), exclude_fields=(), **options): if not form_class: - raise Exception('form_class is required for FormMutation') + raise Exception('form_class is required for DjangoFormMutation') form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) @@ -128,6 +128,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) + input_fields['id'] = convert_form_field(model._meta.pk) registry = get_global_registry() model_type = registry.get_type_for_model(model) From 6d7a0d053dfff709dabef85d5509a648df594715 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Wed, 29 Nov 2017 15:25:24 -0600 Subject: [PATCH 042/140] Make id field an ID type --- graphene_django/forms/mutation.py | 2 +- graphene_django/forms/tests/test_mutation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index e8e76a2..793aafe 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -128,7 +128,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) - input_fields['id'] = convert_form_field(model._meta.pk) + input_fields['id'] = graphene.ID() registry = get_global_registry() model_type = registry.get_type_for_model(model) diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 3bfb883..10a15ae 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -22,7 +22,7 @@ def test_needs_form_class(): class MyMutation(DjangoFormMutation): pass - assert exc.value.args[0] == 'form_class is required for FormMutation' + assert exc.value.args[0] == 'form_class is required for DjangoFormMutation' def test_has_output_fields(): From c3938d1e4f7679f61a939aa875ac4e37734b47a1 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Wed, 29 Nov 2017 15:31:26 -0600 Subject: [PATCH 043/140] Fix line length --- graphene_django/forms/mutation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 793aafe..4ce9a5d 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -153,7 +153,11 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): input_fields, _as=InputField, ) - super(DjangoModelFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) + super(DjangoModelFormMutation, cls).__init_subclass_with_meta__( + _meta=_meta, + input_fields=input_fields, + **options + ) @classmethod def perform_mutate(cls, form, info): From 443b5eaafc22919d828b30a1d86cf8c117c0b8e6 Mon Sep 17 00:00:00 2001 From: Stefan Stammberger Date: Sat, 2 Dec 2017 19:35:17 +0100 Subject: [PATCH 044/140] Docs: Update Cookbook tutorial --- docs/tutorial-plain.rst | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index eca7904..966d973 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -68,7 +68,8 @@ Let's get started with these models: class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField() - category = models.ForeignKey(Category, related_name='ingredients') + category = models.ForeignKey( + Category, related_name='ingredients', on_delete=models.CASCADE) def __str__(self): return self.name @@ -80,9 +81,21 @@ Add ingredients as INSTALLED_APPS: INSTALLED_APPS = [ ... # Install the ingredients app - 'ingredients', + 'cookbook.ingredients', ] +Register models with admin panel: + +.. code:: python + + # cookbook/ingredients/admin.py + from django.contrib import admin + from cookbook.ingredients.models import Category, Ingredient + + admin.site.register(Category) + admin.site.register(Ingredient) + + Don't forget to create & run migrations: .. code:: bash From 73706fa6bbe692642db28a622d28db7894506072 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 5 Dec 2017 15:01:52 -0500 Subject: [PATCH 045/140] Add on_delete atrributes to test models foreignkeys --- examples/starwars/models.py | 6 +++--- graphene_django/tests/models.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/starwars/models.py b/examples/starwars/models.py index 2f80e27..45741da 100644 --- a/examples/starwars/models.py +++ b/examples/starwars/models.py @@ -5,7 +5,7 @@ from django.db import models class Character(models.Model): name = models.CharField(max_length=50) - ship = models.ForeignKey('Ship', blank=True, null=True, related_name='characters') + ship = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True, related_name='characters') def __str__(self): return self.name @@ -13,7 +13,7 @@ class Character(models.Model): class Faction(models.Model): name = models.CharField(max_length=50) - hero = models.ForeignKey(Character) + hero = models.ForeignKey(Character, on_delete=models.CASCADE) def __str__(self): return self.name @@ -21,7 +21,7 @@ class Faction(models.Model): class Ship(models.Model): name = models.CharField(max_length=50) - faction = models.ForeignKey(Faction, related_name='ships') + faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name='ships') def __str__(self): return self.name diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index 0c62f28..db1fca4 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -15,7 +15,7 @@ class Pet(models.Model): class FilmDetails(models.Model): location = models.CharField(max_length=30) - film = models.OneToOneField('Film', related_name='details') + film = models.OneToOneField('Film', on_delete=models.CASCADE, related_name='details') class Film(models.Model): @@ -37,8 +37,8 @@ class Reporter(models.Model): class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() - reporter = models.ForeignKey(Reporter, related_name='articles') - editor = models.ForeignKey(Reporter, related_name='edited_articles_+') + reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE, related_name='articles') + editor = models.ForeignKey(Reporter, on_delete=models.CASCADE, related_name='edited_articles_+') lang = models.CharField(max_length=2, help_text='Language', choices=[ ('es', 'Spanish'), ('en', 'English') From 62c0694901cb0e4f711750bebb2901d509088cc0 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 5 Dec 2017 15:04:29 -0500 Subject: [PATCH 046/140] models.DateField corresponds to graphene Date Scalar --- graphene_django/converter.py | 9 +++++++-- graphene_django/filter/tests/test_fields.py | 10 ++++++---- graphene_django/rest_framework/serializer_converter.py | 6 +++++- .../rest_framework/tests/test_field_converter.py | 4 ++-- graphene_django/tests/models.py | 1 + graphene_django/tests/test_converter.py | 7 +++++-- graphene_django/tests/test_form_converter.py | 2 ++ graphene_django/tests/test_query.py | 7 +++++++ graphene_django/tests/test_types.py | 7 +++++-- 9 files changed, 40 insertions(+), 13 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index dff77a8..fa771e2 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -3,7 +3,7 @@ from django.utils.encoding import force_text from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List, NonNull, String, UUID) -from graphene.types.datetime import DateTime, Time +from graphene.types.datetime import DateTime, Date, Time from graphene.types.json import JSONString from graphene.utils.str_converters import to_camel_case, to_const from graphql import assert_valid_name @@ -121,11 +121,16 @@ def convert_field_to_float(field, registry=None): return Float(description=field.help_text, required=not field.null) -@convert_django_field.register(models.DateField) +@convert_django_field.register(models.DateTimeField) def convert_date_to_string(field, registry=None): return DateTime(description=field.help_text, required=not field.null) +@convert_django_field.register(models.DateField) +def convert_date_to_string(field, registry=None): + return Date(description=field.help_text, required=not field.null) + + @convert_django_field.register(models.TimeField) def convert_time_to_string(field, registry=None): return Time(description=field.help_text, required=not field.null) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 258da3e..c730ef3 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -157,8 +157,8 @@ def test_filter_shortcut_filterset_context(): r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com') r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com') - Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1, editor=r1) - Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2, editor=r2) + Article.objects.create(headline='a1', pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r1, editor=r1) + Article.objects.create(headline='a2', pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r2, editor=r2) class context(object): reporter = r2 @@ -245,8 +245,8 @@ def test_filter_filterset_related_results(): r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com') r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com') - Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1) - Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2) + Article.objects.create(headline='a1', pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r1) + Article.objects.create(headline='a2', pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r2) query = ''' query { @@ -464,6 +464,7 @@ def test_should_query_filter_node_limit(): Article.objects.create( headline='Article Node 1', pub_date=datetime.now(), + pub_date_time=datetime.now(), reporter=r, editor=r, lang='es' @@ -471,6 +472,7 @@ def test_should_query_filter_node_limit(): Article.objects.create( headline='Article Node 2', pub_date=datetime.now(), + pub_date_time=datetime.now(), reporter=r, editor=r, lang='en' diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 6a57f5f..0c10a65 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -92,11 +92,15 @@ def convert_serializer_field_to_float(field): @get_graphene_type_from_serializer_field.register(serializers.DateTimeField) -@get_graphene_type_from_serializer_field.register(serializers.DateField) def convert_serializer_field_to_date_time(field): return graphene.types.datetime.DateTime +@get_graphene_type_from_serializer_field.register(serializers.DateField) +def convert_serializer_field_to_date_time(field): + return graphene.types.datetime.Date + + @get_graphene_type_from_serializer_field.register(serializers.TimeField) def convert_serializer_field_to_time(field): return graphene.types.datetime.Time diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 623cf58..ec851c2 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -87,8 +87,8 @@ def test_should_date_time_convert_datetime(): assert_conversion(serializers.DateTimeField, graphene.types.datetime.DateTime) -def test_should_date_convert_datetime(): - assert_conversion(serializers.DateField, graphene.types.datetime.DateTime) +def test_should_date_convert_date(): + assert_conversion(serializers.DateField, graphene.types.datetime.Date) def test_should_time_convert_time(): diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index db1fca4..56fcecf 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -37,6 +37,7 @@ class Reporter(models.Model): class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() + pub_date_time = models.DateTimeField() reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE, related_name='articles') editor = models.ForeignKey(Reporter, on_delete=models.CASCADE, related_name='edited_articles_+') lang = models.CharField(max_length=2, help_text='Language', choices=[ diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index d616106..3dd2963 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -5,7 +5,7 @@ from py.test import raises import graphene from graphene.relay import ConnectionField, Node -from graphene.types.datetime import DateTime, Time +from graphene.types.datetime import DateTime, Date, Time from graphene.types.json import JSONString from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType @@ -38,9 +38,12 @@ def test_should_unknown_django_field_raise_exception(): convert_django_field(None) assert 'Don\'t know how to convert the Django field' in str(excinfo.value) +def test_should_date_time_convert_string(): + assert_conversion(models.DateTimeField, DateTime) + def test_should_date_convert_string(): - assert_conversion(models.DateField, DateTime) + assert_conversion(models.DateField, Date) def test_should_time_convert_string(): diff --git a/graphene_django/tests/test_form_converter.py b/graphene_django/tests/test_form_converter.py index 5a13554..97932d8 100644 --- a/graphene_django/tests/test_form_converter.py +++ b/graphene_django/tests/test_form_converter.py @@ -30,6 +30,8 @@ def test_should_date_convert_string(): def test_should_time_convert_string(): assert_conversion(forms.TimeField, graphene.String) +def test_should_date_convert_string(): + assert_conversion(forms.DateField, graphene.String) def test_should_date_time_convert_string(): assert_conversion(forms.DateTimeField, graphene.String) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 0dece3f..96e0f32 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -371,6 +371,7 @@ def test_should_query_node_filtering(): Article.objects.create( headline='Article Node 1', pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang='es' @@ -378,6 +379,7 @@ def test_should_query_node_filtering(): Article.objects.create( headline='Article Node 2', pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang='en' @@ -453,6 +455,7 @@ def test_should_query_node_multiple_filtering(): Article.objects.create( headline='Article Node 1', pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang='es' @@ -460,6 +463,7 @@ def test_should_query_node_multiple_filtering(): Article.objects.create( headline='Article Node 2', pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang='es' @@ -467,6 +471,7 @@ def test_should_query_node_multiple_filtering(): Article.objects.create( headline='Article Node 3', pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang='en' @@ -692,6 +697,7 @@ def test_should_query_dataloader_fields(): Article.objects.create( headline='Article Node 1', pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang='es' @@ -699,6 +705,7 @@ def test_should_query_dataloader_fields(): Article.objects.create( headline='Article Node 2', pub_date=datetime.date.today(), + pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang='en' diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 83d9b40..0e0679b 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -64,7 +64,7 @@ def test_django_objecttype_map_correct_fields(): def test_django_objecttype_with_node_have_correct_fields(): fields = Article._meta.fields - assert list(fields.keys()) == ['id', 'headline', 'pub_date', 'reporter', 'editor', 'lang', 'importance'] + assert list(fields.keys()) == ['id', 'headline', 'pub_date', 'pub_date_time', 'reporter', 'editor', 'lang', 'importance'] def test_schema_representation(): @@ -76,7 +76,8 @@ schema { type Article implements Node { id: ID! headline: String! - pubDate: DateTime! + pubDate: Date! + pubDateTime: DateTime! reporter: Reporter! editor: Reporter! lang: ArticleLang! @@ -104,6 +105,8 @@ enum ArticleLang { EN } +scalar Date + scalar DateTime interface Node { From d314d1041fc86bf08224bf0ddb8da7f7d279cd5b Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 6 Dec 2017 12:45:03 -0500 Subject: [PATCH 047/140] rel -> remote_field: Update Django DeprecatedAttribute --- graphene_django/filter/filterset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py index c716b05..4873f71 100644 --- a/graphene_django/filter/filterset.py +++ b/graphene_django/filter/filterset.py @@ -57,7 +57,7 @@ class GrapheneFilterSetMixin(BaseFilterSet): Global IDs (the default implementation expects database primary keys) """ - rel = f.field.rel + rel = f.field.remote_field default = { 'name': name, 'label': capfirst(rel.related_name) From 384bff08e8fa6fb61cf41be81d914ddb65d70a03 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 6 Dec 2017 13:10:03 -0500 Subject: [PATCH 048/140] Add fallback for depracated atrribute --- graphene_django/filter/filterset.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py index 4873f71..231a8e1 100644 --- a/graphene_django/filter/filterset.py +++ b/graphene_django/filter/filterset.py @@ -57,7 +57,10 @@ class GrapheneFilterSetMixin(BaseFilterSet): Global IDs (the default implementation expects database primary keys) """ - rel = f.field.remote_field + try: + rel = f.field.remote_field + except AttributeError: + rel = f.field.rel default = { 'name': name, 'label': capfirst(rel.related_name) From 489d2878da1d3563930c47329baffe08a8af5e38 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 6 Dec 2017 15:10:50 -0500 Subject: [PATCH 049/140] django version depends on python version --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index beee83c..2e835d1 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ tests_require = [ 'pytest-django==2.9.1', ] + rest_framework_require +django_version = 'Django>=1.8.0,<2' if sys.version_info[0] < 3 else 'Django>=1.8.0' setup( name='graphene-django', version=version, @@ -58,7 +59,7 @@ setup( install_requires=[ 'six>=1.10.0', 'graphene>=2.0,<3', - 'Django>=1.8.0', + django_version, 'iso8601', 'singledispatch>=3.4.0.3', 'promise>=2.1', From f687406ac5f2b1b7ff55a0f1ae91dc1117f371c8 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 5 Dec 2017 15:01:52 -0500 Subject: [PATCH 050/140] Add on_delete atrributes to test models foreignkeys --- examples/starwars/models.py | 6 +++--- graphene_django/tests/models.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/starwars/models.py b/examples/starwars/models.py index 2f80e27..45741da 100644 --- a/examples/starwars/models.py +++ b/examples/starwars/models.py @@ -5,7 +5,7 @@ from django.db import models class Character(models.Model): name = models.CharField(max_length=50) - ship = models.ForeignKey('Ship', blank=True, null=True, related_name='characters') + ship = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True, related_name='characters') def __str__(self): return self.name @@ -13,7 +13,7 @@ class Character(models.Model): class Faction(models.Model): name = models.CharField(max_length=50) - hero = models.ForeignKey(Character) + hero = models.ForeignKey(Character, on_delete=models.CASCADE) def __str__(self): return self.name @@ -21,7 +21,7 @@ class Faction(models.Model): class Ship(models.Model): name = models.CharField(max_length=50) - faction = models.ForeignKey(Faction, related_name='ships') + faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name='ships') def __str__(self): return self.name diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index 0c62f28..db1fca4 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -15,7 +15,7 @@ class Pet(models.Model): class FilmDetails(models.Model): location = models.CharField(max_length=30) - film = models.OneToOneField('Film', related_name='details') + film = models.OneToOneField('Film', on_delete=models.CASCADE, related_name='details') class Film(models.Model): @@ -37,8 +37,8 @@ class Reporter(models.Model): class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() - reporter = models.ForeignKey(Reporter, related_name='articles') - editor = models.ForeignKey(Reporter, related_name='edited_articles_+') + reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE, related_name='articles') + editor = models.ForeignKey(Reporter, on_delete=models.CASCADE, related_name='edited_articles_+') lang = models.CharField(max_length=2, help_text='Language', choices=[ ('es', 'Spanish'), ('en', 'English') From 61829abc8c7d15587cc63866e907a1462f8deab3 Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 7 Dec 2017 13:06:31 -0500 Subject: [PATCH 051/140] Add testing to django 2 --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index ef8a3d6..d2349ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,6 +38,12 @@ env: matrix: fast_finish: true include: + - python: '3.4' + env: TEST_TYPE=build DJANGO_VERSION=2.0 + - python: '3.5' + env: TEST_TYPE=build DJANGO_VERSION=2.0 + - python: '3.6' + env: TEST_TYPE=build DJANGO_VERSION=2.0 - python: '2.7' env: TEST_TYPE=build DJANGO_VERSION=1.8 - python: '2.7' From f8a5860f3474b9d5839f7220c56698b83abb3550 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 6 Dec 2017 15:10:50 -0500 Subject: [PATCH 052/140] django version depends on python version --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index beee83c..2e835d1 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ tests_require = [ 'pytest-django==2.9.1', ] + rest_framework_require +django_version = 'Django>=1.8.0,<2' if sys.version_info[0] < 3 else 'Django>=1.8.0' setup( name='graphene-django', version=version, @@ -58,7 +59,7 @@ setup( install_requires=[ 'six>=1.10.0', 'graphene>=2.0,<3', - 'Django>=1.8.0', + django_version, 'iso8601', 'singledispatch>=3.4.0.3', 'promise>=2.1', From d2db5f5584c74bbcdccd05b9099d5318a44adea3 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 10 Dec 2017 00:53:13 -0500 Subject: [PATCH 053/140] Remove Django1.7 shims --- graphene_django/form_converter.py | 8 +-- .../management/commands/graphql_schema.py | 71 ++++++------------- 2 files changed, 21 insertions(+), 58 deletions(-) diff --git a/graphene_django/form_converter.py b/graphene_django/form_converter.py index 46a38b3..195c8c4 100644 --- a/graphene_django/form_converter.py +++ b/graphene_django/form_converter.py @@ -8,12 +8,6 @@ from .utils import import_single_dispatch singledispatch = import_single_dispatch() -try: - UUIDField = forms.UUIDField -except AttributeError: - class UUIDField(object): - pass - @singledispatch def convert_form_field(field): @@ -36,7 +30,7 @@ def convert_form_field_to_string(field): return String(description=field.help_text, required=field.required) -@convert_form_field.register(UUIDField) +@convert_form_field.register(forms.UUIDField) def convert_form_field_to_uuid(field): return UUID(description=field.help_text, required=field.required) diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index 7e2dbac..3a1690a 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -1,64 +1,33 @@ import importlib import json -from distutils.version import StrictVersion -from optparse import make_option -from django import get_version as get_django_version from django.core.management.base import BaseCommand, CommandError from graphene_django.settings import graphene_settings -LT_DJANGO_1_8 = StrictVersion(get_django_version()) < StrictVersion('1.8') +class CommandArguments(BaseCommand): -if LT_DJANGO_1_8: - class CommandArguments(BaseCommand): - option_list = BaseCommand.option_list + ( - make_option( - '--schema', - type=str, - dest='schema', - default='', - help='Django app containing schema to dump, e.g. myproject.core.schema.schema', - ), - make_option( - '--out', - type=str, - dest='out', - default='', - help='Output file (default: schema.json)' - ), - make_option( - '--indent', - type=int, - dest='indent', - default=None, - help='Output file indent (default: None)' - ), - ) -else: - class CommandArguments(BaseCommand): + def add_arguments(self, parser): + parser.add_argument( + '--schema', + type=str, + dest='schema', + default=graphene_settings.SCHEMA, + help='Django app containing schema to dump, e.g. myproject.core.schema.schema') - def add_arguments(self, parser): - parser.add_argument( - '--schema', - type=str, - dest='schema', - default=graphene_settings.SCHEMA, - help='Django app containing schema to dump, e.g. myproject.core.schema.schema') + parser.add_argument( + '--out', + type=str, + dest='out', + default=graphene_settings.SCHEMA_OUTPUT, + help='Output file (default: schema.json)') - parser.add_argument( - '--out', - type=str, - dest='out', - default=graphene_settings.SCHEMA_OUTPUT, - help='Output file (default: schema.json)') - - parser.add_argument( - '--indent', - type=int, - dest='indent', - default=graphene_settings.SCHEMA_INDENT, - help='Output file indent (default: None)') + parser.add_argument( + '--indent', + type=int, + dest='indent', + default=graphene_settings.SCHEMA_INDENT, + help='Output file indent (default: None)') class Command(CommandArguments): From 6008cb6de44458f91a23359b60e8b5e78c7342a0 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 11 Dec 2017 21:08:42 -0500 Subject: [PATCH 054/140] Fix qfactor rankings for HTTP-ACCEPT --- graphene_django/tests/test_views.py | 14 ++++++++++++++ graphene_django/views.py | 11 +++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py index c31db8d..ae6cf6f 100644 --- a/graphene_django/tests/test_views.py +++ b/graphene_django/tests/test_views.py @@ -30,6 +30,20 @@ jl = lambda **kwargs: json.dumps([kwargs]) def test_graphiql_is_enabled(client): response = client.get(url_string(), HTTP_ACCEPT='text/html') assert response.status_code == 200 + assert response['Content-Type'].split(';')[0] == 'text/html' + +def test_qfactor_graphiql(client): + response = client.get(url_string(query='{test}'), HTTP_ACCEPT='application/json;q=0.8, text/html;q=0.9') + assert response.status_code == 200 + assert response['Content-Type'].split(';')[0] == 'text/html' + +def test_qfactor_json(client): + response = client.get(url_string(query='{test}'), HTTP_ACCEPT='text/html;q=0.8, application/json;q=0.9') + assert response.status_code == 200 + assert response['Content-Type'].split(';')[0] == 'application/json' + assert response_json(response) == { + 'data': {'test': "Hello World"} + } def test_allows_get_with_query_param(client): diff --git a/graphene_django/views.py b/graphene_django/views.py index cc9e8bb..8b413f2 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -35,8 +35,8 @@ def get_accepted_content_types(request): match = re.match(r'(^|;)q=(0(\.\d{,3})?|1(\.0{,3})?)(;|$)', parts[1]) if match: - return parts[0], float(match.group(2)) - return parts[0], 1 + return parts[0].strip(), float(match.group(2)) + return parts[0].strip(), 1 raw_content_types = request.META.get('HTTP_ACCEPT', '*/*').split(',') qualified_content_types = map(qualify, raw_content_types) @@ -280,8 +280,11 @@ class GraphQLView(View): @classmethod def request_wants_html(cls, request): accepted = get_accepted_content_types(request) - html_index = accepted.count('text/html') - json_index = accepted.count('application/json') + accepted_length = len(accepted) + #the list will be ordered in preferred first - so we have to make + #sure the most preferred gets the highest number + html_index = accepted_length - accepted.index('text/html') if 'text/html' in accepted else 0 + json_index = accepted_length - accepted.index('application/json') if 'application/json' in accepted else 0 return html_index > json_index From 65e63026a0d3f2da18a8df1055303ae2cd9f83ad Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 11 Dec 2017 21:23:00 -0500 Subject: [PATCH 055/140] Add tests --- .../rest_framework/tests/test_mutation.py | 13 +++++++++++++ graphene_django/tests/test_forms.py | 13 ++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index c34a971..491192a 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -52,6 +52,19 @@ def test_has_input_fields(): assert 'model' in MyMutation.Input._meta.fields +def test_exclude_fields(): + class MyMutation(SerializerMutation): + class Meta: + serializer_class = MyModelSerializer + exclude_fields = ['created'] + + assert 'cool_name' in MyMutation._meta.fields + assert 'created' not in MyMutation._meta.fields + assert 'errors' in MyMutation._meta.fields + assert 'cool_name' in MyMutation.Input._meta.fields + assert 'created' not in MyMutation.Input._meta.fields + + def test_nested_model(): class MyFakeModelGrapheneType(DjangoObjectType): diff --git a/graphene_django/tests/test_forms.py b/graphene_django/tests/test_forms.py index ada9e8a..b15e866 100644 --- a/graphene_django/tests/test_forms.py +++ b/graphene_django/tests/test_forms.py @@ -1,7 +1,7 @@ from django.core.exceptions import ValidationError from py.test import raises -from ..forms import GlobalIDFormField +from ..forms import GlobalIDFormField,GlobalIDMultipleChoiceField # 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc' @@ -18,6 +18,17 @@ def test_global_id_invalid(): field.clean('badvalue') +def test_global_id_multiple_valid(): + field = GlobalIDMultipleChoiceField() + field.clean(['TXlUeXBlOmFiYw==', 'TXlUeXBlOmFiYw==']) + + +def test_global_id_multiple_invalid(): + field = GlobalIDMultipleChoiceField() + with raises(ValidationError): + field.clean(['badvalue', 'another bad avue']) + + def test_global_id_none(): field = GlobalIDFormField() with raises(ValidationError): From de59d26968752b749141dc5122bec2e3fa9c7af8 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 12 Dec 2017 12:24:11 -0500 Subject: [PATCH 056/140] Test: erro if last is greater than max - plus fix wrong variable --- graphene_django/fields.py | 2 +- graphene_django/tests/test_query.py | 47 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index aa7f124..e755b93 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -116,7 +116,7 @@ class DjangoConnectionField(ConnectionField): if last: assert last <= max_limit, ( 'Requesting {} records on the `{}` connection exceeds the `last` limit of {} records.' - ).format(first, info.field_name, max_limit) + ).format(last, info.field_name, max_limit) args['last'] = min(last, max_limit) iterable = resolver(root, info, **args) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 0dece3f..7bb8fa8 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -606,6 +606,53 @@ def test_should_error_if_first_is_greater_than_max(): graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False +def test_should_error_if_last_is_greater_than_max(): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + + schema = graphene.Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters(last: 101) { + edges { + node { + id + } + } + } + } + ''' + + expected = { + 'allReporters': None + } + + result = schema.execute(query) + assert len(result.errors) == 1 + assert str(result.errors[0]) == ( + 'Requesting 101 records on the `allReporters` connection ' + 'exceeds the `last` limit of 100 records.' + ) + assert result.data == expected + + graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False + + def test_should_query_promise_connectionfields(): from promise import Promise From 29935c2d333b0d79b872a0cb2f8abe0d79d2b713 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 12 Dec 2017 12:33:32 -0500 Subject: [PATCH 057/140] Test Last works --- graphene_django/tests/test_query.py | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 7bb8fa8..d400a37 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -695,6 +695,48 @@ def test_should_query_promise_connectionfields(): assert not result.errors assert result.data == expected +def test_should_query_promise_connectionfields_with_last(): + from promise import Promise + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + def resolve_all_reporters(self, info, **args): + return Promise.resolve([Reporter(id=1)]) + + schema = graphene.Schema(query=Query) + query = ''' + query ReporterPromiseConnectionQuery { + allReporters(last: 1) { + edges { + node { + id + } + } + } + } + ''' + + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'id': 'UmVwb3J0ZXJUeXBlOjE=' + } + }] + } + } + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + def test_should_query_dataloader_fields(): from promise import Promise From 616c549d2cd14dd19956fcd9e120a8907474f2ba Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 12 Dec 2017 12:49:02 -0500 Subject: [PATCH 058/140] Fix ConnectionField Last Test --- graphene_django/tests/test_query.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index d400a37..a785a49 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -667,7 +667,7 @@ def test_should_query_promise_connectionfields(): def resolve_all_reporters(self, info, **args): return Promise.resolve([Reporter(id=1)]) - + schema = graphene.Schema(query=Query) query = ''' query ReporterPromiseConnectionQuery { @@ -695,8 +695,14 @@ def test_should_query_promise_connectionfields(): assert not result.errors assert result.data == expected -def test_should_query_promise_connectionfields_with_last(): - from promise import Promise +def test_should_query_connectionfields_with_last(): + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) class ReporterType(DjangoObjectType): @@ -708,11 +714,11 @@ def test_should_query_promise_connectionfields_with_last(): all_reporters = DjangoConnectionField(ReporterType) def resolve_all_reporters(self, info, **args): - return Promise.resolve([Reporter(id=1)]) - + return Reporter.objects.all() + schema = graphene.Schema(query=Query) query = ''' - query ReporterPromiseConnectionQuery { + query ReporterLastQuery { allReporters(last: 1) { edges { node { From 51f794edb6d4d4ed3a3aaef874a8fe911d8820ae Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 12 Dec 2017 12:52:32 -0500 Subject: [PATCH 059/140] Test Connectionfield with custom Manager --- graphene_django/tests/models.py | 5 +++ graphene_django/tests/test_query.py | 55 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index 0c62f28..406d184 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -22,6 +22,9 @@ class Film(models.Model): reporters = models.ManyToManyField('Reporter', related_name='films') +class DoeReporterManager(models.Manager): + def get_queryset(self): + return super(DoeReporterManager, self).get_queryset().filter(last_name="Doe") class Reporter(models.Model): first_name = models.CharField(max_length=30) @@ -29,6 +32,8 @@ class Reporter(models.Model): email = models.EmailField() pets = models.ManyToManyField('self') a_choice = models.CharField(max_length=30, choices=CHOICES) + objects = models.Manager() + doe_objects = DoeReporterManager() def __str__(self): # __unicode__ on Python 2 return "%s %s" % (self.first_name, self.last_name) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index a785a49..c4c26f5 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -743,6 +743,61 @@ def test_should_query_connectionfields_with_last(): assert not result.errors assert result.data == expected +def test_should_query_connectionfields_with_manager(): + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + + r = Reporter.objects.create( + first_name='John', + last_name='NotDoe', + email='johndoe@example.com', + a_choice=1 + ) + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType, on='doe_objects') + + def resolve_all_reporters(self, info, **args): + return Reporter.objects.all() + + schema = graphene.Schema(query=Query) + query = ''' + query ReporterLastQuery { + allReporters(first: 2) { + edges { + node { + id + } + } + } + } + ''' + + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'id': 'UmVwb3J0ZXJUeXBlOjE=' + } + }] + } + } + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + def test_should_query_dataloader_fields(): from promise import Promise From 6da95d72eabb71d53824b0000563d7f7219c6e5e Mon Sep 17 00:00:00 2001 From: = <=> Date: Sat, 16 Dec 2017 19:32:01 -0500 Subject: [PATCH 060/140] Rename [html/json]_index to _priority --- graphene_django/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index 8b413f2..2ce2146 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -283,10 +283,10 @@ class GraphQLView(View): accepted_length = len(accepted) #the list will be ordered in preferred first - so we have to make #sure the most preferred gets the highest number - html_index = accepted_length - accepted.index('text/html') if 'text/html' in accepted else 0 - json_index = accepted_length - accepted.index('application/json') if 'application/json' in accepted else 0 + html_priority = accepted_length - accepted.index('text/html') if 'text/html' in accepted else 0 + json_priority = accepted_length - accepted.index('application/json') if 'application/json' in accepted else 0 - return html_index > json_index + return html_priority > json_priority @staticmethod def get_graphql_params(request, data): From 28cccb49f7772f785e6b6424e9e579ce0933717a Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 18 Dec 2017 11:40:19 -0500 Subject: [PATCH 061/140] add saces for linter --- graphene_django/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index 2ce2146..e27105d 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -281,8 +281,8 @@ class GraphQLView(View): def request_wants_html(cls, request): accepted = get_accepted_content_types(request) accepted_length = len(accepted) - #the list will be ordered in preferred first - so we have to make - #sure the most preferred gets the highest number + # the list will be ordered in preferred first - so we have to make + # sure the most preferred gets the highest number html_priority = accepted_length - accepted.index('text/html') if 'text/html' in accepted else 0 json_priority = accepted_length - accepted.index('application/json') if 'application/json' in accepted else 0 From f31db13cd00b4ba6bccdf2df0777d897ce389562 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 18 Dec 2017 12:02:04 -0500 Subject: [PATCH 062/140] Add blank line for linting --- graphene_django/management/commands/graphql_schema.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index 3a1690a..14ecf0c 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -5,6 +5,7 @@ from django.core.management.base import BaseCommand, CommandError from graphene_django.settings import graphene_settings + class CommandArguments(BaseCommand): def add_arguments(self, parser): From c952ef1a880e271303bdcc7887b293fb5130eac6 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 18 Dec 2017 12:21:49 -0500 Subject: [PATCH 063/140] Date Scalar only added in graphene 2.0.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index beee83c..e31f87b 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphene>=2.0,<3', + 'graphene>=2.0.1,<3', 'Django>=1.8.0', 'iso8601', 'singledispatch>=3.4.0.3', From fe35baa627cce4c8bca671df6e677d93bfefb96e Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 6 Dec 2017 15:10:50 -0500 Subject: [PATCH 064/140] Fix merge conflict --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index e31f87b..b4a1633 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ tests_require = [ 'pytest-django==2.9.1', ] + rest_framework_require +django_version = 'Django>=1.8.0,<2' if sys.version_info[0] < 3 else 'Django>=1.8.0' setup( name='graphene-django', version=version, @@ -59,6 +60,7 @@ setup( 'six>=1.10.0', 'graphene>=2.0.1,<3', 'Django>=1.8.0', + django_version, 'iso8601', 'singledispatch>=3.4.0.3', 'promise>=2.1', From 1352d4e02b0fe21c65ddeadc454ded07fe9523c2 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 18 Dec 2017 12:33:42 -0500 Subject: [PATCH 065/140] Fix func names --- graphene_django/converter.py | 2 +- graphene_django/rest_framework/serializer_converter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index fa771e2..dbcbbd5 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -122,7 +122,7 @@ def convert_field_to_float(field, registry=None): @convert_django_field.register(models.DateTimeField) -def convert_date_to_string(field, registry=None): +def convert_datetime_to_string(field, registry=None): return DateTime(description=field.help_text, required=not field.null) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 0c10a65..44cb01d 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -92,7 +92,7 @@ def convert_serializer_field_to_float(field): @get_graphene_type_from_serializer_field.register(serializers.DateTimeField) -def convert_serializer_field_to_date_time(field): +def convert_serializer_field_to_datetime_time(field): return graphene.types.datetime.DateTime From 76a1d43e665c629f606c5e12ccfdc98d1548bd73 Mon Sep 17 00:00:00 2001 From: yothinix Date: Sat, 30 Dec 2017 18:29:23 +0700 Subject: [PATCH 066/140] Update example query document link --- examples/cookbook-plain/README.md | 2 +- examples/cookbook/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cookbook-plain/README.md b/examples/cookbook-plain/README.md index 018c584..4075082 100644 --- a/examples/cookbook-plain/README.md +++ b/examples/cookbook-plain/README.md @@ -60,5 +60,5 @@ Now you should be ready to start the server: Now head on over to [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) and run some queries! -(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial#testing-our-graphql-schema) +(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema) for some example queries) diff --git a/examples/cookbook/README.md b/examples/cookbook/README.md index 1d3fc31..0ec906b 100644 --- a/examples/cookbook/README.md +++ b/examples/cookbook/README.md @@ -60,5 +60,5 @@ Now you should be ready to start the server: Now head on over to [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) and run some queries! -(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial#testing-our-graphql-schema) +(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema) for some example queries) From ea4ddc7f7908e6e5cff54a94ccae98450d1cc681 Mon Sep 17 00:00:00 2001 From: Martijn Faassen Date: Thu, 18 Jan 2018 16:46:08 +0100 Subject: [PATCH 067/140] Adjustments to make it work with Django 2.0.1 I tried the tutorial but ran into these two problems in Django 2.0.1. --- docs/tutorial-plain.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index eca7904..cf877eb 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -68,7 +68,8 @@ Let's get started with these models: class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField() - category = models.ForeignKey(Category, related_name='ingredients') + category = models.ForeignKey(Category, related_name='ingredients', + on_delete=models.CASCADE) def __str__(self): return self.name @@ -80,7 +81,7 @@ Add ingredients as INSTALLED_APPS: INSTALLED_APPS = [ ... # Install the ingredients app - 'ingredients', + 'cookbook.ingredients', ] Don't forget to create & run migrations: From 167d0a396442dfb6f3cc636123d7e69d4c42d220 Mon Sep 17 00:00:00 2001 From: mongkok Date: Mon, 22 Jan 2018 01:03:52 +0530 Subject: [PATCH 068/140] Allow DjangoObjectType to have default _meta --- graphene_django/types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index 684863a..54ed87b 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -45,7 +45,7 @@ class DjangoObjectType(ObjectType): @classmethod def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False, only_fields=(), exclude_fields=(), filter_fields=None, connection=None, - connection_class=None, use_connection=None, interfaces=(), **options): + connection_class=None, use_connection=None, interfaces=(), _meta=None, **options): assert is_valid_django_model(model), ( 'You need to pass a valid Django Model in {}.Meta, received "{}".' ).format(cls.__name__, model) @@ -82,7 +82,9 @@ class DjangoObjectType(ObjectType): "The connection must be a Connection. Received {}" ).format(connection.__name__) - _meta = DjangoObjectTypeOptions(cls) + if not _meta: + _meta = DjangoObjectTypeOptions(cls) + _meta.model = model _meta.registry = registry _meta.filter_fields = filter_fields From 0dddea534f8ddda63921c881cbedaa93c019f174 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Wed, 31 Jan 2018 10:46:02 +0000 Subject: [PATCH 069/140] Fix django rest framework version when testing --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ef8a3d6..1999433 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: pip install -e .[test] pip install psycopg2 # Required for Django postgres fields testing pip install django==$DJANGO_VERSION - if [ $DJANGO_VERSION = 1.8 ]; then # DRF dropped 1.8 support at 3.7.0 + if (($(echo "$DJANGO_VERSION <= 1.9" | bc -l))); then # DRF dropped 1.8 and 1.9 support at 3.7.0 pip install djangorestframework==3.6.4 fi python setup.py develop From 6a152820caeeca220f2c32bd160f89acdf0dc25e Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 23 Jan 2018 16:17:33 +0000 Subject: [PATCH 070/140] Improve ErrorType Marks some fields as required and non null, it also prevents to do useless checks on the frontend if using a typed language. --- graphene_django/rest_framework/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_django/rest_framework/types.py b/graphene_django/rest_framework/types.py index 956dc43..4c84c69 100644 --- a/graphene_django/rest_framework/types.py +++ b/graphene_django/rest_framework/types.py @@ -3,8 +3,8 @@ from graphene.types.unmountedtype import UnmountedType class ErrorType(graphene.ObjectType): - field = graphene.String() - messages = graphene.List(graphene.String) + field = graphene.String(required=True) + messages = graphene.List(graphene.NonNull(graphene.String), required=True) class DictType(UnmountedType): From bb2d24ec2783881c0fb068957ec23c789f009569 Mon Sep 17 00:00:00 2001 From: Samuel Cormier-Iijima Date: Mon, 8 Jan 2018 13:48:47 -0500 Subject: [PATCH 071/140] Convert Date/Time/DateTime form fields to appropriate Graphene types --- graphene_django/form_converter.py | 16 ++++++++++++++++ graphene_django/tests/test_form_converter.py | 12 ++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/graphene_django/form_converter.py b/graphene_django/form_converter.py index 195c8c4..fbda377 100644 --- a/graphene_django/form_converter.py +++ b/graphene_django/form_converter.py @@ -2,6 +2,7 @@ from django import forms from django.forms.fields import BaseTemporalField from graphene import ID, Boolean, Float, Int, List, String, UUID +from graphene.types.datetime import Date, DateTime, Time from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField from .utils import import_single_dispatch @@ -63,6 +64,21 @@ def convert_form_field_to_list(field): return List(ID, required=field.required) +@convert_form_field.register(forms.DateField) +def convert_form_field_to_date(field): + return Date(description=field.help_text, required=field.required) + + +@convert_form_field.register(forms.DateTimeField) +def convert_form_field_to_datetime(field): + return DateTime(description=field.help_text, required=field.required) + + +@convert_form_field.register(forms.TimeField) +def convert_form_field_to_time(field): + return Time(description=field.help_text, required=field.required) + + @convert_form_field.register(forms.ModelChoiceField) @convert_form_field.register(GlobalIDFormField) def convert_form_field_to_id(field): diff --git a/graphene_django/tests/test_form_converter.py b/graphene_django/tests/test_form_converter.py index 5a13554..fbd0176 100644 --- a/graphene_django/tests/test_form_converter.py +++ b/graphene_django/tests/test_form_converter.py @@ -23,16 +23,16 @@ def test_should_unknown_django_field_raise_exception(): assert 'Don\'t know how to convert the Django form field' in str(excinfo.value) -def test_should_date_convert_string(): - assert_conversion(forms.DateField, graphene.String) +def test_should_date_convert_date(): + assert_conversion(forms.DateField, graphene.types.datetime.Date) -def test_should_time_convert_string(): - assert_conversion(forms.TimeField, graphene.String) +def test_should_time_convert_time(): + assert_conversion(forms.TimeField, graphene.types.datetime.Time) -def test_should_date_time_convert_string(): - assert_conversion(forms.DateTimeField, graphene.String) +def test_should_date_time_convert_date_time(): + assert_conversion(forms.DateTimeField, graphene.types.datetime.DateTime) def test_should_char_convert_string(): From cf35f7c76c582786d22ef94951671cd380a55da0 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 28 Dec 2017 08:55:03 +0000 Subject: [PATCH 072/140] Upgrade graphiql and react versions --- graphene_django/templates/graphene/graphiql.html | 10 +++++----- graphene_django/views.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 949b850..1ba0613 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -16,11 +16,11 @@ add "&raw" to the end of the URL within a browser. width: 100%; } - - - - - + + + + +