diff --git a/admin_tools_stats/admin.py b/admin_tools_stats/admin.py index 1712c2a8..1e2729e6 100644 --- a/admin_tools_stats/admin.py +++ b/admin_tools_stats/admin.py @@ -105,7 +105,7 @@ class DashboardStatsAdmin(admin.ModelAdmin): "fields": ( "graph_key", "graph_title", - ("model_app_name", "model_name", "date_field_name"), + ("model_app_name", "model_name", "date_field_name", "queryset_modifiers"), ("operation_field_name", "distinct"), ("user_field_name", "show_to_users"), ("allowed_type_operation_field_name", "type_operation_field_name"), diff --git a/admin_tools_stats/migrations/0022_dashboardstats_queryset_modifiers.py b/admin_tools_stats/migrations/0022_dashboardstats_queryset_modifiers.py new file mode 100644 index 00000000..10c799df --- /dev/null +++ b/admin_tools_stats/migrations/0022_dashboardstats_queryset_modifiers.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.3 on 2023-10-12 11:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("admin_tools_stats", "0021_auto_20230210_1102"), + ] + + operations = [ + migrations.AddField( + model_name="dashboardstats", + name="queryset_modifiers", + field=models.JSONField( + blank=True, + help_text=( + "Additional queryset modifiers in JSON format:
" + "
"
+                    "[
" + ' {"filter": {"status": "active"}},
' + ' {"exclude": {"status": "deleted"}}
' + ' {"my_annotion_function": {}}
' + "]" + "
" + "Ensure the format is a valid JSON array of objects." + ), + null=True, + verbose_name="Queryset modifiers", + ), + ), + ] diff --git a/admin_tools_stats/models.py b/admin_tools_stats/models.py index c5f05ec6..cef22fe1 100644 --- a/admin_tools_stats/models.py +++ b/admin_tools_stats/models.py @@ -315,6 +315,22 @@ class DashboardStats(models.Model): "Can contain multiple fields divided by comma.", ), ) + queryset_modifiers = JSONField( + verbose_name=_("Queryset modifiers"), + null=True, + blank=True, + help_text=mark_safe( + "Additional queryset modifiers in JSON format:
" + "
"
+            "[
" + ' {"filter": {"status": "active"}},
' + ' {"exclude": {"status": "deleted"}}
' + ' {"my_annotion_function": {}}
' + "]" + "
" + "Ensure the format is a valid JSON array of objects." + ), + ) distinct = models.BooleanField( default=False, null=False, @@ -437,12 +453,24 @@ def allowed_chart_types_choices(self): def get_model(self): return apps.get_model(self.model_app_name, self.model_name) + def get_queryset(self): + qs = self.get_model().objects + if self.queryset_modifiers: + for modifier in self.queryset_modifiers: + method_name = list(modifier.keys())[0] + method_args = modifier[method_name] + if isinstance(method_args, dict): + qs = getattr(qs, method_name)(**method_args) + else: + qs = getattr(qs, method_name)(*method_args) + return qs + def get_operation_field(self, operation): - query = self.get_model().objects.all().query + query = self.get_queryset().all().query return query.resolve_ref(operation).field def get_date_field(self): - query = self.get_model().objects.all().query + query = self.get_queryset().all().query return query.resolve_ref(self.date_field_name).field def clean(self, *args, **kwargs): @@ -526,7 +554,6 @@ def get_time_series( interval: Interval, ): """Get the stats time series""" - model_name = apps.get_model(self.model_app_name, self.model_name) kwargs = {} dynamic_kwargs: List[Optional[Q]] = [] if not user.has_perm("admin_tools_stats.view_dashboardstats") and self.user_field_name: @@ -596,7 +623,7 @@ def get_time_series( # TODO: maybe backport values_list support back to django-qsstats-magic and use it again for the query time_range = {"%s__range" % self.date_field_name: (time_since, time_until)} - qs = model_name.objects + qs = self.get_queryset() qs = qs.filter(**time_range) qs = qs.filter(**kwargs) if isinstance(self.get_date_field(), DateTimeField): @@ -881,9 +908,9 @@ def get_dynamic_criteria_field_name(self): return self.prefix + self.criteria.dynamic_criteria_field_name return self.criteria.dynamic_criteria_field_name - def get_dynamic_field(self, model): + def get_dynamic_field(self): field_name = self.get_dynamic_criteria_field_name() - query = model.objects.all().query + query = self.stats.get_queryset().all().query return query.resolve_ref(field_name).field @memoize(60 * 60 * 24 * 7) @@ -896,7 +923,6 @@ def _get_dynamic_choices( operation_field_choice=None, user=None, ) -> "Optional[OrderedDict[str, Tuple[Union[str, bool, List[str]], str]]]": - model = self.stats.get_model() field_name = self.get_dynamic_criteria_field_name() if self.criteria.criteria_dynamic_mapping: return OrderedDict(self.criteria.criteria_dynamic_mapping) @@ -909,7 +935,7 @@ def _get_dynamic_choices( ("False", (False, "Non blank")), ) ) - field = self.get_dynamic_field(model) + field = self.get_dynamic_field() if field.__class__ == models.BooleanField: return OrderedDict( ( @@ -940,7 +966,7 @@ def _get_dynamic_choices( ) end_time = time_until date_filters["%s__lte" % self.stats.date_field_name] = end_time - choices_queryset = model.objects.filter( + choices_queryset = self.stats.get_queryset().filter( **date_filters, ) if user and not user.has_perm("admin_tools_stats.view_dashboardstats"):