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:
"
+ "
" + "[" + "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"):
" + ' {"filter": {"status": "active"}},
' + ' {"exclude": {"status": "deleted"}}
' + ' {"my_annotion_function": {}}
' + "]" + "