Coverage for backend/django/core/auxiliary/models/Flowsheet.py: 74%

103 statements  

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

1from django.db import connection, models, transaction 

2from django.db.utils import NotSupportedError 

3from django.db.models import Prefetch, QuerySet 

4from django.utils import timezone 

5 

6from core.auxiliary.enums.FlowsheetTemplateType import FlowsheetTemplateType 

7from authentication.user.models import User 

8from authentication.user.AccessTable import AccessTable 

9import core.auxiliary.enums.ViewType as ViewType 

10from core.auxiliary.on_delete import ( 

11 protect_current_state_unless_flowsheet_deleting, 

12) 

13 

14 

15class Flowsheet(models.Model): 

16 name = models.CharField(max_length=64, default="Flowsheet") 

17 project = models.ForeignKey( 

18 "Project", on_delete=models.CASCADE, related_name="flowsheets", null=True 

19 ) 

20 savedDate = models.DateTimeField(null=True) 

21 owner = models.ForeignKey( 

22 User, on_delete=models.CASCADE, related_name="flowsheets", null=True 

23 ) 

24 flowsheet_template_type = models.CharField( 

25 max_length=32, 

26 choices=FlowsheetTemplateType.choices, 

27 default=FlowsheetTemplateType.NotTemplate, 

28 ) 

29 current_state = models.OneToOneField( 

30 "FlowsheetState", 

31 on_delete=protect_current_state_unless_flowsheet_deleting, 

32 related_name="current_for_flowsheet", 

33 ) 

34 revision_sequence = models.PositiveBigIntegerField(default=0) 

35 content_revision = models.PositiveBigIntegerField(default=0) 

36 auto_snapshot_after_single_solve = models.BooleanField(default=False) 

37 created_at = models.DateTimeField(auto_now_add=True) 

38 binned_at = models.DateTimeField(null=True) 

39 

40 @classmethod 

41 @transaction.atomic 

42 def create(cls, **kwargs): 

43 from flowsheetInternals.graphicData.models.groupingModel import Grouping 

44 from PinchAnalysis.models.StreamDataProject import StreamDataProject 

45 from core.auxiliary.models.FlowsheetState import FlowsheetState 

46 

47 saved_date = timezone.now() 

48 explicit_name = kwargs.get("name") 

49 project = kwargs.get("project") 

50 

51 field_values = { 

52 "name": explicit_name or cls.next_default_name(project=project), 

53 "project": project, 

54 "savedDate": saved_date, 

55 "owner": kwargs.get("owner"), 

56 "flowsheet_template_type": kwargs.get( 

57 "flowsheet_template_type", 

58 FlowsheetTemplateType.NotTemplate, 

59 ), 

60 "auto_snapshot_after_single_solve": kwargs.get( 

61 "auto_snapshot_after_single_solve", 

62 False, 

63 ), 

64 } 

65 

66 # PostgreSQL foreign keys are deferred by Django. Reserve the state 

67 # primary key so both sides of the required stable/state cycle can be 

68 # inserted in one transaction without ever committing a null pointer. 

69 if connection.vendor != "postgresql": 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true

70 raise NotSupportedError( 

71 "Flowsheet construction requires PostgreSQL deferred foreign keys." 

72 ) 

73 with connection.cursor() as cursor: 

74 cursor.execute( 

75 "SELECT nextval(pg_get_serial_sequence(%s, %s))", 

76 [FlowsheetState._meta.db_table, FlowsheetState._meta.pk.column], 

77 ) 

78 state_id = cursor.fetchone()[0] 

79 

80 instance = Flowsheet.objects.create( 

81 **field_values, 

82 current_state_id=state_id, 

83 ) 

84 state = FlowsheetState( 

85 pk=state_id, 

86 flowsheet=instance, 

87 created_by=instance.owner, 

88 build_version=kwargs.get("buildVersion", "Not set"), 

89 build_date=kwargs.get("buildDate", "No Build Date Set"), 

90 source_saved_at=saved_date, 

91 ) 

92 state.save(force_insert=True) 

93 

94 if not kwargs.get("initialize_state", True): 

95 # Clone/restore services populate the aggregate before their outer 

96 # transaction commits. Public creation retains the full factory. 

97 return instance 

98 

99 # create the root group 

100 rootGroup = Grouping.create( 

101 state, 

102 group=None, 

103 componentName="Flowsheet", 

104 visible=True, 

105 isRoot=True, 

106 ) 

107 

108 state.root_grouping = rootGroup 

109 state.save(update_fields=["root_grouping"]) 

110 

111 StreamDataProject.create(rootGroup, flowsheet_state=state) 

112 return instance 

113 

114 def clean(self): 

115 """Require the selected working state to belong to this stable flowsheet.""" 

116 

117 super().clean() 

118 if self.current_state_id is None: 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true

119 return 

120 if self.current_state.flowsheet_id != self.pk: 120 ↛ 126line 120 didn't jump to line 126 because the condition on line 120 was always true

121 from django.core.exceptions import ValidationError 

122 

123 raise ValidationError( 

124 {"current_state": "The current state must belong to this flowsheet."} 

125 ) 

126 if not self.current_state.is_working: 

127 from django.core.exceptions import ValidationError 

128 

129 raise ValidationError( 

130 {"current_state": "The current state must be working."} 

131 ) 

132 

133 @classmethod 

134 def next_default_name(cls, *, project=None) -> str: 

135 """ 

136 Return the next generated flowsheet name within a project. 

137 

138 Explicitly named flowsheets do not advance the generated-name sequence; 

139 only names already matching ``Flowsheet-x`` are considered. 

140 """ 

141 queryset = cls.objects.filter(project=project) 

142 max_index = 0 

143 for name in queryset.values_list("name", flat=True): 

144 prefix = "Flowsheet-" 

145 if not name or not name.startswith(prefix): 

146 continue 

147 

148 suffix = name.removeprefix(prefix) 

149 if suffix.isdecimal(): 149 ↛ 143line 149 didn't jump to line 143 because the condition on line 149 was always true

150 max_index = max(max_index, int(suffix)) 

151 return f"Flowsheet-{max_index + 1}" 

152 

153 def set_saved_date(self): 

154 """Timestamp fresh database state without changing active selection.""" 

155 

156 from core.auxiliary.services.flowsheet_template_transitions import ( 

157 touch_flowsheet_saved_date, 

158 ) 

159 

160 flowsheet = touch_flowsheet_saved_date( 

161 candidate=self, 

162 activate_if_regular=False, 

163 ) 

164 self.savedDate = flowsheet.savedDate 

165 

166 def record_content_change(self): 

167 """Record an unjournalled content change and invalidate undo/redo.""" 

168 

169 from flowsheetInternals.unitops.services.edit_operations.context import ( 

170 history_transition_in_progress, 

171 ) 

172 

173 if history_transition_in_progress(): 

174 return 

175 

176 from core.auxiliary.services.flowsheet_template_transitions import ( 

177 touch_flowsheet_saved_date, 

178 ) 

179 

180 with transaction.atomic(): 

181 flowsheet = touch_flowsheet_saved_date( 

182 candidate=self, 

183 activate_if_regular=True, 

184 ) 

185 flowsheet.invalidate_edit_history() 

186 self.content_revision = flowsheet.content_revision 

187 self.savedDate = flowsheet.savedDate 

188 self.flowsheet_template_type = flowsheet.flowsheet_template_type 

189 

190 def invalidate_edit_history(self) -> None: 

191 """Advance content identity and invalidate edits for locked content. 

192 

193 Callers must hold this flowsheet's row lock. This lower-level helper is 

194 also used when revision restore has already taken the lock and replaced 

195 the complete working state. 

196 """ 

197 

198 from flowsheetInternals.unitops.models.FlowsheetEditOperation import ( 

199 FlowsheetEditOperation, 

200 ) 

201 

202 self.content_revision += 1 

203 self.save(update_fields=["content_revision"]) 

204 # Once the aggregate changes outside the recorder, none of these 

205 # payloads can be replayed safely, so retaining them only wastes space. 

206 FlowsheetEditOperation.objects.filter(flowsheet=self).delete() 

207 

208 @classmethod 

209 def with_user_access(cls, queryset: QuerySet, user: User) -> QuerySet: 

210 if user is None or not user.is_authenticated: 

211 return queryset 

212 

213 return queryset.prefetch_related( 

214 Prefetch( 

215 "access_list", 

216 queryset=AccessTable.objects.filter(user=user), 

217 to_attr="current_user_access_entries", 

218 ) 

219 ) 

220 

221 @classmethod 

222 def get_flowsheets_by_view_type(cls, user, view_type): 

223 # filter to exclude templates 

224 base_query = cls.objects.filter( 

225 flowsheet_template_type=FlowsheetTemplateType.NotTemplate 

226 ) 

227 

228 if view_type == ViewType.ALL: 

229 return base_query.filter(access_list__user=user) 

230 elif view_type == ViewType.SHARED: 

231 return base_query.filter(access_list__user=user).exclude(owner=user) 

232 elif view_type == ViewType.OWNED: 

233 return base_query.filter(owner=user) 

234 else: 

235 return base_query.filter(owner=user)