Skip to content

Commit

Permalink
feat: make validation and visibility decorators chainable
Browse files Browse the repository at this point in the history
Make `@filter_queryset_for` and `@validation_for` chainable for reuse.
The same method can now be reused for multiple Node types / Mutations.
  • Loading branch information
Stefan Borer committed Jun 4, 2020
1 parent da4e5f7 commit b8ed8c9
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 20 deletions.
30 changes: 30 additions & 0 deletions caluma/caluma_core/tests/test_validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,33 @@ class Meta:

MyMutation.mutate_and_get_payload(None, admin_info)
assert FakeModel.objects.first().created_by_group == "foobar"


def test_custom_validation_chained_decorators(db, info):
FakeModel = get_fake_model(model_base=models.UUIDModel)

class Serializer(serializers.ModelSerializer):
class Meta:
model = FakeModel
fields = "__all__"

class CustomMutation1(Mutation):
class Meta:
serializer_class = Serializer

class CustomMutation2(Mutation):
class Meta:
serializer_class = Serializer

class CustomValidation(BaseValidation):
@validation_for(CustomMutation1)
@validation_for(CustomMutation2)
def validate_custom_mutation(self, mutation, data, info):
data["test"] = "test"
return data

data = CustomValidation().validate(CustomMutation1, {}, info)
assert data["test"] == "test"

data = CustomValidation().validate(CustomMutation2, {}, info)
assert data["test"] == "test"
42 changes: 42 additions & 0 deletions caluma/caluma_core/tests/test_visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,45 @@ class ConfiguredUnion(Union):
assert result.count() == 0
queryset = ConfiguredUnion().filter_queryset(CustomNode, queryset, None)
assert queryset.count() == 0


def test_custom_visibility_chained_decorators(db, history_mock):
"""The first matching filter 'wins'."""
FakeModel1 = get_fake_model(
dict(name=CharField(max_length=255)), model_base=models.UUIDModel
)
FakeModel1.objects.create(name="Name1")
FakeModel1.objects.create(name="Name2")

FakeModel2 = get_fake_model(
dict(name=CharField(max_length=255)), model_base=models.UUIDModel
)
FakeModel2.objects.create(name="Name1")
FakeModel2.objects.create(name="Name2")

class CustomNode1(DjangoObjectType):
class Meta:
model = FakeModel1

class CustomNode2(DjangoObjectType):
class Meta:
model = FakeModel2

class CustomVisibility(BaseVisibility):
@filter_queryset_for(Node)
def filter_queryset_for_all(self, node, queryset, info):
return queryset.none()

@filter_queryset_for(CustomNode1)
@filter_queryset_for(CustomNode2)
def filter_queryset_for_custom_node(self, node, queryset, info):
return queryset.filter(name="Name1")

assert FakeModel1.objects.count() == 2
assert FakeModel2.objects.count() == 2
queryset = CustomVisibility().filter_queryset(Node, FakeModel1.objects, None)
assert queryset.count() == 0
queryset = CustomVisibility().filter_queryset(CustomNode1, FakeModel1.objects, None)
assert queryset.count() == 1
queryset = CustomVisibility().filter_queryset(CustomNode2, FakeModel2.objects, None)
assert queryset.count() == 1
23 changes: 14 additions & 9 deletions caluma/caluma_core/validations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import inspect
from functools import wraps

from django.core.exceptions import ImproperlyConfigured

Expand All @@ -10,12 +9,11 @@ def validation_for(mutation):
"""Decorate function to overwriting validation of specific mutation."""

def decorate(fn):
@wraps(fn)
def _decorated(self, mutation, data, info):
return fn(self, mutation, data, info)
if not hasattr(fn, "_validations"):
fn._validations = []

_decorated._validation = mutation
return _decorated
fn._validations.append(mutation)
return fn

return decorate

Expand Down Expand Up @@ -48,15 +46,22 @@ class BaseValidation(object):
"""

def __init__(self):
validation_fns = inspect.getmembers(self, lambda m: hasattr(m, "_validation"))
validation_muts = [fn._validation.__name__ for _, fn in validation_fns]
validation_fns = inspect.getmembers(self, lambda m: hasattr(m, "_validations"))

validation_muts = [
mutation.__name__
for _, fn in validation_fns
for mutation in fn._validations
]
validation_muts_dups = list_duplicates(validation_muts)
if validation_muts_dups:
raise ImproperlyConfigured(
f"`validation_for` defined multiple times for "
f"{', '.join(validation_muts_dups)} in {str(self)}"
)
self._validations = {fn._validation: fn for _, fn in validation_fns}
self._validations = {
mutation: fn for _, fn in validation_fns for mutation in fn._validations
}

def validate(self, mutation, data, info):
for cls in mutation.mro():
Expand Down
22 changes: 11 additions & 11 deletions caluma/caluma_core/visibilities.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import inspect
from functools import wraps

from django.core.exceptions import ImproperlyConfigured

Expand All @@ -10,12 +9,11 @@ def filter_queryset_for(node):
"""Decorate function to define filtering of queryset of specific node."""

def decorate(fn):
@wraps(fn)
def _decorated(self, node, queryset, info):
return fn(self, node, queryset, info)
if not hasattr(fn, "_visibilities"):
fn._visibilities = []

_decorated._filter_queryset_for = node
return _decorated
fn._visibilities.append(node)
return fn

return decorate

Expand All @@ -42,18 +40,20 @@ class BaseVisibility(object):
"""

def __init__(self):
queryset_fns = inspect.getmembers(
self, lambda m: hasattr(m, "_filter_queryset_for")
)
queryset_nodes = [fn._filter_queryset_for.__name__ for _, fn in queryset_fns]
queryset_fns = inspect.getmembers(self, lambda m: hasattr(m, "_visibilities"))

queryset_nodes = [
node.__name__ for _, fn in queryset_fns for node in fn._visibilities
]
queryset_nodes_dups = list_duplicates(queryset_nodes)
if queryset_nodes_dups:
raise ImproperlyConfigured(
f"`filter_queryset_for` defined multiple times for "
f"{', '.join(queryset_nodes_dups)} in {str(self)}"
)

self._filter_querysets_for = {
fn._filter_queryset_for: fn for _, fn in queryset_fns
node: fn for _, fn in queryset_fns for node in fn._visibilities
}

def filter_queryset(self, node, queryset, info):
Expand Down

0 comments on commit b8ed8c9

Please sign in to comment.