core.viewsets
Overview
ViewSet base classes with atomic transactions, row-level filtering, and flex-fields.
Classes
- AtomicCreateModelViewSetMixin
- AtomicDestroyModelViewSetMixin
- AtomicModelViewSet
- AtomicModelViewSetMixin
- AtomicUpdateModelViewSetMixin
- DeactivateActionViewSetMixin
- FlexFieldsMixin
- ListRowLevelViewSetMixin
- NoExtraFieldsForViewSetMixin
- PerActionSerializerMixin
- VuedaReadOnlyViewSet
- VuedaViewSet
- WarningConfirmationMixin
build_prefetch_plan
Derive the select_related/prefetch_related paths needed for serializer's resolved fields against model, so a list or retrieve response's query count no longer grows with the row count for whatever a request's ?e= actually expanded.
Walks serializer.fields rather than re-deriving expansion rules, so it depends on that resolution already having happened: for the root serializer, the caller must have already applied the request's query-param resolution (vueda.core.serializers.ensure_flex_fields_applied -- VuedaViewSet.get_queryset does this before calling here); a recursive call on a nested child gets this for free, because a child's expand/fields/omit values were passed as constructor kwargs and rest_flex_fields applies those automatically the moment .fields is accessed. Either way, a nested serializer only appears in .fields when the response will actually traverse it. A field never named in ?e= (or excluded by a permit_{action}_expands restriction) never becomes one, so it is never planned.
?f= (sparse fields) and ?om= (omit) play no part in this either way, through two separate mechanisms that both happen to have the same effect: FlexFieldsWriteableNestedSerializerMixin.apply_flex_fields re-admits an already-requested expand's own name into the sparse-fields set, as a side effect of defaulting its sub-fields to a wildcard when none are requested; VuedaExpandableFieldsSerializerMixin._get_expanded_field_names does the same for omit, as a side effect of always hiding available_actions from an expanded object. Neither parameter can drop an expand this plan would otherwise cover, nor can either one add one the request never named in ?e=. The plan simply mirrors whatever survives in .fields.
Depth is bounded the same way: a serializer with nothing further expanded at some level has no further nested serializer fields, so the recursion here terminates exactly where rest_flex_fields's own (already-validated) expansion depth does, with no second bound to maintain.
A field's source (defaulting to its name, honoring an explicit source= in its expandable_fields declaration) is resolved against model's actual relations via :func:resolve_relation_path, not against the nested serializer's own declared Meta.model, so a plan is only produced for a source that genuinely names a relation chain.
A GenericForeignKey expand (GenericForeignKeySerializer) resolves its concrete serializer per-instance at representation time, after any queryset planning could run, so it is intentionally not planned here -- it keeps resolving lazily, same as it does today. A GenericRelation (a reverse collection onto a fixed, known model) is a normal to-many relation and is planned like any other.
Returns (select_related, prefetch_related): lists ready to splat into queryset.select_related(*select_related) and queryset.prefetch_related(*prefetch_related).
Signature
build_prefetch_plan(serializer, model)
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| serializer | yes | ||
| model | yes |
Source
server/vueda/core/viewsets/__init__.py:434
filter_new_prefetch_lookups
Drop any entry in prefetch_related (as returned by :func:build_prefetch_plan) whose cache key queryset -- or an earlier-accepted entry in this same call -- already registers, so VuedaViewSet.get_queryset() never hands Django two different querysets for the same lookup.
prefetch_related raises ValueError: '<lookup>' lookup was already seen with a different queryset the moment two lookups register the same cache key with two different Prefetch.queryset values, and this happens even when one side is a bare string (an implicit default-manager queryset) and the other an explicit Prefetch. A viewset whose own queryset/get_queryset() already prefetches a relation this plan also covers -- most plausibly a relation the application hand-optimized before this plan existed -- would otherwise crash the first time a request actually resolves that relation. Deferring to the existing lookup keeps whatever customization it carries (including its own formatted_name annotation, if it needs one) rather than overriding it with the plan's default. Two plan entries can collide the same way -- a serializer that aliases one relation under two expandable-field names produces two Prefetch objects for the same path -- so an entry this call already accepted also counts as "already registered" for the entries that follow it. Django's cache key is the lookup's own prefetch_to, not its full path, so a lookup that sets to_attr registers under that alias and never collides with a plan entry for the same path under its default attribute name.
Reads queryset._prefetch_related_lookups, a private Django attribute with no public equivalent; it is a plain tuple of str/Prefetch entries across the Django versions this package supports (5.2, 6.0, 6.1).
Signature
filter_new_prefetch_lookups(queryset, prefetch_related)
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| queryset | yes | ||
| prefetch_related | yes |
Source
server/vueda/core/viewsets/__init__.py:535
resolve_relation_path
Walk a serializer field's dotted source against model's relations, one segment per dot, the same traversal DRF's own attribute resolution performs. Returns (orm_path, leaf_model, is_to_one): orm_path is source with dots replaced by the __ lookup separator, leaf_model is the model the last segment relates to, and is_to_one says whether every segment in the chain is single-valued (forward foreign key or one-to-one, either direction) -- the condition under which the whole chain can live in select_related rather than needing prefetch_related.
Returns None when source does not resolve to a chain of relations -- a plain column, a dotted path through a Python property or method, or a path through a GenericForeignKey. Such a source cannot be turned into a queryset plan and is left for the ORM to resolve lazily at representation time, same as it does today.
Signature
resolve_relation_path(model, source)
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| model | yes | ||
| source | yes |
Source
server/vueda/core/viewsets/__init__.py:405
Source
server/vueda/core/viewsets/__init__.py