---
url: "https://vueda.dev/v3/reference/api/py/vueda.core.viewsets/ListRowLevelViewSetMixin.md"
title: "ListRowLevelViewSetMixin"
---

# 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_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-signature}

`apply_row_level_filter(self, queryset, perm_type)`

#### Parameters {#apply_row_level_filter-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| self |  | yes |  |  |
| <span id="apply_row_level_filter-param-queryset">queryset</span> |  | yes |  |  |
| <span id="apply_row_level_filter-param-perm_type">perm_type</span> |  | no | `'list'` |  |

### Source {#apply_row_level_filter-source}

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

## `get_column_info` {#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 ``RowLevelPermissions``
``Q`` 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-signature}

`get_column_info(self, queryset, requested_column_totals)`

#### Parameters {#get_column_info-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| self |  | yes |  |  |
| <span id="get_column_info-param-queryset">queryset</span> |  | yes |  |  |
| <span id="get_column_info-param-requested_column_totals">requested_column_totals</span> |  | no | `()` |  |

### Source {#get_column_info-source}

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

## `get_declared_column_totals` {#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-signature}

`get_declared_column_totals(self)`

#### Parameters {#get_declared_column_totals-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| self |  | yes |  |  |

### Source {#get_declared_column_totals-source}

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

## `get_requested_column_totals` {#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-signature}

`get_requested_column_totals(self, request)`

#### Parameters {#get_requested_column_totals-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| self |  | yes |  |  |
| <span id="get_requested_column_totals-param-request">request</span> |  | yes |  |  |

### Source {#get_requested_column_totals-source}

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

## `list` {#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-signature}

`list(self, request, args, kwargs)`

#### Parameters {#list-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| self |  | yes |  |  |
| <span id="list-param-request">request</span> |  | yes |  |  |
| <span id="list-param-args">args</span> |  | no |  |  |
| <span id="list-param-kwargs">kwargs</span> |  | no |  |  |

### Source {#list-source}

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

## `applies_workflow_state_list_filter` {#applies_workflow_state_list_filter}

## `column_totals` {#column_totals}

## Source

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