Enabling the Aggregate Endpoint
Set aggregate_enabled = True and list the fields that may take part in
aggregation in aggregate_fields. Resources without the opt-in return HTTP 404
for both /admin/<resource>/aggregate and /admin/<resource>/aggregate/schema.
from habarico import ModelAdmin
from habarico_enterprise import FieldAggregation
from habarico_enterprise.aggregations.schema import AggregateFunctionType as AF, GroupByType as GB
from .models import Product
class ProductAdmin(ModelAdmin):
model = Product
fields = ["name", "stock", "price", "category", "status", "created_at"]
filter_fields = ["status", "category", "created_at"]
aggregate_enabled = True
aggregate_max_buckets = 50 # default; override per-admin if needed
aggregate_fields = [
FieldAggregation(name="stock", aggregations=[AF.SUM, AF.AVG, AF.MIN, AF.MAX]),
FieldAggregation(name="price", aggregations=[AF.AVG]),
FieldAggregation(name="created_at", group_by=[GB.TRUNC_DAY, GB.TRUNC_MONTH, GB.TRUNC_YEAR]),
FieldAggregation(name="category__name", group_by=[GB.EXACT]),
FieldAggregation(name="status", group_by=[GB.EXACT]),
]
Permissions
Aggregating is a different level of access than reading rows: a resource can be readable while its
totals are not, and a summary can be worth sharing with someone who may not see the records behind
it. Both /admin/<resource>/aggregate and /admin/<resource>/aggregate/schema therefore resolve:
can_read_aggregations, when declared on the admin — the same contract as the change history'scan_read_history: a bool for a fixed answer, or a method taking the request for per-user rules;- the admin's
can_readotherwise.
Both forms are the same class attribute — def can_read_aggregations(self, request) binds that
name just as can_read_aggregations = True does, and the resolution calls it only when it turns
out callable:
class ReportAdmin(ModelAdmin):
aggregate_enabled = True
can_read = False # the rows stay closed...
can_read_aggregations = True # ...while the totals are open to everyone
The callable form has to be a method, not a lambda assigned to the attribute. A function stored
on a class is a descriptor, so it is reached as a bound method and the request arrives as its
second argument — can_read_aggregations = lambda request: ... raises TypeError: <lambda>() takes 1 positional argument but 2 were given. (The Ruby library takes a lambda here, which is
where the expectation usually comes from.)
The schema endpoint is gated with the data: it describes the whitelist, the labels and the filters
the numbers are served through, so it is no less sensitive than the numbers themselves. A request
without the permission gets 405 from the data endpoint — the status core answers a disallowed
action with — and 404 from the schema one, and aggregate_enabled is reported as false for
that user, so the frontend offers no aggregation widget rather than one that fails when opened.
class OrderAdmin(ModelAdmin):
model = Order
aggregate_enabled = True
aggregate_fields = [FieldAggregation(name="total", aggregations=[AF.SUM, AF.AVG])]
# Everyone with read access sees orders; only finance may aggregate over them.
def can_read_aggregations(self, request):
return request.user.groups.filter(name="finance").exists()
aggregate_enabled: true is also reflected in the resource schema returned by
/admin/<resource>/schema so the frontend can decide whether to render
aggregation-driven widgets. The endpoint contract is fully described in
the repo-root contract/openapi.yaml (/admin/{resource_name}/aggregate,
AggregateResult, AggregateBucket, AggregateSchema).