Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
37b90de
Remove mirror of input fields
oliviarves Jan 3, 2019
e150c41
Add tests to DjangoFormMutation
oliviarves Jan 3, 2019
5f17d6e
Add PermissionField
oliviarves Jan 4, 2019
72b8493
Remove import
oliviarves Jan 4, 2019
d773e42
Add tests to DjangoPermissionField
oliviarves Jan 4, 2019
a314511
Change PermissionField to DjangoPermissionField
oliviarves Jan 4, 2019
9e62c71
Add PermissionObjectType
oliviarves Jan 4, 2019
4c6e720
Add viewer management
oliviarves Jan 4, 2019
1fe99a0
Merge remote-tracking branch 'origin/permissions-to-fields#3' into pe…
oliviarves Jan 4, 2019
d1391db
Add permission type
oliviarves Jan 6, 2019
beba130
Refactoring permission field
oliviarves Jan 7, 2019
0b34a21
Set resolver as class attr
oliviarves Jan 7, 2019
56a4ae9
Merge branch 'permissions-to-fields#3' into permission-to-type#5
oliviarves Jan 7, 2019
106d3e0
Add test to permission type
oliviarves Jan 7, 2019
9e007b9
Add converter to graphene_django
oliviarves Jan 7, 2019
b3d4182
Add test to converter
oliviarves Jan 7, 2019
b8eef47
Set default resolver
oliviarves Feb 15, 2019
813b31e
Get unbound function to resolver
oliviarves Feb 15, 2019
120b94e
Merge branch 'permissions-to-fields#3' into permission-to-type#5
oliviarves Feb 18, 2019
7b86824
Mixing DjangoObjectType and DjangoPermissionObjectType
oliviarves Feb 18, 2019
7d643f3
Get bound
oliviarves Feb 18, 2019
da26750
Merge branch 'permissions-to-fields#3' into permission-to-type#5
oliviarves Feb 18, 2019
7c936d9
Merge branch 'permission-to-type#5' into permission-to-type
oliviarves Feb 18, 2019
715fbbe
Update permission to type
oliviarves Feb 19, 2019
142f514
Add permission to class
oliviarves Feb 19, 2019
114202b
Force push
oliviarves Feb 27, 2019
6e087c4
Remove change
oliviarves Feb 27, 2019
f357d7e
Remove field resolver
oliviarves Feb 28, 2019
ad7497d
Refactor PermissionField
oliviarves Feb 28, 2019
cc7bab3
Merge branch 'permissions-to-field' into permissions-to-fields#3
oliviarves Feb 28, 2019
d14af48
Change naming
oliviarves Feb 28, 2019
6cc6492
Merge branch 'permissions-to-field' into permissions-to-fields#3
oliviarves Feb 28, 2019
eca4319
Change get_unbound_function importation
oliviarves Feb 28, 2019
b5dc9ae
Merge branch 'permissions-to-field' into permissions-to-fields#3
oliviarves Feb 28, 2019
c518ddb
Set raise_exception as arg
oliviarves Feb 28, 2019
1382e93
Merge branch 'permissions-to-fields#3' into permission-to-type#5
oliviarves Feb 28, 2019
9a645f3
Refactor DjangoObjectType
oliviarves Feb 28, 2019
10d28ba
Refactor DjangoObjectType
oliviarves Feb 28, 2019
1adb13d
Set function as unbound
oliviarves Feb 28, 2019
ef3cd81
Reorder field_permissions
oliviarves Feb 28, 2019
7272d34
Pass field_permissions as variable to __set_as_nullable__ method
oliviarves Feb 28, 2019
3eee96c
Update get_unbound_function import
oliviarves Mar 1, 2019
063cced
Update tests
oliviarves Mar 1, 2019
6964646
Merge branch 'permissions-to-fields#3' into permission-to-type#5
oliviarves Mar 1, 2019
cb2a2f7
Merge pull request #2 from revolico/remove-mirror-input-on-djangoform…
alejandronunez Mar 1, 2019
ad2342a
Verify there is a viewer on context
oliviarves Mar 1, 2019
76cbad1
Change DjangoField to PermissionField
oliviarves Mar 1, 2019
8912175
Set permissions as optional
oliviarves Mar 1, 2019
e7a4538
Merge branch 'permissions-to-fields#3' into permission-to-type#5
oliviarves Mar 1, 2019
88209cf
Merge pull request #4 from revolico/permissions-to-fields#3
alejandronunez Mar 1, 2019
603c950
Merge branch 'master' into permission-to-type#5
alejandronunez Mar 1, 2019
1e1e849
Rename to DjangoField
oliviarves Mar 1, 2019
ece1677
Merge branch 'permission-to-type#5' of github.com:revolico/graphene-d…
oliviarves Mar 1, 2019
49bb86f
Merge pull request #6 from revolico/permission-to-type#5
alejandronunez Mar 1, 2019
e99d225
Merge remote-tracking branch 'origin/master' into convert_form_choice#7
oliviarves Mar 1, 2019
8e2f626
Set conversion to enum
oliviarves Mar 1, 2019
0541b71
Merge remote-tracking branch 'origin/master' into convert-form-choice
oliviarves Mar 1, 2019
6ea5e2b
Update converter
oliviarves Mar 3, 2019
f3660b3
Update tests
oliviarves Mar 3, 2019
41222c1
Fix lint errors
oliviarves Mar 4, 2019
4996a72
Set model dependency in conversion
oliviarves Mar 4, 2019
b93bbb9
Add form param to comments
oliviarves Mar 4, 2019
1151897
Merge pull request #8 from revolico/convert_form_choice#7
alejandronunez Mar 4, 2019
0ee2327
Change verification order
oliviarves Mar 4, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion graphene_django/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

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 .utils import maybe_queryset
from .utils import maybe_queryset, auth_resolver


class DjangoListField(Field):
Expand Down Expand Up @@ -151,3 +152,21 @@ def get_resolver(self, parent_resolver):
self.max_limit,
self.enforce_first_or_last,
)


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(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(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
45 changes: 44 additions & 1 deletion graphene_django/forms/converter.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -82,3 +84,44 @@ def convert_form_field_to_time(field):
@convert_form_field.register(GlobalIDFormField)
def convert_form_field_to_id(field):
return ID(required=field.required)


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
: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 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:
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}

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) # pylint: disable=E1102
return convert_form_field(field)
11 changes: 7 additions & 4 deletions graphene_django/forms/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_with_choices
from .types import ErrorType


Expand All @@ -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(field)
fields[name] = convert_form_field_with_choices(field, name=name, form=form)
return fields


Expand Down Expand Up @@ -103,15 +103,18 @@ 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:
raise Exception("form_class is required for DjangoFormMutation")

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
Expand Down
9 changes: 8 additions & 1 deletion graphene_django/forms/tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
DateTime,
Date,
Time,
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):
Expand Down Expand Up @@ -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')), label='field')
graphene_type = convert_form_field_with_choices(field, name='field_name')
assert isinstance(graphene_type, Enum)
33 changes: 33 additions & 0 deletions graphene_django/forms/tests/test_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
46 changes: 46 additions & 0 deletions graphene_django/tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from unittest import TestCase
from django.core.exceptions import PermissionDenied
from graphene_django.fields import DjangoField


class MyInstance(object):
value = "value"

def resolver(self):
return "resolver method"


class PermissionFieldTests(TestCase):

def test_permission_field(self):
MyType = object()
field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver')
resolver = field.get_resolver(None)

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 = DjangoField(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())
100 changes: 99 additions & 1 deletion graphene_django/tests/test_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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
Expand Down Expand Up @@ -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(DjangoObjectType):
"""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
22 changes: 21 additions & 1 deletion graphene_django/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ..utils import get_model_fields
from ..utils import get_model_fields, has_permissions
from .models import Film, Reporter


Expand All @@ -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
Loading