Skip to content

Commit

Permalink
feat: add functions for converting a FilterSet class to a tree
Browse files Browse the repository at this point in the history
  • Loading branch information
SquakR committed Jan 27, 2022
1 parent aadab08 commit c172f06
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 8 deletions.
40 changes: 40 additions & 0 deletions graphene_django_filter/filter_set_converters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Functions for converting a FilterSet class to a tree and then to an input type."""

from typing import List, Optional, Sequence, Type

from anytree import Node
from django_filters import FilterSet


def sequence_to_tree(values: Sequence[str]) -> Node:
"""Convert a sequence to a tree."""
node: Optional[Node] = None
for value in values:
node = Node(value, parent=node)
return node.root


def try_add_sequence(tree: Node, values: Sequence[str]) -> bool:
"""Try to add a sequence to a tree.
Return a flag indicating whether the mutation was made.
"""
if tree.name == values[0]:
for child in tree.children:
is_mutated = try_add_sequence(child, values[1:])
if is_mutated:
return True
tree.children = (*tree.children, sequence_to_tree(values[1:]))
return True
else:
return False


def filter_set_to_trees(filter_set: Type[FilterSet]) -> List[Node]:
"""Convert a FilterSet class to a tree."""
trees: List[Node] = []
for filter_value in filter_set.base_filters.values():
values = (*filter_value.field_name.split('__'), filter_value.lookup_expr)
if len(trees) == 0 or not any([try_add_sequence(tree, values) for tree in trees]):
trees.append(sequence_to_tree(values))
return trees
21 changes: 20 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ graphene = "2.1.9"
graphene-django = "^2.15.0"
django-filter = "^21.1"
stringcase = "^1.2.0"
anytree = "^2.8.0"

[tool.poetry.dev-dependencies]
flake8 = "^4.0.1"
Expand Down
3 changes: 0 additions & 3 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
"""Tests for graphene-django-filter project."""

from .connection_field import DjangoFilterConnectionFieldTest
from .input_type_builders import FilterInputTypeBuilderTest, LookupInputTypeBuilderTest
13 changes: 9 additions & 4 deletions tests/filter_set.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""FilterSet classes."""

from django_filters import FilterSet
import django_filters

from .models import Task, User


class UserFilter(FilterSet):
class UserFilter(django_filters.FilterSet):
"""User FilterSet class for testing."""

class Meta:
Expand All @@ -20,13 +20,18 @@ class Meta:
]


class TaskFilter(FilterSet):
class TaskFilter(django_filters.FilterSet):
"""Task FilterSet class for testing."""

user__email_exact = django_filters.CharFilter(field_name='user__email', lookup_expr='exact')
user__email_contains = django_filters.CharFilter(
field_name='user__email',
lookup_expr='contains',
)

class Meta:
model = Task
fields = [
'name',
'user__email',
'user__last_name',
]
5 changes: 5 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
},
}

DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

INSTALLED_APPS = (
'django.contrib.contenttypes',
'django.contrib.auth',
'django_filters',
'tests',
)

MIDDLEWARE = []

USE_TZ = True

TIME_ZONE = 'UTC'
File renamed without changes.
98 changes: 98 additions & 0 deletions tests/test_filter_set_converters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""FilterSet converters tests."""

from anytree import Node
from anytree.exporter import DictExporter
from django.test import TestCase
from graphene_django_filter.filter_set_converters import (
filter_set_to_trees,
sequence_to_tree,
try_add_sequence,
)

from .filter_set import TaskFilter


class FilterSetConverterTest(TestCase):
"""FilterSet converters tests."""

def setUp(self) -> None:
"""Set up TreeFunctions tests."""
self.tree = Node(
'field1', children=(
Node(
'field2', children=(
Node(
'field3', children=(
Node('field4'),
),
),
),
),
),
)

def test_list_to_tree(self) -> None:
"""Test the sequence_to_tree function."""
self.assertEqual(
DictExporter().export(sequence_to_tree(('field1', 'field2'))),
{
'name': 'field1',
'children': [{'name': 'field2'}],
},
)

def test_possible_try_add_iterable(self) -> None:
"""Test the try_add_sequence function when adding a sequence is possible."""
is_mutated = try_add_sequence(self.tree, ('field1', 'field5', 'field6'))
self.assertEqual(is_mutated, True)
self.assertEqual(
DictExporter().export(self.tree), {
'name': 'field1',
'children': [
{
'name': 'field2',
'children': [
{
'name': 'field3',
'children': [{'name': 'field4'}],
},
],
},
{
'name': 'field5',
'children': [{'name': 'field6'}],
},
],
},
)

def test_impossible_try_add_iterable(self) -> None:
"""Test the try_add_sequence function when adding a sequence is impossible."""
is_mutated = try_add_sequence(self.tree, ('field5', 'field6'))
self.assertEqual(is_mutated, False)

def test_filter_set_to_trees(self) -> None:
"""Test the filter_set_to_trees function."""
trees = filter_set_to_trees(TaskFilter)
exporter = DictExporter()
self.assertEqual(
[exporter.export(tree) for tree in trees], [
{
'name': 'name',
'children': [{'name': 'exact'}],
},
{
'name': 'user',
'children': [
{
'name': 'last_name',
'children': [{'name': 'exact'}],
},
{
'name': 'email',
'children': [{'name': 'exact'}, {'name': 'contains'}],
},
],
},
],
)

0 comments on commit c172f06

Please sign in to comment.