Coverage for backend/django/core/auxiliary/services/flowsheet_states/new_flowsheet.py: 91%

89 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-07-22 05:22 +0000

1"""Create a new stable flowsheet through the NEW_FLOWSHEET clone profile.""" 

2 

3from dataclasses import dataclass 

4 

5from django.db import transaction 

6 

7from authentication.user.models import User 

8from core.auxiliary.enums.FlowsheetTemplateType import FlowsheetTemplateType 

9from core.auxiliary.models.DataRow import DataRow 

10from core.auxiliary.models.Flowsheet import Flowsheet 

11from core.auxiliary.models.Project import Project 

12from core.auxiliary.models.Solution import Solution 

13 

14from .cloning import ( 

15 ProjectCloneContext, 

16 clone_state, 

17 finalize_project_clone, 

18) 

19from .errors import FlowsheetStateCloneError, merge_clone_validation_errors 

20from .registry import CloneProfile 

21from .validation import ( 

22 repair_legacy_formula_unit_references, 

23 validate_state_for_clone, 

24) 

25 

26 

27@dataclass(frozen=True) 

28class ScenarioResultCopySummary: 

29 """User-facing size of the scenario results available to duplicate.""" 

30 

31 has_results: bool 

32 row_count: int 

33 property_count: int 

34 

35 

36def summarize_scenario_results(*, flowsheet_state_ids) -> ScenarioResultCopySummary: 

37 """Count result-table rows and distinct result properties in the scope.""" 

38 

39 state_ids = tuple(flowsheet_state_ids) 

40 if not state_ids: 

41 return ScenarioResultCopySummary(False, 0, 0) 

42 solutions = Solution.all_states.filter(flowsheet_state_id__in=state_ids) 

43 if not solutions.exists(): 

44 return ScenarioResultCopySummary(False, 0, 0) 

45 result_scenario_ids = solutions.values("scenario_id") 

46 return ScenarioResultCopySummary( 

47 has_results=True, 

48 row_count=DataRow.all_states.filter( 

49 flowsheet_state_id__in=state_ids, 

50 scenario_id__in=result_scenario_ids, 

51 ).count(), 

52 property_count=solutions.exclude(property_id=None) 

53 .values("property_id") 

54 .distinct() 

55 .count(), 

56 ) 

57 

58 

59@transaction.atomic 

60def clone_to_new_flowsheet( 

61 *, 

62 source_flowsheet: Flowsheet, 

63 user, 

64 target_project=None, 

65 project_context: ProjectCloneContext | None = None, 

66 name: str | None = None, 

67 repair_legacy_formula_units: bool = False, 

68 include_scenario_results: bool = False, 

69) -> Flowsheet: 

70 """Clone current durable configuration under a fresh stable identity.""" 

71 

72 source_queryset = Flowsheet.objects.select_related( 

73 "current_state", 

74 "project", 

75 ) 

76 if repair_legacy_formula_units: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true

77 source_queryset = source_queryset.select_for_update() 

78 source_flowsheet = source_queryset.get(pk=source_flowsheet.pk) 

79 if repair_legacy_formula_units: 79 ↛ 80line 79 didn't jump to line 80 because the condition on line 79 was never true

80 repair_legacy_formula_unit_references( 

81 source_state=source_flowsheet.current_state, 

82 profile=CloneProfile.NEW_FLOWSHEET, 

83 ) 

84 project = target_project or source_flowsheet.project 

85 owner = project.owner if project is not None else user 

86 owns_context = False 

87 if ( 

88 project_context is None 

89 and source_flowsheet.project_id is not None 

90 and project is not None 

91 and project.pk != source_flowsheet.project_id 

92 ): 

93 project_context = ProjectCloneContext(source_flowsheet.project, project) 

94 owns_context = True 

95 

96 copied = Flowsheet.create( 

97 name=(name or f"{source_flowsheet.name} (copy)")[:64], 

98 project=project, 

99 owner=owner, 

100 flowsheet_template_type=FlowsheetTemplateType.NotTemplate, 

101 auto_snapshot_after_single_solve=( 

102 source_flowsheet.auto_snapshot_after_single_solve 

103 ), 

104 buildVersion=source_flowsheet.current_state.build_version, 

105 buildDate=source_flowsheet.current_state.build_date, 

106 initialize_state=False, 

107 ) 

108 if project_context is not None: 

109 project_context.register_flowsheet(source_flowsheet, copied) 

110 try: 

111 clone_state( 

112 source_state=source_flowsheet.current_state, 

113 target_state=copied.current_state, 

114 profile=CloneProfile.NEW_FLOWSHEET, 

115 project_context=project_context, 

116 include_scenario_results=include_scenario_results, 

117 ) 

118 except FlowsheetStateCloneError as exc: 

119 raise exc.for_source_flowsheet(source_flowsheet) from exc 

120 if owns_context: 

121 finalize_project_clone(project_context) 

122 return copied 

123 

124 

125@transaction.atomic 

126def create_project_from_template( 

127 *, 

128 source_template: Flowsheet, 

129 user: User, 

130) -> tuple[Project, Flowsheet]: 

131 """Create a project whose first working state comes from a template.""" 

132 

133 project = Project.create_empty( 

134 name=f"{source_template.name} (copy)"[:64], 

135 owner=user, 

136 active_flowsheet=None, 

137 is_starred=False, 

138 is_binned=False, 

139 binned_at=None, 

140 ) 

141 copied = clone_to_new_flowsheet( 

142 source_flowsheet=source_template, 

143 user=user, 

144 target_project=project, 

145 ) 

146 copied.flowsheet_template_type = FlowsheetTemplateType.NotTemplate 

147 copied.save(update_fields=["flowsheet_template_type"]) 

148 project.set_active_flowsheet(copied) 

149 return project, copied 

150 

151 

152@transaction.atomic 

153def copy_project_data( 

154 *, 

155 source_project: Project, 

156 user: User, 

157 repair_legacy_formula_units: bool = False, 

158 include_scenario_results: bool = False, 

159) -> Project: 

160 """Duplicate a project and all of its non-template flowsheet states.""" 

161 

162 source_queryset = ( 

163 source_project.flowsheets.filter( 

164 flowsheet_template_type=FlowsheetTemplateType.NotTemplate, 

165 ) 

166 .select_related("current_state") 

167 .order_by("created_at", "id") 

168 ) 

169 if repair_legacy_formula_units: 169 ↛ 170line 169 didn't jump to line 170 because the condition on line 169 was never true

170 source_queryset = source_queryset.select_for_update() 

171 source_flowsheets = list(source_queryset) 

172 validation_errors = [] 

173 for source_flowsheet in source_flowsheets: 

174 if repair_legacy_formula_units: 174 ↛ 175line 174 didn't jump to line 175 because the condition on line 174 was never true

175 repair_legacy_formula_unit_references( 

176 source_state=source_flowsheet.current_state, 

177 profile=CloneProfile.NEW_FLOWSHEET, 

178 ) 

179 try: 

180 validate_state_for_clone( 

181 source_state=source_flowsheet.current_state, 

182 profile=CloneProfile.NEW_FLOWSHEET, 

183 ) 

184 except FlowsheetStateCloneError as exc: 

185 validation_errors.append(exc.for_source_flowsheet(source_flowsheet)) 

186 if validation_errors: 

187 raise merge_clone_validation_errors(validation_errors) 

188 

189 copied_project = Project.create_empty( 

190 name=f"{source_project.name} (copy)"[:32], 

191 owner=user, 

192 active_flowsheet=None, 

193 is_starred=False, 

194 is_binned=False, 

195 binned_at=None, 

196 ) 

197 project_context = ProjectCloneContext( 

198 source_project=source_project, 

199 target_project=copied_project, 

200 ) 

201 project_context.validated_source_state_ids.update( 

202 flowsheet.current_state_id for flowsheet in source_flowsheets 

203 ) 

204 

205 active_source_id = source_project.active_flowsheet_id 

206 copied_active_flowsheet = None 

207 for source_flowsheet in source_flowsheets: 

208 copied_flowsheet = clone_to_new_flowsheet( 

209 source_flowsheet=source_flowsheet, 

210 user=user, 

211 target_project=copied_project, 

212 project_context=project_context, 

213 include_scenario_results=include_scenario_results, 

214 ) 

215 if source_flowsheet.pk == active_source_id: 

216 copied_active_flowsheet = copied_flowsheet 

217 

218 if copied_active_flowsheet is None: 218 ↛ 219line 218 didn't jump to line 219 because the condition on line 218 was never true

219 copied_active_flowsheet = copied_project.flowsheets.order_by( 

220 "created_at", 

221 "id", 

222 ).first() 

223 if copied_active_flowsheet is not None: 223 ↛ 226line 223 didn't jump to line 226 because the condition on line 223 was always true

224 copied_project.set_active_flowsheet(copied_active_flowsheet) 

225 

226 finalize_project_clone(project_context) 

227 return copied_project