Skip to content

Commit

Permalink
Stat from dict to namedtuple
Browse files Browse the repository at this point in the history
  • Loading branch information
daanvdk authored and stefanmajoor committed Nov 17, 2023
1 parent c8c661c commit ba25a60
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 58 deletions.
52 changes: 25 additions & 27 deletions binder/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,19 @@
from .route_decorators import list_route


# expr: an aggregate expr to get the statistic,
# filter: a dict of filters to filter the queryset with before getting the aggregate, leading dot not included (optional),
# group_by: a field to group by separated by dots if following relations (optional),
# annotations: a list of annotation names that have to be applied to the queryset for the expr to work (optional),
Stat = namedtuple(
'Stat',
['expr', 'filters', 'group_by', 'annotations'],
defaults=[{}, None, []],
)


DEFAULT_STATS = {
'total': {
'expr': Count(Value(1)),
},
'total': Stat(Count(Value(1))),
}


Expand Down Expand Up @@ -454,16 +463,7 @@ class ModelView(View):
# NOTICE: alternative_filters may not contain a field or annotation as key
alternative_filters = {}

# A dict that looks like:
# {
# name: {
# 'expr': an aggregate expr to get the statistic,
# 'filter': a dict of filters to filter the queryset with before getting the aggregate, leading dot not included (optional),
# 'group_by': a field to group by separated by dots if following relations (optional),
# 'annotations': a list of annotation names that have to be applied to the queryset for the expr to work (optional),
# },
# ...
# }
# A dict that maps stat name to instances of binder.views.Stat
# These statistics can then be used in the stats view
stats = {}

Expand Down Expand Up @@ -2965,31 +2965,29 @@ def _get_stat(self, request, queryset, stat, annotations, include_annotations):
raise BinderRequestError(f'unknown stat: {stat}')

# Apply filters
for key, value in stat.get('filters', {}).items():
for key, value in stat.filters.items():
q, distinct = self._parse_filter(key, value, request, include_annotations)
queryset = self._apply_q_with_possible_annotations(queryset, q, annotations)
if distinct:
queryset = queryset.distinct()

# Apply required annotations
for key in stat.get('annotations', []):
for key in stat.annotations:
try:
expr = annotations.pop(key)
except KeyError:
pass
else:
queryset = queryset.annotate(**{key: expr})

try:
group_by = stat['group_by']
except KeyError:
if stat.group_by is None:
# No group by so just return a simple stat
return {
'value': queryset.aggregate(result=stat['expr'])['result'],
'filters': stat.get('filters', {}),
'value': queryset.aggregate(result=stat.expr)['result'],
'filters': stat.filters,
}

django_group_by = group_by.replace('.', '__')
group_by = stat.group_by.replace('.', '__')
return {
'value': {
# The jsonloads/jsondumps is to make sure we can handle different
Expand All @@ -2998,14 +2996,14 @@ def _get_stat(self, request, queryset, stat, annotations, include_annotations):
for key, value in (
queryset
.order_by()
.exclude(**{django_group_by: None})
.values(django_group_by)
.annotate(_binder_stat=stat['expr'])
.values_list(django_group_by, '_binder_stat')
.exclude(**{group_by: None})
.values(group_by)
.annotate(_binder_stat=stat.expr)
.values_list(group_by, '_binder_stat')
)
},
'group_by': group_by,
'filters': stat.get('filters', {}),
'group_by': stat.group_by,
'filters': stat.filters,
}


Expand Down
33 changes: 13 additions & 20 deletions docs/stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,18 @@ statistics.
## Defining Stats

You can define stats by setting the `stats` property on the view. This should
be a dict that resembles this:

```
{
name: {
'expr': an aggregate expr to get the statistic,
'filter': a dict of filters to filter the queryset with before getting the aggregate, leading dot not included (optional),
'group_by': a field to group by separated by dots if following relations (optional),
'annotations': a list of annotation names that have to be applied to the queryset for the expr to work (optional),
},
...
}
```
be a mapping of stat names to `binder.views.Stat`-instances.

This class has the following signature
`Stat(expr, filters={}, group_by=None, annotations=[])` where:
- **expr**: an aggregate expr to get the statistic,
- **filter**: a dict of filters to filter the queryset with before getting
the aggregate, leading dot not included (optional),
- **group_by**: a field to group by separated by dots if following relations
(optional),
- **annotations**: a list of annotation names that have to be applied to the
queryset for the expr to work (optional),

By default the stat `total` is already defined for every view. This will give
the total amount of records in the dataset. The definition for this looks like this:

```
'total': {
'expr': Count(Value(1)),
},
```
the total amount of records in the dataset. This stat is defined as
`Stat(Count(Value(1)))`.
20 changes: 9 additions & 11 deletions tests/testapp/views/animal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.db.models import Count, Value

from binder.views import ModelView
from binder.views import ModelView, Stat

from ..models import Animal

Expand All @@ -12,14 +12,12 @@ class AnimalView(ModelView):
transformed_searches = {'zoo_id': int}

stats = {
'without_caretaker': {
'expr': Count(Value(1)),
'filters': {
'caretaker:isnull': 'true',
},
},
'by_zoo': {
'expr': Count(Value(1)),
'group_by': 'zoo.name',
},
'without_caretaker': Stat(
Count(Value(1)),
filters={'caretaker:isnull': 'true'},
),
'by_zoo': Stat(
Count(Value(1)),
group_by='zoo.name',
),
}

0 comments on commit ba25a60

Please sign in to comment.