Skip to content

Commit

Permalink
made the filter explicit included required changes in it
Browse files Browse the repository at this point in the history
  • Loading branch information
findsah committed Jan 9, 2024
1 parent ef07602 commit f26af90
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 173 deletions.
185 changes: 76 additions & 109 deletions dashboards/component/filters.py
Original file line number Diff line number Diff line change
@@ -1,137 +1,104 @@
from dataclasses import asdict, dataclass, field
from typing import Any, Dict, List, Type, Optional, Tuple
from dataclasses import asdict
from typing import Any, Dict, List, Optional, Type

from django.http import HttpRequest
from django.urls import reverse
from django.core.exceptions import FieldDoesNotExist
from django.core.paginator import Page, Paginator
from django_filters import FilterSet, CharFilter
from django.forms.widgets import NumberInput
from django.db.models import F, Q, CharField, QuerySet
from django.db.models import CharField, F, Q, QuerySet
from django.db.models.functions import Lower
from django.core.exceptions import FieldDoesNotExist
from django_filters import FilterSet, CharFilter

from .. import config
from ..forms import DashboardForm
from ..types import ValueData
from .base import Component, value_render_encoder
from typing import Any, Dict, List, Type, Optional, Tuple, Literal,Union
from django_filters import FilterSet

@dataclass
class ConfigurationError(Exception):
pass

class FilterData:
form: Dict[str, Any]
dependents: List[str] = field(default_factory=list)

class TableFilterSet(FilterSet):
global_search = CharFilter(
method='filter_global_search',
widget=NumberInput(attrs={'placeholder': 'Global Search'}), # Use NumberInput or another widget
label='',
)

def filter_global_search(self, queryset, name, value):
model_fields = [f.name for f in queryset.model._meta.get_fields()]
q_list = Q()

for field in model_fields:
q_list |= Q(**{f"{field}__icontains": value})

return queryset.filter(q_list)

class TableFilterProcessor:
@staticmethod
def filter(qs: QuerySet, filters: Dict[str, Any]) -> QuerySet:
filter_set = TableFilterSet(data=filters, queryset=qs)
return filter_set.qs

@staticmethod
def sort(qs: QuerySet, fields: List[Any], filters: Dict[str, Any], force_lower: bool) -> QuerySet:
orders = []

for o in range(len(fields)):
order_index = filters.get(f"order[{o}][column]")
if order_index is not None:
field = fields[int(order_index)]
try:
django_field = qs.model._meta.get_field(field)
except FieldDoesNotExist:
django_field = None

if force_lower and django_field and isinstance(django_field, CharField):
field = Lower(field)
else:
field = F(field)

ordered_field = (
field.desc(nulls_last=True)
if filters.get(f"order[{o}][dir]") == "desc"
else field.asc(nulls_last=True)
)
orders.append(ordered_field)

if orders:
qs = qs.order_by(*orders)

return qs

@staticmethod
def count(qs: QuerySet) -> int:
return qs.count()

@dataclass
def __init__(self, action, form, method, dependents=None):
self.action = action
self.form = form
self.method = method
self.dependents = dependents

class DynamicFilterSet(FilterSet):
pass # No need to define any specific filters here

class SampleFilterSet(FilterSet):
# This is a placeholder for the FilterSet. You should replace or extend it.
sample_filter = CharFilter(field_name='sample_field', lookup_expr='icontains')

class Filter(Component):
template_name: Optional[str] = None
method: Optional[Literal["get", "post"]] = "get"
filterset_class: Optional[Type[FilterSet]] = None # Added filterset_class attribute
filter_fields: Type[FilterSet] = SampleFilterSet
form: Type[DashboardForm] = DashboardForm
method: str = "get"
submit_url: str = None
dependents: List[str] = None

def __init__(self, form: Type[DashboardForm], **kwargs):
def __init__(self, filter_fields=None, form=None, method=None, submit_url=None, dependents=None, **kwargs):
super().__init__(**kwargs)
self.form = form
self.filterset_class = None # Initialize filterset_class to None
self.filter_fields = filter_fields or self.filter_fields
self.form = form or self.form
self.method = method or self.method
self.submit_url = submit_url or self.submit_url
self.dependents = dependents or self.dependents

def __post_init__(self):
default_css_classes = config.Config().DASHBOARDS_COMPONENT_CLASSES["Filter"]
self._set_default_css_classes()

if self.css_classes and isinstance(self.css_classes, str):
def _set_default_css_classes(self):
default_css_classes = config.Config().DASHBOARDS_COMPONENT_CLASSES["Filter"]
if isinstance(self.css_classes, str):
self.css_classes = {"filter": self.css_classes}

if isinstance(default_css_classes, dict) and isinstance(self.css_classes, dict):
default_css_classes.update(self.css_classes)

self.css_classes = default_css_classes

if not self.filterset_class:
raise ConfigurationError("Filter set class must be specified for Filter Component")
def get_submit_url(self):
if not self.dashboard:
raise Exception("Dashboard is not set on Filter Component")

def get_filterset(self) -> Type[FilterSet]:
return self.filterset_class # Use filterset_class attribute instead of TableFilterSet
return self.submit_url or self._default_submit_url()

def get_value(self, request: HttpRequest = None, call_deferred=False, filters: Optional[Dict[str, Any]] = None) -> ValueData:
filter_set = self.get_filterset()(data=filters, queryset=QuerySet()) # Pass an empty queryset
queryset = filter_set.qs
def _default_submit_url(self):
args = [self.dashboard._meta.app_label, self.dashboard_class, self.key]
if self.object:
args.insert(2, getattr(self.object, self.dashboard._meta.lookup_field))
return reverse("dashboards:filter_component", args=args)

def get_filter_form(self, data: Dict[str, Any]) -> DashboardForm:
form = self.form(data)
if not form.is_valid():
raise ValueError("Invalid data provided to the form.")
return form

def get_filterset(self) -> Type[FilterSet]:
if not self.filter_fields:
raise ConfigurationError("FilterSet class must be specified for Filter Component")
return self.filter_fields

def get_value(
self,
request: HttpRequest = None,
call_deferred=False,
filters: Optional[Dict[str, Any]] = None,
) -> ValueData:
if not self.form or not self.filter_fields:
raise ConfigurationError("Form and filter_fields must be specified for Filter Component")

filter_form_data = {} # Assuming you have some data to populate the form
filter_form = self.get_filter_form(filter_form_data)
filter_set = self.get_filterset()(data=filter_form_data, queryset=QuerySet()) # Initialize with an empty queryset

filter_data = FilterData(
form=asdict(filter_set.form, dict_factory=value_render_encoder),
method=self.method,
form=asdict(filter_form, dict_factory=value_render_encoder),
action=self.get_submit_url(),
dependents=self.dependents,
)

value = asdict(filter_data, dict_factory=value_render_encoder)

return value

@classmethod
def filter_data(cls, data: Union[QuerySet, List], filters: Dict[str, Any]) -> Union[List, QuerySet]:
return TableFilterProcessor.filter(data, filters)

@classmethod
def sort_data(cls, data: Union[QuerySet, List], filters: Dict[str, Any]) -> Union[List, QuerySet]:
force_lower = cls._meta.force_lower
fields = list(cls._meta.columns)
return TableFilterProcessor.sort(data, fields, filters, force_lower)

@classmethod
def count_data(cls, data: Union[QuerySet, List]) -> int:
return TableFilterProcessor.count(data)

@staticmethod
def apply_paginator(data: Union[QuerySet, List], start: int, length: int) -> Tuple[Page, int]:
paginator = Paginator(data, length)
page_number = (int(start) / int(length)) + 1
return paginator.get_page(page_number), paginator.count
return asdict(filter_data, dict_factory=value_render_encoder)
118 changes: 54 additions & 64 deletions tests/dashboards/components/test_filter.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,60 @@
import pytest
from django.http import HttpRequest
from django.db.models import CharField, Model
from django.core.exceptions import FieldDoesNotExist
from django_filters import FilterSet
from django.forms.widgets import NumberInput
from .. import config
from ..forms import DashboardForm
from ..types import ValueData
from .base import Component, value_render_encoder
from typing import Literal, Union, Type, Dict, Any, List
from dashboards.component.filters import FilterData, TableFilterSet, TableFilterProcessor, Filter
from dashboards.component.filters import Filter, FilterData, DynamicFilterSet
from dashboards.forms import DashboardForm
from django.core.exceptions import ImproperlyConfigured
from typing import Type, Literal

class FakeModel(Model):
name = CharField(max_length=255)
age = CharField(max_length=3)
class MockDashboard:
_meta = type("MockMeta", (), {"app_label": "mock_app", "lookup_field": "id"})

@pytest.fixture
def fake_model_queryset():
fake_data = [
{'name': 'John', 'age': '25'},
{'name': 'Alice', 'age': '30'},
{'name': 'Bob', 'age': '22'},
]
return FakeModel.objects.bulk_create([FakeModel(**data) for data in fake_data])

def test_table_filter_processor(fake_model_queryset):
qs = FakeModel.objects.all()
filters = {'global_search': 'John'}

# Test filter method
filtered_qs = TableFilterProcessor.filter(qs, filters)
assert len(filtered_qs) == 1
assert filtered_qs[0].name == 'John'

sorted_qs = TableFilterProcessor.sort(qs, ['name'], filters, force_lower=False)
assert list(sorted_qs) == sorted(list(qs), key=lambda x: x.name)

count = TableFilterProcessor.count(qs)
assert count == len(qs)

def test_filter(fake_model_queryset):
form_mock = lambda: None
filter_component = Filter(form=form_mock)
filter_component.model = FakeModel

filterset_class = filter_component.get_filterset()
assert issubclass(filterset_class, FilterSet)
assert hasattr(filterset_class, 'global_search')

request_mock = HttpRequest()
filters = {'global_search': 'John'}
value = filter_component.get_value(request=request_mock, filters=filters)
expected_value = FilterData(form={'global_search': 'John'}, dependents=[])
assert value == expected_value

data = FakeModel.objects.all()
filtered_data = Filter.filter_data(data, filters)
assert list(filtered_data) == list(data.filter(name__icontains='John'))

sorted_data = Filter.sort_data(data, filters)
assert list(sorted_data) == sorted(list(data), key=lambda x: x.name)

count_data = Filter.count_data(data)
assert count_data == len(data)

page, total_records = Filter.apply_paginator(data, start=0, length=10)
assert len(page.object_list) == 3
assert total_records == len(data)
def mock_dashboard():
return MockDashboard()

@pytest.fixture
def sample_filter(mock_dashboard):
class MockForm(DashboardForm):
pass # Implement a mock form if needed

class MockFilterSet(DynamicFilterSet):
pass # Implement a mock FilterSet if needed

return Filter(
filter_fields=MockFilterSet,
form=MockForm,
dashboard=mock_dashboard,
)

def test_submit_url_generation(sample_filter):
# Ensure submit_url is generated correctly
assert sample_filter.get_submit_url() == f"/mock_app/mock_dashboard/{sample_filter.key}"

def test_get_filter_form(sample_filter):
# Ensure get_filter_form returns a valid form instance
form_data = {"key": "value"} # Replace with actual form data
form_instance = sample_filter.get_filter_form(form_data)
assert isinstance(form_instance, DashboardForm)
assert form_instance.is_valid()

def test_get_filterset(sample_filter):
# Ensure get_filterset returns a valid FilterSet class
filterset_class = sample_filter.get_filterset()
assert filterset_class == DynamicFilterSet

def test_get_value(sample_filter):
# Ensure get_value returns a valid ValueData
request = HttpRequest()
value_data = sample_filter.get_value(request)
assert isinstance(value_data, dict)
assert "method" in value_data
assert "form" in value_data
assert "action" in value_data
assert "dependents" in value_data

def test_invalid_configuration():
# Ensure ConfigurationError is raised for invalid configuration
with pytest.raises(ImproperlyConfigured):
Filter() # Missing required parameters

# Additional tests can be added based on specific scenarios and requirements

0 comments on commit f26af90

Please sign in to comment.