Loading src/resources/models.py +45 −0 Original line number Diff line number Diff line Loading @@ -934,6 +934,9 @@ class Collection(models.Model): """ reverse=True for grandparent -> parent -> child list reverse=False for child -> parent -> grandparent list This walks the parent relation lazily. Prefer a batched ancestor hydrator for list endpoints that serialize many collections at once. """ ancestors = [] parent = self.parent Loading @@ -957,9 +960,17 @@ class Collection(models.Model): ) def descendants(self) -> Iterator["Collection"]: """Yield descendant collections in child-title order.""" yield from _recurse_collection(self) def descendants_resources(self) -> Iterator[Resource]: """ Yield direct resources first, then resources from each descendant collection. Callers that need file-specific fields should prefetch/select related data after collecting the resources, because this generic traversal intentionally returns plain Resource rows. """ for res in ( self.resources.filter(deleted_at__isnull=True) .order_by("collectionmembership__rank", "title") Loading Loading @@ -1079,6 +1090,7 @@ class Collection(models.Model): return row def dublin_core_metas(self) -> List["MetadataCollectionValue"]: """Return all Dublin Core metadata values attached to this collection.""" metas = [] try: dublin_core_set = MetadataSet.objects.only("id").get( Loading @@ -1102,6 +1114,7 @@ class Collection(models.Model): return [] def dublin_core_title(self) -> str: """Return the Dublin Core title override, falling back to the collection title.""" meta_value = ( self.metadatacollectionvalue_set.filter( metadata__set__title__iexact="Dublin Core", Loading @@ -1115,6 +1128,12 @@ class Collection(models.Model): return self.title def to_path(self, include_pk: bool = False): """ Return the path from the root collection to this collection. `include_pk=True` is useful for API payloads that need stable breadcrumb identifiers; the default keeps the historical plain title list. """ titles = [] for ancestor in self.ancestors(): if include_pk: Loading @@ -1128,6 +1147,12 @@ class Collection(models.Model): return titles def _subtree_collection_ids_for_export(self) -> List[int]: """ Return this collection id plus all descendant ids using one recursive query. Export code only needs ids at this stage; full rows are hydrated later with an explicit field list and ordered in Python. """ with connection.cursor() as cursor: cursor.execute( """ Loading @@ -1148,10 +1173,17 @@ class Collection(models.Model): return [int(row[0]) for row in cursor.fetchall()] def _ordered_subtree_collections_for_export(self) -> List["Collection"]: """ Hydrate the export subtree and return collections in parent-before-child order. The recursive CTE gives membership in the subtree, then this method rebuilds the tree in memory so sibling ordering stays consistent with collection lists. """ collection_ids = self._subtree_collection_ids_for_export() if not collection_ids: return [] # Batch-load the narrow row shape needed by XLSX export rows. collections = { collection.pk: collection for collection in Collection.objects.filter(pk__in=collection_ids).only( Loading @@ -1167,6 +1199,7 @@ class Collection(models.Model): ordered_collections = [] def visit(collection_id: int): # Depth-first traversal keeps each collection before its descendants. collection = collections.get(collection_id) if not collection: return Loading @@ -1182,6 +1215,12 @@ class Collection(models.Model): metadata_ids: Union[List[int], None] = None, metadataset_ids: Union[List[int], None] = None, ) -> Iterator[List[Union[int, str, None]]]: """ Stream XLSX rows for this collection subtree. Metadata values and resource memberships are fetched in batches and grouped by object id so row generation does not query once per collection/resource. """ known_metadatas = list( self.project.metadatas( not settings.JAMA_XLSX_EXPORT_AUTOMATIC_METADATAS, Loading Loading @@ -1304,6 +1343,12 @@ class Collection(models.Model): def _recurse_collection(collection: Collection) -> Iterator[Collection]: """ Recursive generator used by public descendant helpers. It follows `children()` ordering and stops silently on excessive recursion, matching the historical behavior of descendant traversal. """ try: for child in collection.children().iterator(): yield child Loading Loading
src/resources/models.py +45 −0 Original line number Diff line number Diff line Loading @@ -934,6 +934,9 @@ class Collection(models.Model): """ reverse=True for grandparent -> parent -> child list reverse=False for child -> parent -> grandparent list This walks the parent relation lazily. Prefer a batched ancestor hydrator for list endpoints that serialize many collections at once. """ ancestors = [] parent = self.parent Loading @@ -957,9 +960,17 @@ class Collection(models.Model): ) def descendants(self) -> Iterator["Collection"]: """Yield descendant collections in child-title order.""" yield from _recurse_collection(self) def descendants_resources(self) -> Iterator[Resource]: """ Yield direct resources first, then resources from each descendant collection. Callers that need file-specific fields should prefetch/select related data after collecting the resources, because this generic traversal intentionally returns plain Resource rows. """ for res in ( self.resources.filter(deleted_at__isnull=True) .order_by("collectionmembership__rank", "title") Loading Loading @@ -1079,6 +1090,7 @@ class Collection(models.Model): return row def dublin_core_metas(self) -> List["MetadataCollectionValue"]: """Return all Dublin Core metadata values attached to this collection.""" metas = [] try: dublin_core_set = MetadataSet.objects.only("id").get( Loading @@ -1102,6 +1114,7 @@ class Collection(models.Model): return [] def dublin_core_title(self) -> str: """Return the Dublin Core title override, falling back to the collection title.""" meta_value = ( self.metadatacollectionvalue_set.filter( metadata__set__title__iexact="Dublin Core", Loading @@ -1115,6 +1128,12 @@ class Collection(models.Model): return self.title def to_path(self, include_pk: bool = False): """ Return the path from the root collection to this collection. `include_pk=True` is useful for API payloads that need stable breadcrumb identifiers; the default keeps the historical plain title list. """ titles = [] for ancestor in self.ancestors(): if include_pk: Loading @@ -1128,6 +1147,12 @@ class Collection(models.Model): return titles def _subtree_collection_ids_for_export(self) -> List[int]: """ Return this collection id plus all descendant ids using one recursive query. Export code only needs ids at this stage; full rows are hydrated later with an explicit field list and ordered in Python. """ with connection.cursor() as cursor: cursor.execute( """ Loading @@ -1148,10 +1173,17 @@ class Collection(models.Model): return [int(row[0]) for row in cursor.fetchall()] def _ordered_subtree_collections_for_export(self) -> List["Collection"]: """ Hydrate the export subtree and return collections in parent-before-child order. The recursive CTE gives membership in the subtree, then this method rebuilds the tree in memory so sibling ordering stays consistent with collection lists. """ collection_ids = self._subtree_collection_ids_for_export() if not collection_ids: return [] # Batch-load the narrow row shape needed by XLSX export rows. collections = { collection.pk: collection for collection in Collection.objects.filter(pk__in=collection_ids).only( Loading @@ -1167,6 +1199,7 @@ class Collection(models.Model): ordered_collections = [] def visit(collection_id: int): # Depth-first traversal keeps each collection before its descendants. collection = collections.get(collection_id) if not collection: return Loading @@ -1182,6 +1215,12 @@ class Collection(models.Model): metadata_ids: Union[List[int], None] = None, metadataset_ids: Union[List[int], None] = None, ) -> Iterator[List[Union[int, str, None]]]: """ Stream XLSX rows for this collection subtree. Metadata values and resource memberships are fetched in batches and grouped by object id so row generation does not query once per collection/resource. """ known_metadatas = list( self.project.metadatas( not settings.JAMA_XLSX_EXPORT_AUTOMATIC_METADATAS, Loading Loading @@ -1304,6 +1343,12 @@ class Collection(models.Model): def _recurse_collection(collection: Collection) -> Iterator[Collection]: """ Recursive generator used by public descendant helpers. It follows `children()` ordering and stops silently on excessive recursion, matching the historical behavior of descendant traversal. """ try: for child in collection.children().iterator(): yield child Loading