From 37b90de09cc920934802a1680fa3d371ee3b1d9d Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 3 Jan 2019 11:32:18 -0500 Subject: [PATCH 01/44] Remove mirror of input fields --- graphene_django/forms/mutation.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 63ea08981..273df8bdb 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -103,7 +103,7 @@ class Meta: @classmethod def __init_subclass_with_meta__( - cls, form_class=None, only_fields=(), exclude_fields=(), **options + cls, form_class=None, mirror_input=False, only_fields=(), exclude_fields=(), **options ): if not form_class: @@ -111,7 +111,10 @@ def __init_subclass_with_meta__( form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) - output_fields = fields_for_form(form, only_fields, exclude_fields) + if mirror_input: + output_fields = fields_for_form(form, only_fields, exclude_fields) + else: + output_fields = {} _meta = DjangoFormMutationOptions(cls) _meta.form_class = form_class From e150c419d16c7159554c64521279f6ee7648dc0e Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 3 Jan 2019 11:32:36 -0500 Subject: [PATCH 02/44] Add tests to DjangoFormMutation --- graphene_django/forms/tests/test_mutation.py | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index df0ffd5ee..1f39afe78 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -139,3 +139,36 @@ class Meta: self.assertEqual(result.errors[0].messages, ["This field is required."]) self.assertIn("age", fields_w_error) self.assertEqual(result.errors[1].messages, ["This field is required."]) + + +class FormMutationTests(TestCase): + def test_default_meta_fields(self): + class MyMutation(DjangoFormMutation): + class Meta: + form_class = MyForm + self.assertNotIn("text", MyMutation._meta.fields) + + def test_mirror_meta_fields(self): + class MyMutation(DjangoFormMutation): + class Meta: + form_class = MyForm + mirror_input = True + + self.assertIn("text", MyMutation._meta.fields) + + def test_default_input_meta_fields(self): + class MyMutation(DjangoFormMutation): + class Meta: + form_class = MyForm + + self.assertIn("client_mutation_id", MyMutation.Input._meta.fields) + self.assertIn("text", MyMutation.Input._meta.fields) + + def test_exclude_fields_input_meta_fields(self): + class MyMutation(DjangoFormMutation): + class Meta: + form_class = MyForm + exclude_fields = ['text'] + + self.assertNotIn("text", MyMutation.Input._meta.fields) + self.assertIn("client_mutation_id", MyMutation.Input._meta.fields) From 5f17d6e025b88b1bc623af25fda6e1c776981cf0 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 4 Jan 2019 14:24:15 -0500 Subject: [PATCH 03/44] Add PermissionField --- graphene_django/fields.py | 37 ++++++++++++++++++++++++++++++++++++- graphene_django/utils.py | 27 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 1ecce454c..7c3cdfa69 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,5 +1,6 @@ from functools import partial +from django.core.exceptions import PermissionDenied from django.db.models.query import QuerySet from promise import Promise @@ -9,7 +10,7 @@ from graphql_relay.connection.arrayconnection import connection_from_list_slice from .settings import graphene_settings -from .utils import maybe_queryset +from .utils import maybe_queryset, has_permissions, resolve_bound_resolver class DjangoListField(Field): @@ -151,3 +152,37 @@ def get_resolver(self, parent_resolver): self.max_limit, self.enforce_first_or_last, ) + + +class DjangoPermissionField(Field): + """Class to manage permission for fields""" + + def __init__(self, type, permissions, *args, **kwargs): + """Get permissions to access a field""" + super(DjangoPermissionField, self).__init__(type, *args, **kwargs) + self.permissions = permissions + + def permission_resolver(self, parent_resolver, raise_exception, root, info, **args): + """ + Middleware resolver to check viewer's permissions + :param parent_resolver: Field resolver + :param raise_exception: If True a PermissionDenied is raised + :param root: Schema root + :param info: Schema info + :param args: Schema args + :return: Resolved field. None if the viewer does not have permission to access the field. + """ + # Get viewer from context + user = info.context.user + if has_permissions(user, self.permissions): + if parent_resolver: + # A resolver is provided in the class + return resolve_bound_resolver(parent_resolver, root, info, **args) + # Get default resolver + elif raise_exception: + raise PermissionDenied() + return None + + def get_resolver(self, parent_resolver): + """Intercept resolver to analyse permissions""" + return partial(self.permission_resolver, parent_resolver, True) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 560f60472..5adf4279d 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -5,6 +5,8 @@ # from graphene.utils import LazyList +from graphene.types.resolver import get_default_resolver +from graphene.utils.get_unbound_function import get_unbound_function class LazyList(object): @@ -81,3 +83,28 @@ def import_single_dispatch(): ) return singledispatch + + +def has_permissions(viewer, permissions): + """ + Verify that at least one permission is accomplished + :param viewer: Field's viewer + :param permissions: Field permissions + :return: True if viewer has permission. False otherwise. + """ + if not permissions: + return True + return any([viewer.has_perm(perm) for perm in permissions]) + + +def resolve_bound_resolver(resolver, root, info, **args): + """ + Resolve provided resolver + :param resolver: Explicit field resolver + :param root: Schema root + :param info: Schema info + :param args: Schema args + :return: Resolved field + """ + resolver = get_unbound_function(resolver) + return resolver(root, info, **args) From 72b849363268f4c68458f512b4ff86dc48b74092 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 4 Jan 2019 14:24:44 -0500 Subject: [PATCH 04/44] Remove import --- graphene_django/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 5adf4279d..3318e1ac5 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -5,7 +5,6 @@ # from graphene.utils import LazyList -from graphene.types.resolver import get_default_resolver from graphene.utils.get_unbound_function import get_unbound_function From d773e4232a748ee2a7c6c51e011d7d408005e391 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 4 Jan 2019 14:24:55 -0500 Subject: [PATCH 05/44] Add tests to DjangoPermissionField --- graphene_django/tests/test_fields.py | 46 ++++++++++++++++++++++++++++ graphene_django/tests/test_utils.py | 22 ++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 graphene_django/tests/test_fields.py diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py new file mode 100644 index 000000000..83f3b9c5f --- /dev/null +++ b/graphene_django/tests/test_fields.py @@ -0,0 +1,46 @@ +from unittest import TestCase +from django.core.exceptions import PermissionDenied +from graphene_django.fields import DjangoPermissionField + + +class MyInstance(object): + value = "value" + + def resolver(self): + return "resolver method" + + +class PermissionFieldTests(TestCase): + + def test_permission_field(self): + MyType = object() + field = DjangoPermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') + resolver = field.get_resolver(field.resolver) + + class Viewer(object): + def has_perm(self, perm): + return perm == 'perm2' + + class Info(object): + class Context(object): + user = Viewer() + context = Context() + + self.assertEqual(resolver(MyInstance(), Info()), MyInstance().resolver()) + + def test_permission_field_without_permission(self): + MyType = object() + field = DjangoPermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') + resolver = field.get_resolver(field.resolver) + + class Viewer(object): + def has_perm(self, perm): + return False + + class Info(object): + class Context(object): + user = Viewer() + context = Context() + + with self.assertRaises(PermissionDenied): + resolver(MyInstance(), Info()) diff --git a/graphene_django/tests/test_utils.py b/graphene_django/tests/test_utils.py index becd03188..e1068c17b 100644 --- a/graphene_django/tests/test_utils.py +++ b/graphene_django/tests/test_utils.py @@ -1,4 +1,4 @@ -from ..utils import get_model_fields +from ..utils import get_model_fields, has_permissions from .models import Film, Reporter @@ -10,3 +10,23 @@ def test_get_model_fields_no_duplication(): film_fields = get_model_fields(Film) film_name_set = set([field[0] for field in film_fields]) assert len(film_fields) == len(film_name_set) + + +def test_has_permissions(): + class Viewer(object): + @staticmethod + def has_perm(permission): + return permission + + viewer_as_perm = has_permissions(Viewer(), [False, True, False]) + assert viewer_as_perm + + +def test_viewer_without_permissions(): + class Viewer(object): + @staticmethod + def has_perm(permission): + return permission + + viewer_as_perm = has_permissions(Viewer(), [False, False, False]) + assert not viewer_as_perm From a314511ea44e59cbe9170bccfc140230aeb82d5a Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 4 Jan 2019 14:25:08 -0500 Subject: [PATCH 06/44] Change PermissionField to DjangoPermissionField --- graphene_django/tests/test_fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 83f3b9c5f..8f6129859 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -10,7 +10,7 @@ def resolver(self): return "resolver method" -class PermissionFieldTests(TestCase): +class DjangoPermissionFieldTests(TestCase): def test_permission_field(self): MyType = object() From 9e62c71d721f22d0a2c0a596da89e80200355813 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 4 Jan 2019 14:59:32 -0500 Subject: [PATCH 07/44] Add PermissionObjectType --- graphene_django/types.py | 88 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/graphene_django/types.py b/graphene_django/types.py index aa8b5a30c..d383f0278 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -133,3 +133,91 @@ def get_node(cls, info, id): return cls._meta.model.objects.get(pk=id) except cls._meta.model.DoesNotExist: return None + + +class DjangoPermissionObjectType(DjangoObjectType): + """ + DjangoObjectType inheritance to handle field authorization + Accepts field's permissions description as: + + class Meta: + + field_to_permission = { + 'restricted_field': ('permission1', 'permission2') + } + + permission_to_field = { + 'permission': ('restricted_field_1', 'restricted_field_2') + } + + At least one of the permissions must be accomplished in order to resolve the field. + """ + + class Meta(object): + """Meta Class""" + abstract = True + + @classmethod + def __init_subclass_with_meta__(cls, field_to_permission=None, permission_to_field=None, model=None, registry=None, + **options): # pylint: disable=W0221 + + cls._field_permissions = field_to_permission if field_to_permission else {} + + if permission_to_field: + cls._get_permission_to_fields(permission_to_field) + + for field_name, field_permissions in cls._field_permissions.items(): + attr = 'resolve_{}'.format(field_name) + resolver = getattr(cls, attr, None) + + if not hasattr(field_permissions, '__iter__'): + field_permissions = tuple(field_permissions) + + setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) + + if cls._field_permissions: + cls._set_as_nullable(model, registry) + + super(DjangoPermissionObjectType, cls).__init_subclass_with_meta__(model=model, registry=registry, **options) + + # pylint: disable=W0212 + @classmethod + def _set_as_nullable(cls, model, registry): + """Set restricted fields as nullable""" + django_fields = yank_fields_from_attrs( + construct_fields(model, registry, cls._field_permissions.keys(), ()), + _as=Field, + ) + for name, field in django_fields.items(): + if isinstance(field._type, NonNull): + field._type = field._type._of_type # pylint: disable=W0212 + setattr(cls, name, field) + + @classmethod + def _get_permission_to_fields(cls, permission_to_field): + """ + Accepts a dictionary like + { + permission: [fields] + } + :return: Mapping of fields to permissions + """ + for permission, fields in permission_to_field.items(): + for field in fields: + cls._set_permission_to_field(field, (permission,)) + + @classmethod + def _set_permission_to_field(cls, field, permissions): + """Add list permissions to field""" + cls._field_permissions[field] = cls._field_permissions.get(field, tuple()) + permissions + + @classmethod + def set_auth_resolver(cls, name, permissions, resolver=None): + """ + Set middleware resolver to handle field permissions + :param name: Field name + :param permissions: List of permissions + :param resolver: Field resolver + :return: Middleware resolver to check permissions + """ + return partial(auth_resolver, resolver, name, permissions, None, False) From 4c6e7209c37a44d043c253d6fc0d7b5af0f94472 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 4 Jan 2019 15:02:50 -0500 Subject: [PATCH 08/44] Add viewer management --- graphene_django/fields.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 7c3cdfa69..1926f8af8 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -162,6 +162,10 @@ def __init__(self, type, permissions, *args, **kwargs): super(DjangoPermissionField, self).__init__(type, *args, **kwargs) self.permissions = permissions + def get_viewer(self, root, info, **args): + """Get viewer to verify permissions""" + return info.context.user + def permission_resolver(self, parent_resolver, raise_exception, root, info, **args): """ Middleware resolver to check viewer's permissions @@ -173,7 +177,7 @@ def permission_resolver(self, parent_resolver, raise_exception, root, info, **ar :return: Resolved field. None if the viewer does not have permission to access the field. """ # Get viewer from context - user = info.context.user + user = self.get_viewer(root, info, **args) if has_permissions(user, self.permissions): if parent_resolver: # A resolver is provided in the class From d1391db45aab35ce2c3144b7511f529ee0c9f09d Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Sun, 6 Jan 2019 11:13:44 -0500 Subject: [PATCH 09/44] Add permission type --- graphene_django/types.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index d383f0278..4f0755aa9 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -1,7 +1,8 @@ from collections import OrderedDict +from functools import partial from django.utils.functional import SimpleLazyObject -from graphene import Field +from graphene import Field, NonNull from graphene.relay import Connection, Node from graphene.types.objecttype import ObjectType, ObjectTypeOptions from graphene.types.utils import yank_fields_from_attrs @@ -159,7 +160,8 @@ class Meta(object): @classmethod def __init_subclass_with_meta__(cls, field_to_permission=None, permission_to_field=None, model=None, registry=None, - **options): # pylint: disable=W0221 + **options): + super(DjangoPermissionObjectType, cls).__init_subclass_with_meta__(model=model, registry=registry, **options) cls._field_permissions = field_to_permission if field_to_permission else {} @@ -173,14 +175,11 @@ def __init_subclass_with_meta__(cls, field_to_permission=None, permission_to_fie if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) - setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) + cls.set_auth_resolver(field_name, field_permissions, cls._meta.fields[field_name], resolver) if cls._field_permissions: cls._set_as_nullable(model, registry) - super(DjangoPermissionObjectType, cls).__init_subclass_with_meta__(model=model, registry=registry, **options) - - # pylint: disable=W0212 @classmethod def _set_as_nullable(cls, model, registry): """Set restricted fields as nullable""" @@ -190,7 +189,7 @@ def _set_as_nullable(cls, model, registry): ) for name, field in django_fields.items(): if isinstance(field._type, NonNull): - field._type = field._type._of_type # pylint: disable=W0212 + field._type = field._type._of_type setattr(cls, name, field) @classmethod @@ -212,12 +211,13 @@ def _set_permission_to_field(cls, field, permissions): cls._field_permissions[field] = cls._field_permissions.get(field, tuple()) + permissions @classmethod - def set_auth_resolver(cls, name, permissions, resolver=None): + def set_auth_resolver(cls, name, permissions, field, resolver=None): """ Set middleware resolver to handle field permissions :param name: Field name :param permissions: List of permissions + :param field: Meta's field :param resolver: Field resolver :return: Middleware resolver to check permissions """ - return partial(auth_resolver, resolver, name, permissions, None, False) + field.resolver = partial(auth_resolver, field.resolver or resolver, name, permissions, None, False) From beba1306577f35176522f09c8b394e4efc77228d Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 7 Jan 2019 07:44:20 -0500 Subject: [PATCH 10/44] Refactoring permission field --- graphene_django/fields.py | 29 ++--------------------------- graphene_django/utils.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 1926f8af8..41132e209 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -10,7 +10,7 @@ from graphql_relay.connection.arrayconnection import connection_from_list_slice from .settings import graphene_settings -from .utils import maybe_queryset, has_permissions, resolve_bound_resolver +from .utils import maybe_queryset, auth_resolver class DjangoListField(Field): @@ -162,31 +162,6 @@ def __init__(self, type, permissions, *args, **kwargs): super(DjangoPermissionField, self).__init__(type, *args, **kwargs) self.permissions = permissions - def get_viewer(self, root, info, **args): - """Get viewer to verify permissions""" - return info.context.user - - def permission_resolver(self, parent_resolver, raise_exception, root, info, **args): - """ - Middleware resolver to check viewer's permissions - :param parent_resolver: Field resolver - :param raise_exception: If True a PermissionDenied is raised - :param root: Schema root - :param info: Schema info - :param args: Schema args - :return: Resolved field. None if the viewer does not have permission to access the field. - """ - # Get viewer from context - user = self.get_viewer(root, info, **args) - if has_permissions(user, self.permissions): - if parent_resolver: - # A resolver is provided in the class - return resolve_bound_resolver(parent_resolver, root, info, **args) - # Get default resolver - elif raise_exception: - raise PermissionDenied() - return None - def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(self.permission_resolver, parent_resolver, True) + return partial(auth_resolver, self.resolver or parent_resolver, self.permissions, True) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 3318e1ac5..55581c4ee 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -1,5 +1,6 @@ import inspect +from django.core.exceptions import PermissionDenied from django.db import models from django.db.models.manager import Manager @@ -107,3 +108,25 @@ def resolve_bound_resolver(resolver, root, info, **args): """ resolver = get_unbound_function(resolver) return resolver(root, info, **args) + + +def auth_resolver(parent_resolver, permissions, raise_exception, root, info, **args): + """ + Middleware resolver to check viewer's permissions + :param parent_resolver: Field resolver + :param permissions: Field permissions + :param raise_exception: If True a PermissionDenied is raised + :param root: Schema root + :param info: Schema info + :param args: Schema args + :return: Resolved field. None if the viewer does not have permission to access the field. + """ + # Get viewer from context + user = info.context.user + if has_permissions(user, permissions): + if parent_resolver: + # A resolver is provided in the class + return resolve_bound_resolver(parent_resolver, root, info, **args) + elif raise_exception: + raise PermissionDenied() + return None From 0b34a215adb28fc750965c8e6dfc95119b656333 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 7 Jan 2019 07:46:00 -0500 Subject: [PATCH 11/44] Set resolver as class attr --- graphene_django/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index 4f0755aa9..b78c7ea2f 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -175,7 +175,7 @@ def __init_subclass_with_meta__(cls, field_to_permission=None, permission_to_fie if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) - cls.set_auth_resolver(field_name, field_permissions, cls._meta.fields[field_name], resolver) + setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) if cls._field_permissions: cls._set_as_nullable(model, registry) From 106d3e063dfb1deb7b11ea160e7171411713777c Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 7 Jan 2019 08:25:19 -0500 Subject: [PATCH 12/44] Add test to permission type --- graphene_django/fields.py | 2 +- graphene_django/tests/test_types.py | 102 +++++++++++++++++++++++++++- graphene_django/types.py | 10 +-- graphene_django/utils.py | 21 +++++- 4 files changed, 126 insertions(+), 9 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 41132e209..ae18e9860 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -164,4 +164,4 @@ def __init__(self, type, permissions, *args, **kwargs): def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(auth_resolver, self.resolver or parent_resolver, self.permissions, True) + return partial(auth_resolver, self.resolver or parent_resolver, self.permissions, None, None, True) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 8a8643b9b..8ad1465fe 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -1,10 +1,10 @@ from mock import patch -from graphene import Interface, ObjectType, Schema, Connection, String +from graphene import Interface, ObjectType, Schema, Connection, String, Field from graphene.relay import Node from .. import registry -from ..types import DjangoObjectType, DjangoObjectTypeOptions +from ..types import DjangoObjectType, DjangoObjectTypeOptions, DjangoPermissionObjectType from .models import Article as ArticleModel from .models import Reporter as ReporterModel @@ -224,3 +224,101 @@ class Meta: fields = list(Reporter._meta.fields.keys()) assert "email" not in fields + + +def extra_field_resolver(root, info, **kwargs): + return 'extra field' + + +class PermissionArticle(DjangoPermissionObjectType): + """Basic Type to test""" + + class Meta(object): + """Meta Class""" + field_to_permission = { + 'headline': ('content_type.permission1',), + 'pub_date': ('content_type.permission2',) + } + permission_to_field = { + 'content_type.permission3': ('headline', 'reporter', 'extra_field',) + } + model = ArticleModel + + extra_field = Field(String, resolver=extra_field_resolver) + + def resolve_headline(self, info, **kwargs): + return 'headline' + + +def test_django_permissions(): + expected = { + 'headline': ('content_type.permission1', 'content_type.permission3'), + 'pub_date': ('content_type.permission2',), + 'reporter': ('content_type.permission3',), + 'extra_field': ('content_type.permission3',), + } + assert PermissionArticle._field_permissions == expected + + +def test_permission_resolver(): + MyType = object() + + class Viewer(object): + def has_perm(self, perm): + return perm == 'content_type.permission3' + + class Info(object): + class Context(object): + user = Viewer() + context = Context() + + resolved = PermissionArticle.resolve_headline(MyType, Info()) + assert resolved == 'headline' + + +def test_resolver_without_permission(): + MyType = object() + + class Viewer(object): + def has_perm(self, perm): + return False + + class Info(object): + class Context(object): + user = Viewer() + context = Context() + + resolved = PermissionArticle.resolve_headline(MyType, Info()) + assert resolved is None + + +def test_permission_resolver_to_field(): + MyType = object() + + class Viewer(object): + def has_perm(self, perm): + return perm == 'content_type.permission3' + + class Info(object): + class Context(object): + user = Viewer() + context = Context() + + resolved = PermissionArticle.resolve_extra_field(MyType, Info()) + assert resolved == 'extra field' + + +def test_resolver_to_field_without_permission(): + MyType = object() + + class Viewer(object): + def has_perm(self, perm): + return perm != 'content_type.permission3' + + class Info(object): + class Context(object): + user = Viewer() + context = Context() + + resolved = PermissionArticle.resolve_extra_field(MyType, Info()) + assert resolved is None diff --git a/graphene_django/types.py b/graphene_django/types.py index b78c7ea2f..369930891 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -9,7 +9,7 @@ from .converter import convert_django_field_with_choices from .registry import Registry, get_global_registry -from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model +from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model, auth_resolver def construct_fields(model, registry, only_fields, exclude_fields): @@ -170,7 +170,7 @@ def __init_subclass_with_meta__(cls, field_to_permission=None, permission_to_fie for field_name, field_permissions in cls._field_permissions.items(): attr = 'resolve_{}'.format(field_name) - resolver = getattr(cls, attr, None) + resolver = getattr(cls._meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) @@ -188,7 +188,7 @@ def _set_as_nullable(cls, model, registry): _as=Field, ) for name, field in django_fields.items(): - if isinstance(field._type, NonNull): + if hasattr(field, '_type') and isinstance(field._type, NonNull): field._type = field._type._of_type setattr(cls, name, field) @@ -211,7 +211,7 @@ def _set_permission_to_field(cls, field, permissions): cls._field_permissions[field] = cls._field_permissions.get(field, tuple()) + permissions @classmethod - def set_auth_resolver(cls, name, permissions, field, resolver=None): + def set_auth_resolver(cls, name, permissions, resolver=None): """ Set middleware resolver to handle field permissions :param name: Field name @@ -220,4 +220,4 @@ def set_auth_resolver(cls, name, permissions, field, resolver=None): :param resolver: Field resolver :return: Middleware resolver to check permissions """ - field.resolver = partial(auth_resolver, field.resolver or resolver, name, permissions, None, False) + return partial(auth_resolver, resolver, permissions, name, None, False) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 55581c4ee..bbc984b7b 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -6,6 +6,7 @@ # from graphene.utils import LazyList +from graphene.types.resolver import get_default_resolver from graphene.utils.get_unbound_function import get_unbound_function @@ -110,11 +111,27 @@ def resolve_bound_resolver(resolver, root, info, **args): return resolver(root, info, **args) -def auth_resolver(parent_resolver, permissions, raise_exception, root, info, **args): +def resolve_default_resolver(attname, default_value, root, info, **args): + """ + Resolve field with default resolver + :param attname: Field name + :param default_value: Field default value + :param root: Schema root + :param info: Schema info + :param args: Schema args + :return: Resolved field + """ + resolver = get_default_resolver() + return resolver(attname, default_value, root, info, **args) + + +def auth_resolver(parent_resolver, permissions, attname, default_value, raise_exception, root, info, **args): """ Middleware resolver to check viewer's permissions :param parent_resolver: Field resolver :param permissions: Field permissions + :param attname: Field name + :param default_value: Default value to field if no resolver is provided :param raise_exception: If True a PermissionDenied is raised :param root: Schema root :param info: Schema info @@ -127,6 +144,8 @@ def auth_resolver(parent_resolver, permissions, raise_exception, root, info, **a if parent_resolver: # A resolver is provided in the class return resolve_bound_resolver(parent_resolver, root, info, **args) + # Get default resolver + return resolve_default_resolver(attname, default_value, root, info, **args) elif raise_exception: raise PermissionDenied() return None From 9e007b962e2480e1619b5863ee52f4cd498f80c2 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 7 Jan 2019 09:03:37 -0500 Subject: [PATCH 13/44] Add converter to graphene_django --- graphene_django/forms/converter.py | 25 ++++++++++++++++++++++++- graphene_django/forms/mutation.py | 6 +++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 87180b291..07988f423 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -1,8 +1,10 @@ from django import forms from django.core.exceptions import ImproperlyConfigured -from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time +from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time, Enum +from graphene.utils.str_converters import to_camel_case +from graphene_django.converter import get_choices from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField from ..utils import import_single_dispatch @@ -82,3 +84,24 @@ def convert_form_field_to_time(field): @convert_form_field.register(GlobalIDFormField) def convert_form_field_to_id(field): return ID(required=field.required) + + +@convert_form_field.register(forms.TypedChoiceField) +def convert_form_to_enum(field, name): + choices = getattr(field, 'choices', None) + name = to_camel_case(name) + choices = list(get_choices(choices)) + named_choices = [(c[0], c[1]) for c in choices] + named_choices_descriptions = {c[0]: c[2] for c in choices} + + class EnumWithDescriptionsType(object): + """Enum type definition""" + + @property + def description(self): + """Return field description""" + + return named_choices_descriptions[self.name] + + enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType) + return enum(description=field.help_text, required=field.required) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 63ea08981..c39b3c3db 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -30,7 +30,11 @@ def fields_for_form(form, only_fields, exclude_fields): if is_not_in_only or is_excluded: continue - fields[name] = convert_form_field(field) + choices = getattr(field, 'choices', None) + if choices: + fields[name] = convert_form_field(field, field.label or name) + else: + fields[name] = convert_form_field(field) return fields From b3d418253be972285b8cdadd041e08f084cfa915 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 7 Jan 2019 09:03:49 -0500 Subject: [PATCH 14/44] Add test to converter --- graphene_django/forms/tests/test_converter.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/graphene_django/forms/tests/test_converter.py b/graphene_django/forms/tests/test_converter.py index 955b95290..f7b6f01f4 100644 --- a/graphene_django/forms/tests/test_converter.py +++ b/graphene_django/forms/tests/test_converter.py @@ -14,6 +14,7 @@ DateTime, Date, Time, + Enum, ) from ..converter import convert_form_field @@ -112,3 +113,9 @@ def test_should_manytoone_convert_connectionorlist(): field = forms.ModelChoiceField(queryset=None) graphene_type = convert_form_field(field) assert isinstance(graphene_type, ID) + + +def test_should_typed_choice_convert_enum(): + field = forms.TypedChoiceField(choices=(('A', 'Choice A'), ('B', 'Choice B'))) + graphene_type = convert_form_field(field, 'field') + assert isinstance(graphene_type, Enum) From b8eef4731aae3bc16296bb05dfaecf0255391914 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 15 Feb 2019 15:19:04 -0500 Subject: [PATCH 15/44] Set default resolver --- graphene_django/fields.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 41132e209..f598bf9f0 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -156,6 +156,7 @@ def get_resolver(self, parent_resolver): class DjangoPermissionField(Field): """Class to manage permission for fields""" + AUTH_RESOLVER = auth_resolver def __init__(self, type, permissions, *args, **kwargs): """Get permissions to access a field""" @@ -164,4 +165,4 @@ def __init__(self, type, permissions, *args, **kwargs): def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(auth_resolver, self.resolver or parent_resolver, self.permissions, True) + return partial(self.AUTH_RESOLVER, self.resolver or parent_resolver, self.permissions, True) From 813b31e965979ffc7b2f34968ea3c29fa2fb44f0 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 15 Feb 2019 16:19:15 -0500 Subject: [PATCH 16/44] Get unbound function to resolver --- graphene_django/fields.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index f598bf9f0..384a6b6bb 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -2,6 +2,7 @@ from django.core.exceptions import PermissionDenied from django.db.models.query import QuerySet +from graphene.utils.get_unbound_function import get_unbound_function from promise import Promise @@ -165,4 +166,5 @@ def __init__(self, type, permissions, *args, **kwargs): def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(self.AUTH_RESOLVER, self.resolver or parent_resolver, self.permissions, True) + return partial(get_unbound_function(self.AUTH_RESOLVER), self.resolver or parent_resolver, self.permissions, + True) From 7b86824afa4fcaced6e80e07e4d6f01c32b890f5 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 18 Feb 2019 15:00:35 -0500 Subject: [PATCH 17/44] Mixing DjangoObjectType and DjangoPermissionObjectType --- graphene_django/fields.py | 3 +- graphene_django/tests/test_types.py | 6 +- graphene_django/types.py | 127 +++++++++++++--------------- 3 files changed, 63 insertions(+), 73 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index cff38100e..23ddd9f60 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -165,4 +165,5 @@ def __init__(self, type, permissions, *args, **kwargs): def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(get_unbound_function(self.AUTH_RESOLVER), self.resolver or parent_resolver, self.permissions,None, None, True) + return partial(get_unbound_function(self.AUTH_RESOLVER), self.resolver or parent_resolver, self.permissions, + None, None, True) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 8ad1465fe..27327816a 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -4,7 +4,7 @@ from graphene.relay import Node from .. import registry -from ..types import DjangoObjectType, DjangoObjectTypeOptions, DjangoPermissionObjectType +from ..types import DjangoObjectType, DjangoObjectTypeOptions from .models import Article as ArticleModel from .models import Reporter as ReporterModel @@ -230,7 +230,7 @@ def extra_field_resolver(root, info, **kwargs): return 'extra field' -class PermissionArticle(DjangoPermissionObjectType): +class PermissionArticle(DjangoObjectType): """Basic Type to test""" class Meta(object): @@ -257,7 +257,7 @@ def test_django_permissions(): 'reporter': ('content_type.permission3',), 'extra_field': ('content_type.permission3',), } - assert PermissionArticle._field_permissions == expected + assert PermissionArticle._meta.field_permissions == expected def test_permission_resolver(): diff --git a/graphene_django/types.py b/graphene_django/types.py index 369930891..17ba330a6 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -42,6 +42,22 @@ class DjangoObjectTypeOptions(ObjectTypeOptions): class DjangoObjectType(ObjectType): + """ + DjangoObjectType inheritance to handle field authorization + Accepts field's permissions description as: + + class Meta: + + field_to_permission = { + 'restricted_field': ('permission1', 'permission2') + } + + permission_to_field = { + 'permission': ('restricted_field_1', 'restricted_field_2') + } + + At least one of the permissions must be accomplished in order to resolve the field. + """ @classmethod def __init_subclass_with_meta__( cls, @@ -55,6 +71,8 @@ def __init_subclass_with_meta__( connection_class=None, use_connection=None, interfaces=(), + field_to_permission=None, + permission_to_field=None, _meta=None, **options ): @@ -109,82 +127,33 @@ def __init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options ) - if not skip_registry: - registry.register(cls) - - def resolve_id(self, info): - return self.pk - - @classmethod - def is_type_of(cls, root, info): - if isinstance(root, SimpleLazyObject): - root._setup() - root = root._wrapped - if isinstance(root, cls): - return True - if not is_valid_django_model(type(root)): - raise Exception(('Received incompatible instance "{}".').format(root)) - - model = root._meta.model._meta.concrete_model - return model == cls._meta.model - - @classmethod - def get_node(cls, info, id): - try: - return cls._meta.model.objects.get(pk=id) - except cls._meta.model.DoesNotExist: - return None - - -class DjangoPermissionObjectType(DjangoObjectType): - """ - DjangoObjectType inheritance to handle field authorization - Accepts field's permissions description as: - - class Meta: - - field_to_permission = { - 'restricted_field': ('permission1', 'permission2') - } - - permission_to_field = { - 'permission': ('restricted_field_1', 'restricted_field_2') - } - - At least one of the permissions must be accomplished in order to resolve the field. - """ - - class Meta(object): - """Meta Class""" - abstract = True - - @classmethod - def __init_subclass_with_meta__(cls, field_to_permission=None, permission_to_field=None, model=None, registry=None, - **options): - super(DjangoPermissionObjectType, cls).__init_subclass_with_meta__(model=model, registry=registry, **options) - - cls._field_permissions = field_to_permission if field_to_permission else {} + permissions = field_to_permission if field_to_permission else {} if permission_to_field: - cls._get_permission_to_fields(permission_to_field) + permissions.update(cls.__get_permission_to_fields__(permission_to_field)) + + cls.field_permissions = permissions - for field_name, field_permissions in cls._field_permissions.items(): + for field_name, field_permissions in permissions.items(): attr = 'resolve_{}'.format(field_name) - resolver = getattr(cls._meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) + resolver = getattr(_meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) - if cls._field_permissions: - cls._set_as_nullable(model, registry) + if permissions: + cls.__set_as_nullable__(model, registry) + + if not skip_registry: + registry.register(cls) @classmethod - def _set_as_nullable(cls, model, registry): + def __set_as_nullable__(cls, model, registry): """Set restricted fields as nullable""" django_fields = yank_fields_from_attrs( - construct_fields(model, registry, cls._field_permissions.keys(), ()), + construct_fields(model, registry, cls.field_permissions.keys(), ()), _as=Field, ) for name, field in django_fields.items(): @@ -193,7 +162,7 @@ def _set_as_nullable(cls, model, registry): setattr(cls, name, field) @classmethod - def _get_permission_to_fields(cls, permission_to_field): + def __get_permission_to_fields__(cls, permission_to_field): """ Accepts a dictionary like { @@ -201,14 +170,11 @@ def _get_permission_to_fields(cls, permission_to_field): } :return: Mapping of fields to permissions """ + permissions = {} for permission, fields in permission_to_field.items(): for field in fields: - cls._set_permission_to_field(field, (permission,)) - - @classmethod - def _set_permission_to_field(cls, field, permissions): - """Add list permissions to field""" - cls._field_permissions[field] = cls._field_permissions.get(field, tuple()) + permissions + permissions[field] = permissions.get(field, ()) + (permission,) + return permissions @classmethod def set_auth_resolver(cls, name, permissions, resolver=None): @@ -221,3 +187,26 @@ def set_auth_resolver(cls, name, permissions, resolver=None): :return: Middleware resolver to check permissions """ return partial(auth_resolver, resolver, permissions, name, None, False) + + def resolve_id(self, info): + return self.pk + + @classmethod + def is_type_of(cls, root, info): + if isinstance(root, SimpleLazyObject): + root._setup() + root = root._wrapped + if isinstance(root, cls): + return True + if not is_valid_django_model(type(root)): + raise Exception(('Received incompatible instance "{}".').format(root)) + + model = root._meta.model._meta.concrete_model + return model == cls._meta.model + + @classmethod + def get_node(cls, info, id): + try: + return cls._meta.model.objects.get(pk=id) + except cls._meta.model.DoesNotExist: + return None From 7d643f3b717d8326b35821be8d856f8dabe7dd88 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 18 Feb 2019 15:15:13 -0500 Subject: [PATCH 18/44] Get bound --- graphene_django/fields.py | 3 +-- graphene_django/tests/test_fields.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 384a6b6bb..ddffd07f0 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,8 +1,7 @@ from functools import partial -from django.core.exceptions import PermissionDenied from django.db.models.query import QuerySet -from graphene.utils.get_unbound_function import get_unbound_function +from django.utils.six import get_unbound_function from promise import Promise diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 8f6129859..bf2bbd7fa 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -15,7 +15,7 @@ class DjangoPermissionFieldTests(TestCase): def test_permission_field(self): MyType = object() field = DjangoPermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') - resolver = field.get_resolver(field.resolver) + resolver = field.get_resolver(None) class Viewer(object): def has_perm(self, perm): From 715fbbe1dd1985f625b3bc357e5b80c7fea6cbb1 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Tue, 19 Feb 2019 13:02:06 -0500 Subject: [PATCH 19/44] Update permission to type --- graphene_django/tests/test_types.py | 2 +- graphene_django/types.py | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 27327816a..c9245aedc 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -257,7 +257,7 @@ def test_django_permissions(): 'reporter': ('content_type.permission3',), 'extra_field': ('content_type.permission3',), } - assert PermissionArticle._meta.field_permissions == expected + assert PermissionArticle.field_permissions == expected def test_permission_resolver(): diff --git a/graphene_django/types.py b/graphene_django/types.py index 17ba330a6..b969ae438 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -127,28 +127,35 @@ def __init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options ) - permissions = field_to_permission if field_to_permission else {} + if cls.field_permissions: + cls.__set_as_nullable__(cls._meta.model, cls._meta.registry) + if not skip_registry: + registry.register(cls) + + @classmethod + def __set_permissions__(cls, field_to_permission, permission_to_field): + """Combines permissions from meta""" + permissions = field_to_permission if field_to_permission else {} if permission_to_field: - permissions.update(cls.__get_permission_to_fields__(permission_to_field)) + perm_to_field = cls.__get_permission_to_fields__(permission_to_field) + for field, perms in perm_to_field.items(): + if field in permissions: + permissions[field] += perms + else: + permissions[field] = perms cls.field_permissions = permissions for field_name, field_permissions in permissions.items(): attr = 'resolve_{}'.format(field_name) - resolver = getattr(_meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) + resolver = getattr(cls._meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) - if permissions: - cls.__set_as_nullable__(model, registry) - - if not skip_registry: - registry.register(cls) - @classmethod def __set_as_nullable__(cls, model, registry): """Set restricted fields as nullable""" From 142f5141ea51fd0277da789e94f86c6e907b2b1b Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Tue, 19 Feb 2019 13:31:19 -0500 Subject: [PATCH 20/44] Add permission to class --- graphene_django/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphene_django/types.py b/graphene_django/types.py index b969ae438..adf0eb67c 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -127,6 +127,8 @@ def __init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options ) + cls.__set_permissions__(field_to_permission, permission_to_field) + if cls.field_permissions: cls.__set_as_nullable__(cls._meta.model, cls._meta.registry) From 114202b970bbf6081cee5ed470162dfa952f1dcd Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Wed, 27 Feb 2019 16:11:14 -0500 Subject: [PATCH 21/44] Force push --- graphene_django/fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 1ecce454c..e6703168b 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -151,3 +151,4 @@ def get_resolver(self, parent_resolver): self.max_limit, self.enforce_first_or_last, ) + From 6e087c4bd4f50c8ccae9cdbcaecdaea3d9d0f1fb Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Wed, 27 Feb 2019 16:11:25 -0500 Subject: [PATCH 22/44] Remove change --- graphene_django/fields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index e6703168b..1ecce454c 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -151,4 +151,3 @@ def get_resolver(self, parent_resolver): self.max_limit, self.enforce_first_or_last, ) - From f357d7ebfbe8c5ca4b8818e47179b6edd4ad03cf Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 10:48:30 -0500 Subject: [PATCH 23/44] Remove field resolver --- graphene_django/utils.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index bbc984b7b..2d707421f 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -6,8 +6,8 @@ # from graphene.utils import LazyList +from django.utils.six import get_unbound_function from graphene.types.resolver import get_default_resolver -from graphene.utils.get_unbound_function import get_unbound_function class LazyList(object): @@ -111,20 +111,6 @@ def resolve_bound_resolver(resolver, root, info, **args): return resolver(root, info, **args) -def resolve_default_resolver(attname, default_value, root, info, **args): - """ - Resolve field with default resolver - :param attname: Field name - :param default_value: Field default value - :param root: Schema root - :param info: Schema info - :param args: Schema args - :return: Resolved field - """ - resolver = get_default_resolver() - return resolver(attname, default_value, root, info, **args) - - def auth_resolver(parent_resolver, permissions, attname, default_value, raise_exception, root, info, **args): """ Middleware resolver to check viewer's permissions @@ -145,7 +131,7 @@ def auth_resolver(parent_resolver, permissions, attname, default_value, raise_ex # A resolver is provided in the class return resolve_bound_resolver(parent_resolver, root, info, **args) # Get default resolver - return resolve_default_resolver(attname, default_value, root, info, **args) + return get_default_resolver(attname, default_value, root, info, **args) elif raise_exception: raise PermissionDenied() return None From ad7497de1030b5a2356d980da11159ce7a2f6064 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 14:58:20 -0500 Subject: [PATCH 24/44] Refactor PermissionField --- graphene_django/fields.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 384a6b6bb..0f1733dc4 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,8 +1,7 @@ from functools import partial -from django.core.exceptions import PermissionDenied from django.db.models.query import QuerySet -from graphene.utils.get_unbound_function import get_unbound_function +from django.utils.six import get_unbound_function from promise import Promise @@ -155,16 +154,16 @@ def get_resolver(self, parent_resolver): ) -class DjangoPermissionField(Field): +class DjangoField(Field): """Class to manage permission for fields""" - AUTH_RESOLVER = auth_resolver - def __init__(self, type, permissions, *args, **kwargs): + def __init__(self, type, permissions, permission_resolver=auth_resolver, *args, **kwargs): """Get permissions to access a field""" - super(DjangoPermissionField, self).__init__(type, *args, **kwargs) + super(DjangoField, self).__init__(type, *args, **kwargs) self.permissions = permissions + self.permissions_resolver = permission_resolver def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(get_unbound_function(self.AUTH_RESOLVER), self.resolver or parent_resolver, self.permissions, - True) + return partial(get_unbound_function(self.permission_resolver), self.resolver or parent_resolver, + self.permissions, raise_exception=True) From d14af48866e2384d0a9c2db38da5042173db20bd Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 15:05:39 -0500 Subject: [PATCH 25/44] Change naming --- graphene_django/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 0f1733dc4..113154465 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -157,13 +157,13 @@ def get_resolver(self, parent_resolver): class DjangoField(Field): """Class to manage permission for fields""" - def __init__(self, type, permissions, permission_resolver=auth_resolver, *args, **kwargs): + def __init__(self, type, permissions, permissions_resolver=auth_resolver, *args, **kwargs): """Get permissions to access a field""" super(DjangoField, self).__init__(type, *args, **kwargs) self.permissions = permissions - self.permissions_resolver = permission_resolver + self.permissions_resolver = permissions_resolver def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(get_unbound_function(self.permission_resolver), self.resolver or parent_resolver, + return partial(get_unbound_function(self.permissions_resolver), self.resolver or parent_resolver, self.permissions, raise_exception=True) From eca43192b0f070186ed3bad534bb425c26077f1c Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 15:10:22 -0500 Subject: [PATCH 26/44] Change get_unbound_function importation --- graphene_django/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 113154465..ec3438ef9 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,12 +1,12 @@ from functools import partial from django.db.models.query import QuerySet -from django.utils.six import get_unbound_function from promise import Promise from graphene.types import Field, List from graphene.relay import ConnectionField, PageInfo +from graphene.utils.get_unbound_function import get_unbound_function from graphql_relay.connection.arrayconnection import connection_from_list_slice from .settings import graphene_settings From c518ddbf4bf8a8921fd3e69bee66eb7b8536a347 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 15:15:46 -0500 Subject: [PATCH 27/44] Set raise_exception as arg --- graphene_django/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index ec3438ef9..1bfd33a61 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -166,4 +166,4 @@ def __init__(self, type, permissions, permissions_resolver=auth_resolver, *args, def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" return partial(get_unbound_function(self.permissions_resolver), self.resolver or parent_resolver, - self.permissions, raise_exception=True) + self.permissions, True) From 10d28ba5d0dc7ad7665c065081d75c30abf54303 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 15:30:36 -0500 Subject: [PATCH 28/44] Refactor DjangoObjectType --- graphene_django/types.py | 68 ++++++++++++++++++++++------------------ graphene_django/utils.py | 2 +- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index adf0eb67c..80d4ba53b 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -33,6 +33,17 @@ def construct_fields(model, registry, only_fields, exclude_fields): return fields +def get_auth_resolver(cls, name, permissions, resolver=None): + """ + Get middleware resolver to handle field permissions + :param name: Field name + :param permissions: List of permissions + :param resolver: Field resolver + :return: Middleware resolver to check permissions + """ + return partial(auth_resolver, resolver, permissions, name, None, False) + + class DjangoObjectTypeOptions(ObjectTypeOptions): model = None # type: Model registry = None # type: Registry @@ -127,16 +138,17 @@ def __init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options ) - cls.__set_permissions__(field_to_permission, permission_to_field) + cls.field_permissions = cls.__get_field_permissions__(field_to_permission, permission_to_field) if cls.field_permissions: + cls.__set_permissions_resolvers__(cls.field_permissions) cls.__set_as_nullable__(cls._meta.model, cls._meta.registry) if not skip_registry: registry.register(cls) @classmethod - def __set_permissions__(cls, field_to_permission, permission_to_field): + def __get_field_permissions__(cls, field_to_permission, permission_to_field): """Combines permissions from meta""" permissions = field_to_permission if field_to_permission else {} if permission_to_field: @@ -147,8 +159,29 @@ def __set_permissions__(cls, field_to_permission, permission_to_field): else: permissions[field] = perms - cls.field_permissions = permissions + return permissions + + @classmethod + def __get_permission_to_fields__(cls, permission_to_field): + """ + Accepts a dictionary like + { + permission: [fields] + } + :return: Mapping of fields to permissions like + { + field: [permissions] + } + """ + permissions = {} + for permission, fields in permission_to_field.items(): + for field in fields: + permissions[field] = permissions.get(field, ()) + (permission,) + return permissions + @classmethod + def __set_permissions_resolvers__(cls, permissions): + """Set permission resolvers""" for field_name, field_permissions in permissions.items(): attr = 'resolve_{}'.format(field_name) resolver = getattr(cls._meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) @@ -156,7 +189,7 @@ def __set_permissions__(cls, field_to_permission, permission_to_field): if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) - setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) + setattr(cls, attr, get_auth_resolver(field_name, field_permissions, resolver)) @classmethod def __set_as_nullable__(cls, model, registry): @@ -170,33 +203,6 @@ def __set_as_nullable__(cls, model, registry): field._type = field._type._of_type setattr(cls, name, field) - @classmethod - def __get_permission_to_fields__(cls, permission_to_field): - """ - Accepts a dictionary like - { - permission: [fields] - } - :return: Mapping of fields to permissions - """ - permissions = {} - for permission, fields in permission_to_field.items(): - for field in fields: - permissions[field] = permissions.get(field, ()) + (permission,) - return permissions - - @classmethod - def set_auth_resolver(cls, name, permissions, resolver=None): - """ - Set middleware resolver to handle field permissions - :param name: Field name - :param permissions: List of permissions - :param field: Meta's field - :param resolver: Field resolver - :return: Middleware resolver to check permissions - """ - return partial(auth_resolver, resolver, permissions, name, None, False) - def resolve_id(self, info): return self.pk diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 2d707421f..71093fccb 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -131,7 +131,7 @@ def auth_resolver(parent_resolver, permissions, attname, default_value, raise_ex # A resolver is provided in the class return resolve_bound_resolver(parent_resolver, root, info, **args) # Get default resolver - return get_default_resolver(attname, default_value, root, info, **args) + return get_default_resolver()(attname, default_value, root, info, **args) elif raise_exception: raise PermissionDenied() return None From 1adb13dff8ba68d0725e7359c3ef8cff04433dd4 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 16:35:01 -0500 Subject: [PATCH 29/44] Set function as unbound --- graphene_django/fields.py | 3 ++- graphene_django/types.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 0e1525592..801445eba 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -165,4 +165,5 @@ def __init__(self, type, permissions, permissions_resolver=auth_resolver, *args, def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(get_unbound_function(self.permissions_resolver), self.resolver or parent_resolver, self.permissions,None, None, True) + return partial(get_unbound_function(self.permissions_resolver), self.resolver or parent_resolver, + self.permissions, None, None, True) diff --git a/graphene_django/types.py b/graphene_django/types.py index 80d4ba53b..8eb91a1a7 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -33,7 +33,7 @@ def construct_fields(model, registry, only_fields, exclude_fields): return fields -def get_auth_resolver(cls, name, permissions, resolver=None): +def get_auth_resolver(name, permissions, resolver=None): """ Get middleware resolver to handle field permissions :param name: Field name From ef3cd819593caea020b29e404b1e2c30b4efd232 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 17:21:26 -0500 Subject: [PATCH 30/44] Reorder field_permissions --- graphene_django/types.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index 8eb91a1a7..b8f6899ca 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -134,15 +134,18 @@ def __init_subclass_with_meta__( _meta.fields = django_fields _meta.connection = connection + field_permissions = cls.__get_field_permissions__(field_to_permission, permission_to_field) + if field_permissions: + cls.__set_as_nullable__(model, registry) + super(DjangoObjectType, cls).__init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options ) - cls.field_permissions = cls.__get_field_permissions__(field_to_permission, permission_to_field) + if field_permissions: + cls.__set_permissions_resolvers__(field_permissions) - if cls.field_permissions: - cls.__set_permissions_resolvers__(cls.field_permissions) - cls.__set_as_nullable__(cls._meta.model, cls._meta.registry) + cls.field_permissions = field_permissions if not skip_registry: registry.register(cls) From 7272d34950bd8e5f0d88b8009caa7d6ab64896d0 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 17:51:30 -0500 Subject: [PATCH 31/44] Pass field_permissions as variable to __set_as_nullable__ method --- graphene_django/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index b8f6899ca..6ccd3c86a 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -136,7 +136,7 @@ def __init_subclass_with_meta__( field_permissions = cls.__get_field_permissions__(field_to_permission, permission_to_field) if field_permissions: - cls.__set_as_nullable__(model, registry) + cls.__set_as_nullable__(field_permissions, model, registry) super(DjangoObjectType, cls).__init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options @@ -195,10 +195,10 @@ def __set_permissions_resolvers__(cls, permissions): setattr(cls, attr, get_auth_resolver(field_name, field_permissions, resolver)) @classmethod - def __set_as_nullable__(cls, model, registry): + def __set_as_nullable__(cls, field_permissions, model, registry): """Set restricted fields as nullable""" django_fields = yank_fields_from_attrs( - construct_fields(model, registry, cls.field_permissions.keys(), ()), + construct_fields(model, registry, field_permissions.keys(), ()), _as=Field, ) for name, field in django_fields.items(): From 3eee96c9adb5b549c15143fc4b150a45fc021960 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 22:48:00 -0500 Subject: [PATCH 32/44] Update get_unbound_function import --- graphene_django/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 71093fccb..d3c1e8912 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -6,8 +6,8 @@ # from graphene.utils import LazyList -from django.utils.six import get_unbound_function from graphene.types.resolver import get_default_resolver +from graphene.utils.get_unbound_function import get_unbound_function class LazyList(object): From 063cced5853aaeacfa7b7acf0b8ded3d0d0fdb95 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 1 Mar 2019 09:28:14 -0500 Subject: [PATCH 33/44] Update tests --- graphene_django/tests/test_fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index bf2bbd7fa..95ccb6484 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -1,6 +1,6 @@ from unittest import TestCase from django.core.exceptions import PermissionDenied -from graphene_django.fields import DjangoPermissionField +from graphene_django.fields import DjangoField class MyInstance(object): @@ -14,7 +14,7 @@ class DjangoPermissionFieldTests(TestCase): def test_permission_field(self): MyType = object() - field = DjangoPermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') + field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver') resolver = field.get_resolver(None) class Viewer(object): @@ -30,7 +30,7 @@ class Context(object): def test_permission_field_without_permission(self): MyType = object() - field = DjangoPermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') + field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver') resolver = field.get_resolver(field.resolver) class Viewer(object): From ad2342a08b680aef6154e09ed8307fab647dd459 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 1 Mar 2019 12:40:54 -0500 Subject: [PATCH 34/44] Verify there is a viewer on context --- graphene_django/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 55581c4ee..f4800eba1 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -122,7 +122,10 @@ def auth_resolver(parent_resolver, permissions, raise_exception, root, info, **a :return: Resolved field. None if the viewer does not have permission to access the field. """ # Get viewer from context + if not hasattr(info.context, 'user'): + raise PermissionDenied() user = info.context.user + if has_permissions(user, permissions): if parent_resolver: # A resolver is provided in the class From 76cbad1fb21530ad081c73f7a64c0b95dcc935e1 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 1 Mar 2019 12:42:29 -0500 Subject: [PATCH 35/44] Change DjangoField to PermissionField --- graphene_django/fields.py | 4 ++-- graphene_django/tests/test_fields.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 1bfd33a61..25eb41922 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -154,12 +154,12 @@ def get_resolver(self, parent_resolver): ) -class DjangoField(Field): +class PermissionField(Field): """Class to manage permission for fields""" def __init__(self, type, permissions, permissions_resolver=auth_resolver, *args, **kwargs): """Get permissions to access a field""" - super(DjangoField, self).__init__(type, *args, **kwargs) + super(PermissionField, self).__init__(type, *args, **kwargs) self.permissions = permissions self.permissions_resolver = permissions_resolver diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 95ccb6484..23cce7f7d 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -1,6 +1,6 @@ from unittest import TestCase from django.core.exceptions import PermissionDenied -from graphene_django.fields import DjangoField +from graphene_django.fields import PermissionField class MyInstance(object): @@ -10,11 +10,11 @@ def resolver(self): return "resolver method" -class DjangoPermissionFieldTests(TestCase): +class PermissionFieldTests(TestCase): def test_permission_field(self): MyType = object() - field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver') + field = PermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') resolver = field.get_resolver(None) class Viewer(object): @@ -30,7 +30,7 @@ class Context(object): def test_permission_field_without_permission(self): MyType = object() - field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver') + field = PermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') resolver = field.get_resolver(field.resolver) class Viewer(object): From 8912175cc5393be4619513b78be9cc8458813da3 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 1 Mar 2019 12:44:52 -0500 Subject: [PATCH 36/44] Set permissions as optional --- graphene_django/fields.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 25eb41922..bbf194020 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -157,7 +157,7 @@ def get_resolver(self, parent_resolver): class PermissionField(Field): """Class to manage permission for fields""" - def __init__(self, type, permissions, permissions_resolver=auth_resolver, *args, **kwargs): + def __init__(self, type, permissions=(), permissions_resolver=auth_resolver, *args, **kwargs): """Get permissions to access a field""" super(PermissionField, self).__init__(type, *args, **kwargs) self.permissions = permissions @@ -165,5 +165,7 @@ def __init__(self, type, permissions, permissions_resolver=auth_resolver, *args, def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(get_unbound_function(self.permissions_resolver), self.resolver or parent_resolver, - self.permissions, True) + parent_resolver = super(PermissionField, self).get_resolver(parent_resolver) + if self.permissions: + return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, True) + return parent_resolver From 1e1e849f4a9877a721cc45a88a818a31d53a951b Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 1 Mar 2019 14:34:54 -0500 Subject: [PATCH 37/44] Rename to DjangoField --- graphene_django/fields.py | 6 +++--- graphene_django/tests/test_fields.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 48a7b1eb7..e078ba454 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -154,18 +154,18 @@ def get_resolver(self, parent_resolver): ) -class PermissionField(Field): +class DjangoField(Field): """Class to manage permission for fields""" def __init__(self, type, permissions=(), permissions_resolver=auth_resolver, *args, **kwargs): """Get permissions to access a field""" - super(PermissionField, self).__init__(type, *args, **kwargs) + super(DjangoField, self).__init__(type, *args, **kwargs) self.permissions = permissions self.permissions_resolver = permissions_resolver def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - parent_resolver = super(PermissionField, self).get_resolver(parent_resolver) + parent_resolver = super(DjangoField, self).get_resolver(parent_resolver) if self.permissions: return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, None, None, True) return parent_resolver diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 23cce7f7d..e0478bdb0 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -1,6 +1,6 @@ from unittest import TestCase from django.core.exceptions import PermissionDenied -from graphene_django.fields import PermissionField +from graphene_django.fields import DjangoField class MyInstance(object): @@ -14,7 +14,7 @@ class PermissionFieldTests(TestCase): def test_permission_field(self): MyType = object() - field = PermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') + field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver') resolver = field.get_resolver(None) class Viewer(object): @@ -30,7 +30,7 @@ class Context(object): def test_permission_field_without_permission(self): MyType = object() - field = PermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') + field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver') resolver = field.get_resolver(field.resolver) class Viewer(object): From 8e2f626f350341d385784c1d8b54369fcb0bab66 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 1 Mar 2019 16:38:47 -0500 Subject: [PATCH 38/44] Set conversion to enum --- graphene_django/forms/converter.py | 36 +++++++++++-------- graphene_django/forms/mutation.py | 8 ++--- graphene_django/forms/tests/test_converter.py | 6 ++-- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 07988f423..8b92a8e5a 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -86,22 +86,30 @@ def convert_form_field_to_id(field): return ID(required=field.required) -@convert_form_field.register(forms.TypedChoiceField) -def convert_form_to_enum(field, name): +def convert_form_field_with_choices(name, field): + """ + Helper method to convert a field to graphene Field type. + :param name: form field's name + :param field: form field to convert to + :return: graphene Field + """ choices = getattr(field, 'choices', None) - name = to_camel_case(name) - choices = list(get_choices(choices)) - named_choices = [(c[0], c[1]) for c in choices] - named_choices_descriptions = {c[0]: c[2] for c in choices} - class EnumWithDescriptionsType(object): - """Enum type definition""" + if choices: + name = to_camel_case(field.label or name) + choices = list(get_choices(choices)) + named_choices = [(c[0], c[1]) for c in choices] + named_choices_descriptions = {c[0]: c[2] for c in choices} - @property - def description(self): - """Return field description""" + class EnumWithDescriptionsType(object): + """Enum type definition""" - return named_choices_descriptions[self.name] + @property + def description(self): + """Return field description""" - enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType) - return enum(description=field.help_text, required=field.required) + return named_choices_descriptions[self.name] + + enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType) + return enum(description=field.help_text, required=field.required) # pylint: disable=E1102 + return convert_form_field(field) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 1d1fe74bd..0b64c4638 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -13,7 +13,7 @@ from graphene.types.utils import yank_fields_from_attrs from graphene_django.registry import get_global_registry -from .converter import convert_form_field +from .converter import convert_form_field, convert_form_field_with_choices from .types import ErrorType @@ -30,11 +30,7 @@ def fields_for_form(form, only_fields, exclude_fields): if is_not_in_only or is_excluded: continue - choices = getattr(field, 'choices', None) - if choices: - fields[name] = convert_form_field(field, field.label or name) - else: - fields[name] = convert_form_field(field) + fields[name] = convert_form_field_with_choices(name, field) return fields diff --git a/graphene_django/forms/tests/test_converter.py b/graphene_django/forms/tests/test_converter.py index f7b6f01f4..6039eac89 100644 --- a/graphene_django/forms/tests/test_converter.py +++ b/graphene_django/forms/tests/test_converter.py @@ -17,7 +17,7 @@ Enum, ) -from ..converter import convert_form_field +from ..converter import convert_form_field, convert_form_field_with_choices def assert_conversion(django_field, graphene_field, *args): @@ -116,6 +116,6 @@ def test_should_manytoone_convert_connectionorlist(): def test_should_typed_choice_convert_enum(): - field = forms.TypedChoiceField(choices=(('A', 'Choice A'), ('B', 'Choice B'))) - graphene_type = convert_form_field(field, 'field') + field = forms.TypedChoiceField(choices=(('A', 'Choice A'), ('B', 'Choice B')), label='field') + graphene_type = convert_form_field_with_choices('field_name', field) assert isinstance(graphene_type, Enum) From 6ea5e2bbaf7de64e0bd68dd06a6470eae0fc2246 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Sun, 3 Mar 2019 15:33:11 -0500 Subject: [PATCH 39/44] Update converter --- graphene_django/forms/converter.py | 14 ++++++++++++-- graphene_django/forms/mutation.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 8b92a8e5a..bbaba98e2 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -86,7 +86,13 @@ def convert_form_field_to_id(field): return ID(required=field.required) -def convert_form_field_with_choices(name, field): +def get_form_name(form): + """Get form name""" + class_name = str(form.__class__).split('.')[-1] + return class_name[:-2] + + +def convert_form_field_with_choices(field, name=None, form=None): """ Helper method to convert a field to graphene Field type. :param name: form field's name @@ -96,7 +102,11 @@ def convert_form_field_with_choices(name, field): choices = getattr(field, 'choices', None) if choices: - name = to_camel_case(field.label or name) + if form: + name = to_camel_case("{}_{}".format(get_form_name(form), field.label or name)) + else: + name = field.label or name + name = to_camel_case(name.replace(' ', '_')) choices = list(get_choices(choices)) named_choices = [(c[0], c[1]) for c in choices] named_choices_descriptions = {c[0]: c[2] for c in choices} diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 0b64c4638..4bc3075bc 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -30,7 +30,7 @@ def fields_for_form(form, only_fields, exclude_fields): if is_not_in_only or is_excluded: continue - fields[name] = convert_form_field_with_choices(name, field) + fields[name] = convert_form_field_with_choices(field, name=name, form=form) return fields From f3660b3f30bd45ff72814a7eea3e63c2e2d688ad Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Sun, 3 Mar 2019 15:34:01 -0500 Subject: [PATCH 40/44] Update tests --- graphene_django/forms/tests/test_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/forms/tests/test_converter.py b/graphene_django/forms/tests/test_converter.py index 6039eac89..e76c5c253 100644 --- a/graphene_django/forms/tests/test_converter.py +++ b/graphene_django/forms/tests/test_converter.py @@ -117,5 +117,5 @@ def test_should_manytoone_convert_connectionorlist(): def test_should_typed_choice_convert_enum(): field = forms.TypedChoiceField(choices=(('A', 'Choice A'), ('B', 'Choice B')), label='field') - graphene_type = convert_form_field_with_choices('field_name', field) + graphene_type = convert_form_field_with_choices(field, name='field_name') assert isinstance(graphene_type, Enum) From 41222c15653996bf420836146c828efaf13d20b1 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 4 Mar 2019 08:51:21 -0500 Subject: [PATCH 41/44] Fix lint errors --- graphene_django/fields.py | 3 ++- graphene_django/forms/mutation.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index e078ba454..e3129c69c 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -167,5 +167,6 @@ def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" parent_resolver = super(DjangoField, self).get_resolver(parent_resolver) if self.permissions: - return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, None, None, True) + return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, None, + None, True) return parent_resolver diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 4bc3075bc..ecf492c34 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -13,7 +13,7 @@ from graphene.types.utils import yank_fields_from_attrs from graphene_django.registry import get_global_registry -from .converter import convert_form_field, convert_form_field_with_choices +from .converter import convert_form_field_with_choices from .types import ErrorType From 4996a7272502d0fd283cd74a0ec535eb1c96d852 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 4 Mar 2019 08:52:59 -0500 Subject: [PATCH 42/44] Set model dependency in conversion --- graphene_django/forms/converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index bbaba98e2..09771b604 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -101,7 +101,8 @@ def convert_form_field_with_choices(field, name=None, form=None): """ choices = getattr(field, 'choices', None) - if choices: + # If is a choice field, but not depends on models + if choices and not isinstance(form, (forms.ModelMultipleChoiceField, forms.ModelChoiceField)): if form: name = to_camel_case("{}_{}".format(get_form_name(form), field.label or name)) else: From b93bbb9546a0fdf51a07b1ad2e7585e1487f5c81 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 4 Mar 2019 09:03:52 -0500 Subject: [PATCH 43/44] Add form param to comments --- graphene_django/forms/converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 09771b604..6b17f6bee 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -97,12 +97,13 @@ def convert_form_field_with_choices(field, name=None, form=None): Helper method to convert a field to graphene Field type. :param name: form field's name :param field: form field to convert to + :param form: field's form :return: graphene Field """ choices = getattr(field, 'choices', None) # If is a choice field, but not depends on models - if choices and not isinstance(form, (forms.ModelMultipleChoiceField, forms.ModelChoiceField)): + if choices and not isinstance(field, (forms.ModelMultipleChoiceField, forms.ModelChoiceField)): if form: name = to_camel_case("{}_{}".format(get_form_name(form), field.label or name)) else: From 0ee2327d602a810388aeb93a671896d7cd4ae84a Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 4 Mar 2019 11:31:31 -0500 Subject: [PATCH 44/44] Change verification order --- graphene_django/forms/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 6b17f6bee..023eca745 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -103,7 +103,7 @@ def convert_form_field_with_choices(field, name=None, form=None): choices = getattr(field, 'choices', None) # If is a choice field, but not depends on models - if choices and not isinstance(field, (forms.ModelMultipleChoiceField, forms.ModelChoiceField)): + if not isinstance(field, (forms.ModelMultipleChoiceField, forms.ModelChoiceField)) and choices: if form: name = to_camel_case("{}_{}".format(get_form_name(form), field.label or name)) else: