Skip to content
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

Queryset modifiers #65

Merged
merged 2 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
1.4.0 (2024-08-02)
------------------

* added DashboardStats.queryset_modifiers to allow to modify queryset before it is used in chart, e.g. to add prefetch_related, filters or execute annotation functions. Fixed criteria are now deprecated in favour of queryset_modifiers.
* values in divided chart now are filtered by other criteria choices

1.3.1 (2024-04-12)
Expand Down
10 changes: 10 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ Requirements
* PostgreSQL (MySQL is experimental, other databases probably not working but PRs are welcome)
* ``simplejson`` for charts based on ``DecimalField`` values

=======
Warning
=======

The ``django-admin-charts`` application intended usage is mainly for system admins with access to Django admin interface.
The application is not intended to be used by untrusted users, as it is exposing some Django functionality to the user, especially in the chart configuration.

It has not been examined whether some malicious user with access to the charts could exploit the application to gain access to the system or data.


============
Installation
============
Expand Down
5 changes: 3 additions & 2 deletions admin_tools_stats/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class DashboardStatsCriteriaAdmin(admin.ModelAdmin):
list_display = (
"id",
"criteria_name",
"criteria_name",
"criteria_fix_mapping",
"dynamic_criteria_field_name",
"criteria_dynamic_mapping_preview",
)
Expand Down Expand Up @@ -108,7 +108,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"),
Expand Down Expand Up @@ -140,6 +140,7 @@ class DashboardStatsAdmin(admin.ModelAdmin):
"created_date",
"date_field_name",
"operation_field_name",
"queryset_modifiers",
"default_chart_type",
)
list_filter = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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:<br>"
"<pre>"
"[<br>"
' {"filter": {"status": "active"}},<br>'
' {"exclude": {"status": "deleted"}}<br>'
' {"my_annotion_function": {}}<br>'
"]"
"</pre>"
"Ensure the format is a valid JSON array of objects."
"<br>"
"The format of the dict on each line is:"
"<br>"
'{"function_name": {"arg1": "value1", "arg2": "value2"}}'
"<br>"
"Where the arg/value pairs are the arguments to the function"
"that will be called on the queryset in order given by the list."
),
null=True,
verbose_name="Queryset modifiers",
),
),
migrations.AlterField(
model_name="dashboardstatscriteria",
name="criteria_fix_mapping",
field=models.JSONField(
blank=True,
help_text="DEPRECATED.<br>Use queryset modifiers instead<br>A JSON dictionary of key-value pairs that will be used for the criteria",
null=True,
verbose_name="fixed criteria / value",
),
),
]
37 changes: 36 additions & 1 deletion admin_tools_stats/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ class DashboardStatsCriteria(models.Model):
null=True,
blank=True,
verbose_name=_("fixed criteria / value"),
help_text=_("a JSON dictionary of key-value pairs that will be used for the criteria"),
help_text=_(
"DEPRECATED.<br>Use queryset modifiers instead<br>"
"A JSON dictionary of key-value pairs that will be used for the criteria"
),
)
dynamic_criteria_field_name = models.CharField(
max_length=90,
Expand Down Expand Up @@ -315,6 +318,29 @@ 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:<br>"
"<pre>"
"[<br>"
' {"filter": {"status": "active"}},<br>'
' {"exclude": {"status": "deleted"}}<br>'
' {"my_annotion_function": {}}<br>'
"]"
"</pre>"
"Ensure the format is a valid JSON array of objects."
"<br>"
"The format of the dict on each line is:"
"<br>"
'{"function_name": {"arg1": "value1", "arg2": "value2"}}'
"<br>"
"Where the arg/value pairs are the arguments to the function"
"that will be called on the queryset in order given by the list."
),
)
distinct = models.BooleanField(
default=False,
null=False,
Expand Down Expand Up @@ -439,6 +465,14 @@ def get_model(self):

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):
Expand Down Expand Up @@ -535,6 +569,7 @@ def get_series_query_parameters(
criteria = m2m.criteria
# fixed mapping value passed info queryset_filters
if criteria.criteria_fix_mapping:
logger.warning("criteria_fix_mapping is deprecated. Use queryset_modifiers instead")
for key in criteria.criteria_fix_mapping:
# value => criteria.criteria_fix_mapping[key]
queryset_filters[key] = criteria.criteria_fix_mapping[key]
Expand Down
37 changes: 36 additions & 1 deletion admin_tools_stats/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
from django.utils import timezone as dj_timezone
from model_bakery import baker

from admin_tools_stats.models import CachedValue, Interval, truncate_ceiling
from admin_tools_stats.models import (
CachedValue,
DashboardStats,
Interval,
truncate_ceiling,
)


try:
Expand Down Expand Up @@ -1730,3 +1735,33 @@ def test_get_multi_series_cached_last_value(self):
datetime(2010, 10, 12, 0, 0).astimezone(current_tz): {"": 1},
}
self.assertDictEqual(serie, testing_data)


class DashboardStatsTest(TestCase):
def setUp(self):
self.user = baker.make("User", username="testuser", password="12345")
self.user2 = baker.make("User", username="testuser2", password="12345")

# Create test data using model bakery
self.kid1 = baker.make("TestKid", happy=True, age=10, height=140, author=self.user)
self.kid2 = baker.make("TestKid", happy=False, age=8, height=130, author=self.user2)
self.kid3 = baker.make("TestKid", happy=True, age=7, height=120, author=self.user)

# Create a DashboardStats instance with queryset modifiers
self.dashboard_stats = DashboardStats.objects.create(
graph_key="test_graph",
graph_title="Test Graph",
model_app_name="demoproject",
model_name="TestKid",
date_field_name="birthday",
queryset_modifiers=[
{"filter": {"happy": True}},
{"exclude": {"height__lt": 130}},
{"order_by": ["-age"]},
],
)

def test_get_queryset(self):
qs = self.dashboard_stats.get_queryset()
self.assertEqual(qs.count(), 1)
self.assertEqual(qs.first(), self.kid1)
Loading