Skip to main content
Premium

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's can_read_history: a bool for a fixed answer, or a method taking the request for per-user rules;
  • the admin's can_read otherwise.

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
caution

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).