Coverage for backend/django/core/auxiliary/services/project_folder_queries.py: 100%
31 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1"""Bounded project-folder read models for workspace navigation."""
3from django.db import connection
5from authentication.user.models import User
6from core.auxiliary.enums.FlowsheetTemplateType import FlowsheetTemplateType
7from core.auxiliary.models.Flowsheet import Flowsheet
8from core.auxiliary.models.Project import Project
9from core.auxiliary.models.ProjectFolder import ProjectFolder
12def annotate_project_folder_subtree_counts(
13 *,
14 folders: list[ProjectFolder],
15 user: User,
16) -> list[ProjectFolder]:
17 """Count regular projects in each displayed folder's active subtree.
19 The recursive aggregate is rooted only at the bounded response page, so the
20 API reports the total users expect without materializing either projects or
21 descendant folders in Python.
22 """
24 if not folders:
25 return folders
26 folder_ids = [folder.pk for folder in folders]
27 quote = connection.ops.quote_name
28 folder_table = quote(ProjectFolder._meta.db_table)
29 project_table = quote(Project._meta.db_table)
30 flowsheet_table = quote(Flowsheet._meta.db_table)
31 folder_placeholders = ", ".join(["%s"] * len(folder_ids))
32 sql = f"""
33 WITH RECURSIVE folder_tree(root_id, id) AS (
34 SELECT id, id
35 FROM {folder_table}
36 WHERE id IN ({folder_placeholders})
37 AND owner_id = %s
38 AND NOT is_binned
39 UNION ALL
40 SELECT folder_tree.root_id, child.id
41 FROM {folder_table} child
42 JOIN folder_tree ON child.parent_id = folder_tree.id
43 WHERE child.owner_id = %s AND NOT child.is_binned
44 )
45 SELECT folder_tree.root_id, COUNT(flowsheet.id)
46 FROM folder_tree
47 LEFT JOIN {project_table} project
48 ON project.folder_id = folder_tree.id
49 AND project.owner_id = %s
50 AND NOT project.is_binned
51 LEFT JOIN {flowsheet_table} flowsheet
52 ON flowsheet.id = project.active_flowsheet_id
53 AND flowsheet.flowsheet_template_type = %s
54 GROUP BY folder_tree.root_id
55 """
56 with connection.cursor() as cursor:
57 cursor.execute(
58 sql,
59 [
60 *folder_ids,
61 user.pk,
62 user.pk,
63 user.pk,
64 FlowsheetTemplateType.NotTemplate,
65 ],
66 )
67 counts = dict(cursor.fetchall())
68 for folder in folders:
69 folder.project_count = counts.get(folder.pk, 0)
70 return folders
73def list_active_project_folder_ancestors(
74 *,
75 folder: ProjectFolder,
76 user: User,
77) -> list[ProjectFolder]:
78 """Load one root-to-current path without materializing sibling subtrees."""
80 quote = connection.ops.quote_name
81 folder_table = quote(ProjectFolder._meta.db_table)
82 sql = f"""
83 WITH RECURSIVE ancestors AS (
84 SELECT id, parent_id, 0 AS depth
85 FROM {folder_table}
86 WHERE id = %s AND owner_id = %s AND NOT is_binned
87 UNION ALL
88 SELECT parent.id, parent.parent_id, ancestors.depth + 1
89 FROM {folder_table} parent
90 JOIN ancestors ON ancestors.parent_id = parent.id
91 WHERE parent.owner_id = %s AND NOT parent.is_binned
92 )
93 SELECT id
94 FROM ancestors
95 ORDER BY depth DESC
96 """
97 with connection.cursor() as cursor:
98 cursor.execute(sql, [folder.pk, user.pk, user.pk])
99 ancestor_ids = [row[0] for row in cursor.fetchall()]
100 ancestors_by_id = ProjectFolder.objects.in_bulk(ancestor_ids)
101 return [ancestors_by_id[folder_id] for folder_id in ancestor_ids]