Skip to content

Commit

Permalink
make choices_queryset not dependent on the time range and therefore m…
Browse files Browse the repository at this point in the history
…uch quicker
  • Loading branch information
PetrDlouhy committed Aug 2, 2024
1 parent 6ef6ad2 commit cf335fb
Show file tree
Hide file tree
Showing 4 changed files with 391 additions and 275 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Changelog
* values in divided chart now are filtered by other criteria choices
* removed support for other JSONFields than Django's native JSONField, removed ADMIN_CHARTS_USE_JSONFIELD setting
* admin charts are loaded by JS including chart controls for quicker admin index load
* CriteriaToStatsM2M.choices_based_on_time_range field changed it's meaning. Now choices are always calculated for whole time range. Value of this choice determines the way how the choices are calculated.

1.3.1 (2024-04-12)
------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 5.0.7 on 2024-08-02 15:12

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("admin_tools_stats", "0022_dashboardstats_queryset_modifiers"),
]

operations = [
migrations.AlterField(
model_name="criteriatostatsm2m",
name="choices_based_on_time_range",
field=models.BooleanField(
default=False,
help_text=(
"If checked:<br>\n"
"- divide values will not be cached\n"
"- divide values will change with change of time range\n"
"- other criteria will filter divide values\n"
"\n"
"If unchecked:\n"
"- values will be cached\n"
"- divide values are calculated from related models,"
"which can be much quicker for large datasets\n"
),
verbose_name="Calculate by queryset values",
),
),
]
103 changes: 56 additions & 47 deletions admin_tools_stats/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,8 +587,6 @@ def get_series_query_parameters(
for dynamic_value in dynamic_values:
try:
criteria_value = m2m.get_dynamic_choices(
time_since,
time_until,
operation_choice,
operation_field_choice,
user,
Expand Down Expand Up @@ -700,8 +698,6 @@ def get_multi_time_series(
)
if m2m and m2m.criteria.dynamic_criteria_field_name:
choices = m2m.get_dynamic_choices(
time_since,
time_until,
operation_choice,
operation_field_choice,
user,
Expand Down Expand Up @@ -940,8 +936,22 @@ class Meta:
blank=True,
)
choices_based_on_time_range = models.BooleanField(
verbose_name=_("Choices are dependend on chart time range"),
help_text=_("Choices are not cached if this is set to true"),
verbose_name=_("Calculate by queryset values"),
help_text=_(
mark_safe(
(
"If checked:<br>\n"
"- divide values will not be cached\n"
"- divide values will change with change of time range\n"
"- other criteria will filter divide values\n"
"\n"
"If unchecked:\n"
"- values will be cached\n"
"- divide values are calculated from related models,"
"which can be much quicker for large datasets\n"
)
)
),
default=False,
)
count_limit = models.PositiveIntegerField(
Expand All @@ -960,11 +970,18 @@ def get_dynamic_field(self):
query = self.stats.get_queryset().all().query
return query.resolve_ref(field_name).field

def get_related_model_and_field(self, field_name):
"""Traverse the field_name to get the related model and end target field."""
base_model = self.stats.get_queryset().model
fields = field_name.split("__")
for rel in fields[:-1]: # omit the last segment since it's the field in the target model
relation = base_model._meta.get_field(rel)
base_model = relation.related_model
return base_model, fields[-1] # returns target model and target field name

@memoize(60 * 60 * 24 * 7)
def _get_dynamic_choices(
self,
time_since: datetime.datetime,
time_until: datetime.datetime,
count_limit: Optional[int] = None,
operation_choice=None,
operation_field_choice=None,
Expand Down Expand Up @@ -995,41 +1012,40 @@ def _get_dynamic_choices(
else:
choices: OrderedDict[str, Tuple[Union[str, bool, List[str]], str]] = OrderedDict()
fchoices: Dict[str, str] = dict(field.choices or [])
date_filters = {}
if not self.stats.cache_values:
if time_since is not None:
if (
time_since.tzinfo is None
or time_since.tzinfo.utcoffset(time_since) is None
):
time_since = time_since.astimezone(get_charts_timezone())
date_filters["%s__gte" % self.stats.date_field_name] = time_since
if time_until is not None:
if (
time_until.tzinfo is None
or time_until.tzinfo.utcoffset(time_until) is None
):
time_until = time_until.astimezone(get_charts_timezone()).replace(
hour=23, minute=59
if self.choices_based_on_time_range:
choices_queryset = self.stats.get_queryset()
if queryset_filter:
choices_queryset = choices_queryset.filter(**queryset_filter)
if user and not user.has_perm("admin_tools_stats.view_dashboardstats"):
if not self.stats.user_field_name:
raise Exception(
"User field must be defined to enable charts for non-superusers"
)
end_time = time_until
date_filters["%s__lte" % self.stats.date_field_name] = end_time
choices_queryset = self.stats.get_queryset().filter(
**date_filters,
)
if queryset_filter:
choices_queryset = choices_queryset.filter(**queryset_filter)
if user and not user.has_perm("admin_tools_stats.view_dashboardstats"):
if not self.stats.user_field_name:
raise Exception(
"User field must be defined to enable charts for non-superusers"
choices_queryset = choices_queryset.filter(
**{self.stats.user_field_name: user}
)
choices_queryset = choices_queryset.filter(**{self.stats.user_field_name: user})
choices_queryset = choices_queryset.values_list(
field_name,
flat=True,
).distinct()
choices_queryset = choices_queryset.values_list(
field_name,
flat=True,
).distinct()
else:
# Obtain the related model and the target field dynamically from the field_name
related_model, field_name = self.get_related_model_and_field(field_name)

choices_queryset = related_model.objects.values_list(
field_name, # targeting the final field in the related model
flat=True,
).distinct()

if count_limit:
choices_queryset = (
self.stats.get_queryset()
.values_list(
field_name,
flat=True,
)
.distinct()
)
choices_queryset = choices_queryset.annotate(
f_count=self.stats.get_operation(operation_choice, operation_field_choice),
).order_by(
Expand All @@ -1056,21 +1072,14 @@ def __str__(self):

def get_dynamic_choices(
self,
time_since=None,
time_until=None,
operation_choice=None,
operation_field_choice=None,
user=None,
queryset_filter=None,
):
if not self.count_limit: # We don't have to cache different operation choices
operation_choice = None
if not self.choices_based_on_time_range or self.stats.cache_values:
time_since = None
time_until = None
choices = self._get_dynamic_choices(
time_since,
time_until,
self.count_limit,
operation_choice,
operation_field_choice,
Expand Down
Loading

0 comments on commit cf335fb

Please sign in to comment.