Skip to content

ListRowLevelViewSetMixin ​

Overview ​

Filter out rows the user cannot access and expose column aggregates.

column_totals maps a client-facing column name to the ORM field path aggregated for it::

column_totals = {"hours": "hours", "product_price": "product_option__price"}

The key is what a client asks for in the column totals query parameter (settings.COLUMN_TOTALS_PARAM, ct by default) and the key it gets back under in the response's columnTotals; the value is a server-side detail no client ever sees. Keeping the two apart is what lets a total sit under the column name that renders it, whatever the ORM path behind it is spelled like.

Totals are opt-in. A list request that names none gets columnTotals: {} and runs no aggregation query at all; one that names some aggregates exactly those, one SUM each. A wildcard value (* or ~all, the same spellings ?e= and ?f= take) asks for every declared total.

Every value is summed, so every value has to name a summable column, reached (if at all) through relations that match at most one related row. A relation that can match several -- a reverse foreign key, a many-to-many -- joins a row per related object and would inflate every total in the same aggregate() call, not just its own, which is why such a path is rejected outright rather than aggregated on its own. The vueda_info.E013 system check reports a declaration that breaks any of these rules; see vueda.info.checks.

Totals cover the rows that the list covers. list aggregates after the filter backends and :meth:apply_row_level_filter run. It aggregates over the whole filtered queryset, so a total is the same on every page. Only a paginated response has a columnTotals key to carry them: VUEDAPageNumberPagination.get_paginated_response reads what list sets on paginator.column_totals, and a viewset with pagination_class = None computes none.

list computes totals in the request that returns them and never caches them, so a total is exactly as fresh as the rows beside it. A response carries only the totals that request named. A client therefore names every total it wants on every request and replaces its totals with each response; merging responses would keep a total computed from rows that may since have changed.

apply_row_level_filter ​

Apply row-level and workflow-aware queryset filters for the given perm_type. Calls RowLevelPermissions.check_queryset and, when the model has a workflow, also annotates state permission info and calls check_queryset_workflow.

Signature ​

apply_row_level_filter(self, queryset, perm_type)

Parameters ​

NameTypeRequiredDefaultDescription
selfyes
querysetyes
perm_typeno'list'

Source ​

server/vueda/core/viewsets/__init__.py:307

get_column_info ​

Aggregate the requested column totals over queryset, keyed by their declared names.

requested_column_totals comes from :meth:get_requested_column_totals. An empty one returns {} without touching the database -- a declared total costs nothing until a client asks for it.

Each total carries a zero as its default, so a filter matching no rows totals 0 rather than null. SUM over no rows is NULL in SQL, but the sum of nothing is zero everywhere a reader would think about it, and a footer cell is the wrong place to explain the difference. It also means a client never has to tell "no rows" apart from "no total": the response says a total is a number, and it always is one.

The sum runs over the matched rows re-selected by primary key rather than over queryset itself. SUM counts a row once per joined match, so a query that reached through a multi-valued relation would total a row's value as many times as it has related rows -- silently, as a number that looks plausible. vueda_info.E013 keeps the declared path single-valued, but the join can arrive from somewhere the check cannot see: a filterset_class filter spanning a reverse FK or M2M, or a RowLevelPermissionsQ doing the same. Re-selecting by pk means a row is summed once however it was matched, which is what "the same filtered, permission-limited set" has to mean for a total to be worth showing.

A total over an annotation that the viewset's own get_queryset adds cannot follow that route, so the two kinds are summed in separate calls. The annotation belongs to the queryset being replaced, and it cannot be moved: the expressions in query.annotations are already resolved, and their Col leaves hold the aliases of the query they were resolved against. Re-applying one to another queryset adds no join -- Col has no resolve_expression of its own -- so an annotation reaching through a relation would compile to SQL naming a table the query never joined.

An annotation total is summed over values("pk", <annotation>).distinct() on the filtered queryset instead. A join that matched a row more than once contributes one (pk, value) pair, so each matched row still counts once, and two rows sharing a value stay two rows. What no route can total is an annotation that reads the multi-valued side of a join, such as F("special_care__id"): it has a value per joined row rather than per row, so there is no per-row total to compute, and only exact duplicates collapse. vueda_info.E013 cannot see this either, since an annotation has no model field behind it to check. Where the value could be a column, a GeneratedField or a database view reached through a one-to-one relation avoids both gaps: the check validates its path like any other.

A queryset that picks one row per group with distinct(*fields), leaving out the primary key, gets totals over the rows it picks: the re-selection keeps the queryset's ordering and distinct fields, which decide the row that each group keeps. A DISTINCT ON that includes the primary key only removes duplicates, so it is dropped from the re-selection. When the per-group distinct fields refer to a queryset annotation, the re-selection cannot carry them, and a request that names any total raises NotImplementedError.

Signature ​

get_column_info(self, queryset, requested_column_totals)

Parameters ​

NameTypeRequiredDefaultDescription
selfyes
querysetyes
requested_column_totalsno()

Source ​

server/vueda/core/viewsets/__init__.py:383

get_declared_column_totals ​

This viewset's column_totals mapping, or {} when it declares none.

Anything that isn't a mapping reads as "none declared" rather than raising here: a misconfigured declaration is the vueda_info.E013 system check's to report, and a request is not the place to find out about it. The effect is that such a viewset offers no totals at all, which is also what its metadata advertises.

An override may narrow the mapping per request, and list and the OpenAPI schema both honor it. Metadata cannot: model_column_totals is built from the registered viewset class, so it advertises the column_totals attribute. Keep every total an override might return in that attribute -- one that isn't there is never advertised, so no client learns to ask for it.

Signature ​

get_declared_column_totals(self)

Parameters ​

NameTypeRequiredDefaultDescription
selfyes

Source ​

server/vueda/core/viewsets/__init__.py:315

get_requested_column_totals ​

The declared total names this list request asked for, in declaration order.

The names arrive in settings.COLUMN_TOTALS_PARAM. Repeating the parameter and comma-separating within one value mean the same thing, matching how ?e= and ?f= are read; empty values and duplicates are dropped, so ?ct= on its own asks for nothing.

A wildcard (* or ~all) asks for every declared total. It does not excuse an unknown name sent alongside it: a name that isn't declared is a mistake in the request whatever else it carries, so it is still reported.

Raises :class:~vueda.core.exceptions.VuedaValidationError (HTTP 400) naming the valid totals for any name this viewset doesn't declare.

Signature ​

get_requested_column_totals(self, request)

Parameters ​

NameTypeRequiredDefaultDescription
selfyes
requestyes

Source ​

server/vueda/core/viewsets/__init__.py:333

list ​

applying row level filter in get_queryset() causes problems with other drf actions, specifically encountered with create not finding it's created object

Signature ​

list(self, request, args, kwargs)

Parameters ​

NameTypeRequiredDefaultDescription
selfyes
requestyes
argsno
kwargsno

Source ​

server/vueda/core/viewsets/__init__.py:552

applies_workflow_state_list_filter ​

column_totals ​

Source ​

server/vueda/core/viewsets/__init__.py:267