-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dynamic filtering #24
Changes from 9 commits
0156175
2ac6395
c9c4a1a
a443cd7
784e48b
51bb319
e100ebf
f4daf79
5fb9ccc
817114f
74fa8ab
ef07602
f26af90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
from dataclasses import asdict, dataclass, field | ||
from typing import Any, Dict, List, Literal, Optional, Type | ||
|
||
from django.http import HttpRequest | ||
from django.urls import reverse | ||
from django.db import models | ||
from django_filters import FilterSet, CharFilter | ||
|
||
from .. import config | ||
from ..forms import DashboardForm | ||
from ..types import ValueData | ||
from .base import Component, value_render_encoder | ||
|
||
@dataclass | ||
class FilterData: | ||
action: str | ||
form: Dict[str, Any] | ||
method: str | ||
dependents: Optional[List[str]] = None | ||
|
||
@dataclass | ||
class Filter(Component): | ||
template_name: Optional[str] = None | ||
model: Optional[Type[models.Model]] = None | ||
method: Literal["get", "post"] = "get" | ||
submit_url: Optional[str] = None | ||
filter_fields: Optional[Dict[str, Any]] = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it makes more sense to just store the filter set class here rather than an arbitrary There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably also need to validate that the filter is set and raise a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. have updated it |
||
form: Optional[Type[DashboardForm]] = None # Add this line | ||
|
||
def __post_init__(self): | ||
default_css_classes = config.Config().DASHBOARDS_COMPONENT_CLASSES["Filter"] | ||
|
||
if self.css_classes and 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 | ||
|
||
def get_submit_url(self): | ||
if not self.dashboard: | ||
raise Exception("Dashboard is not set on Filter Component") | ||
|
||
if self.submit_url: | ||
return self.submit_url | ||
|
||
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) -> Type[FilterSet]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should probably be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed as per feedback |
||
class DynamicFilterSet(FilterSet): | ||
class Meta: | ||
model = self.model | ||
|
||
for field_name, field_config in self.filter_fields.items(): | ||
DynamicFilterSet.base_filters[field_name] = CharFilter(**field_config) | ||
|
||
return DynamicFilterSet | ||
|
||
def get_value( | ||
self, | ||
request: HttpRequest = None, | ||
call_deferred=False, | ||
filters: Optional[Dict[str, Any]] = None, | ||
) -> ValueData: | ||
if not self.model or not self.filter_fields or not self.form: # Add form check | ||
raise NotImplementedError("Model, filter_fields, and form must be specified for Filter Component") | ||
|
||
filter_form = self.get_filter_form() | ||
|
||
# Apply filter data to the form | ||
filter_instance = filter_form(request.GET, queryset=self.model.objects.all()) | ||
queryset = filter_instance.qs | ||
|
||
filter_data = FilterData( | ||
method=self.method, | ||
form=asdict(filter_instance.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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
from dataclasses import asdict, dataclass | ||
from typing import Any, Dict, Literal, Optional, Type | ||
from dataclasses import asdict, dataclass, field | ||
from typing import Any, Dict, List, Literal, Optional, Type | ||
|
||
from django.http import HttpRequest | ||
from django.urls import reverse | ||
|
@@ -8,15 +8,14 @@ | |
from ..forms import DashboardForm | ||
from ..types import ValueData | ||
from .base import Component, value_render_encoder | ||
|
||
from dashboards.component.filters import Filter | ||
|
||
@dataclass | ||
class FormData: | ||
action: list[str] | ||
form: list[dict[str, Any]] | ||
action: str | ||
form: Dict[str, Any] | ||
method: str | ||
dependents: Optional[list[str]] = None | ||
|
||
dependents: Optional[List[str]] = None | ||
|
||
@dataclass | ||
class Form(Component): | ||
|
@@ -25,12 +24,16 @@ class Form(Component): | |
method: Literal["get", "post"] = "get" | ||
trigger: Literal["change", "submit"] = "change" | ||
submit_url: Optional[str] = None | ||
# Add these attributes for the GenericFilter | ||
filter_data: Optional[List[Dict[str, Any]]] = None | ||
filter_fields: Optional[Dict[str, Any]] = None | ||
|
||
def __post_init__(self): | ||
default_css_classes = config.Config().DASHBOARDS_COMPONENT_CLASSES["Form"] | ||
|
||
# make sure css_classes is a dict as this is what form template requires | ||
if self.css_classes and isinstance(self.css_classes, str): | ||
# if sting assume this is form class | ||
# if string assume this is form class | ||
self.css_classes = {"form": self.css_classes} | ||
|
||
# update defaults with any css classes which have been passed in | ||
|
@@ -77,6 +80,13 @@ def get_form(self, request: HttpRequest = None) -> DashboardForm: | |
data = request.GET | ||
|
||
form = self.form(data=data) | ||
|
||
# Create and apply the GenericFilter if filter_data and filter_fields are provided | ||
if self.filter_data and self.filter_fields and self.model: | ||
filter_instance = Filter(data=self.filter_data, fields=self.filter_fields) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I may be misunderstanding something but why are we applying a filter here, the form shouldn't be doing any filtering right? it will just be providing the ability to submit data? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. filtering removed from form |
||
queryset = filter_instance.filter(self.model.objects.all(), None, None) | ||
form.fields['filter_field'].queryset = queryset | ||
|
||
return form | ||
|
||
def get_value( | ||
|
@@ -88,7 +98,7 @@ def get_value( | |
form = self.get_form(request=request) | ||
form_data = FormData( | ||
method=self.method, | ||
form=form, | ||
form=asdict(form, dict_factory=value_render_encoder), | ||
action=self.get_submit_url(), | ||
dependents=self.dependents, | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want to set a default here, it should just be a type annotation and default to
None
as many use cases wont want a filter on a chartThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should also be the
FilterSet
that gets passed into the filter component rather than the filter component itselfThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed from form