Skip to content

Commit 6912ce9

Browse files
fix: phase boundaries recalculate after cascade; remove debug logging
- Exclude auto-calculated deadlines (edit_privilege=None) from actually_changed set so E9/T5 phase boundaries always recalculate when inner deadlines cascade forward (per AT1.2.1/AT1.2.3) - Cache calculate_initial/calculate_updated querysets on Deadline instance to avoid repeated DB queries in convergence loop - Remove all debug logging from project.py, serializers/project.py, section.py, and views.py
1 parent 50d720a commit 6912ce9

5 files changed

Lines changed: 39 additions & 165 deletions

File tree

projects/models/deadline.py

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -280,36 +280,52 @@ def _calculate_condition_result(attribute_data, calculation):
280280
return result
281281

282282
def calculate_initial(self, project, preview_attributes={}, raw=False):
283+
# Cache queryset to avoid repeated DB queries in convergence loops
284+
if not hasattr(self, '_cached_initial_calculations'):
285+
self._cached_initial_calculations = list(
286+
self.initial_calculations.all().order_by('-index').select_related(
287+
"datecalculation",
288+
"datecalculation__base_date_attribute",
289+
"datecalculation__base_date_deadline",
290+
"datecalculation__base_date_deadline__attribute",
291+
"datecalculation__base_date_deadline__subtype",
292+
"datecalculation__base_date_deadline__phase",
293+
"datecalculation__base_date_deadline__phase__common_project_phase",
294+
"datecalculation__base_date_deadline__phase__project_subtype"
295+
).prefetch_related("conditions", "not_conditions")
296+
)
297+
283298
return self._calculate(
284299
project,
285-
self.initial_calculations.all().order_by('-index').select_related("datecalculation",
286-
"datecalculation__base_date_attribute",
287-
"datecalculation__base_date_deadline",
288-
"datecalculation__base_date_deadline__attribute",
289-
"datecalculation__base_date_deadline__subtype",
290-
"datecalculation__base_date_deadline__phase",
291-
"datecalculation__base_date_deadline__phase__common_project_phase",
292-
"datecalculation__base_date_deadline__phase__project_subtype")
293-
.prefetch_related("conditions", "not_conditions"),
300+
self._cached_initial_calculations,
294301
self.date_type,
295302
preview_attributes,
296303
raw,
297304
)
298305

299306
def calculate_updated(self, project, preview_attributes={}):
300-
if self.update_calculations.exists():
307+
# Cache queryset to avoid repeated DB queries in convergence loops
308+
if not hasattr(self, '_cached_update_calculations'):
309+
if self.update_calculations.exists():
310+
self._cached_update_calculations = list(
311+
self.update_calculations.all().order_by('-index').select_related(
312+
"datecalculation",
313+
"datecalculation__base_date_attribute",
314+
"datecalculation__base_date_deadline",
315+
"datecalculation__base_date_deadline__attribute",
316+
"datecalculation__base_date_deadline__subtype",
317+
"datecalculation__base_date_deadline__phase",
318+
"datecalculation__base_date_deadline__phase__common_project_phase",
319+
"datecalculation__base_date_deadline__date_type"
320+
).prefetch_related("conditions", "not_conditions")
321+
)
322+
else:
323+
self._cached_update_calculations = None
324+
325+
if self._cached_update_calculations:
301326
return self._calculate(
302327
project,
303-
self.update_calculations.all().order_by('-index')
304-
.select_related("datecalculation",
305-
"datecalculation__base_date_attribute",
306-
"datecalculation__base_date_deadline",
307-
"datecalculation__base_date_deadline__attribute",
308-
"datecalculation__base_date_deadline__subtype",
309-
"datecalculation__base_date_deadline__phase",
310-
"datecalculation__base_date_deadline__phase__common_project_phase",
311-
"datecalculation__base_date_deadline__date_type")
312-
.prefetch_related("conditions", "not_conditions"),
328+
self._cached_update_calculations,
313329
self.date_type,
314330
preview_attributes,
315331
)

projects/models/project.py

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -467,14 +467,8 @@ def _min_distance_target_date(self, prev_date, distance, deadline):
467467
prev_date,
468468
distance.distance_from_previous,
469469
)
470-
logger.info(f"[MIN DISTANCE] {deadline.attribute.identifier if deadline.attribute else 'unknown'}: "
471-
f"prev_date={prev_date}, distance={distance.distance_from_previous}, "
472-
f"date_type={distance.date_type}, min_target={min_candidate}")
473470
else:
474471
min_candidate = prev_date + datetime.timedelta(days=distance.distance_from_previous)
475-
logger.info(f"[MIN DISTANCE] {deadline.attribute.identifier if deadline.attribute else 'unknown'}: "
476-
f"prev_date={prev_date}, distance={distance.distance_from_previous}, "
477-
f"no date_type, min_target={min_candidate}")
478472

479473
if not min_candidate:
480474
return None
@@ -484,13 +478,8 @@ def _min_distance_target_date(self, prev_date, distance, deadline):
484478
return min_candidate
485479

486480
def _enforce_distance_requirements(self, deadline, date, preview_attribute_data=None):
487-
logger = logging.getLogger(__name__)
488-
identifier = getattr(getattr(deadline, "attribute", None), "identifier", None)
489-
logger.info(f"[ENFORCE DISTANCE] Starting for {identifier}, input date={date}")
490-
491481
current_date = self._coerce_date_value(date)
492482
if not current_date:
493-
logger.info(f"[ENFORCE DISTANCE] {identifier}: no current_date after coercion")
494483
return date
495484

496485
combined_attributes = dict(self.attribute_data or {})
@@ -499,15 +488,11 @@ def _enforce_distance_requirements(self, deadline, date, preview_attribute_data=
499488

500489
for distance in deadline.distances_to_previous.all():
501490
conditions_ok = distance.check_conditions(combined_attributes)
502-
logger.info(f"[ENFORCE DISTANCE] {identifier}: checking distance from {distance.previous_deadline.attribute.identifier if distance.previous_deadline and distance.previous_deadline.attribute else 'unknown'}, conditions_ok={conditions_ok}")
503-
504491
if not conditions_ok:
505492
continue
506493

507494
prev_date = self._resolve_deadline_date(distance.previous_deadline, preview_attribute_data)
508495
prev_date = self._coerce_date_value(prev_date)
509-
logger.info(f"[ENFORCE DISTANCE] {identifier}: prev_date={prev_date}")
510-
511496
if not prev_date:
512497
continue
513498

@@ -517,24 +502,20 @@ def _enforce_distance_requirements(self, deadline, date, preview_attribute_data=
517502
continue
518503

519504
if current_date < min_target:
520-
logger.info(f"[ENFORCE DISTANCE] {identifier}: VIOLATION detected, moving from {current_date} to {min_target}")
521505
current_date = min_target
522-
else:
523-
logger.info(f"[ENFORCE DISTANCE] {identifier}: distance OK, current={current_date} >= min_target={min_target}")
524506

525507
# After distance checks, also snap to the deadline's date_type if one exists
526508
# This ensures lautakunta dates are always valid Tuesdays, even if the user
527509
# selected a date that satisfies distance requirements but isn't a valid day
528510
if deadline.date_type and current_date:
529511
valid_date = deadline.date_type.get_closest_valid_date(current_date)
530512
if valid_date and valid_date != current_date:
531-
logger.info(f"[ENFORCE DISTANCE] {identifier}: snapping to date_type from {current_date} to {valid_date}")
532513
current_date = valid_date
533514

515+
identifier = getattr(getattr(deadline, "attribute", None), "identifier", None)
534516
if preview_attribute_data is not None and identifier and current_date:
535517
preview_attribute_data[identifier] = current_date
536518

537-
logger.info(f"[ENFORCE DISTANCE] {identifier}: final enforced date={current_date}")
538519
return current_date
539520

540521
def _set_calculated_deadline(self, deadline, date, user, preview, preview_attribute_data=None, confirmed_fields=None):
@@ -840,14 +821,6 @@ def update_deadlines(self, user=None, initial=False, preview_attributes={}, conf
840821

841822
# Calculate a preview schedule without saving anything
842823
def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=None, timing_metrics=None):
843-
logger = logging.getLogger(__name__)
844-
logger.info("="*80)
845-
logger.info(f"[PREVIEW DEADLINES] get_preview_deadlines started for project: {self.name}")
846-
logger.info(f"[PREVIEW DEADLINES] Subtype: {subtype}")
847-
logger.info(f"[PREVIEW DEADLINES] Updated attributes keys: {list(updated_attributes.keys())}")
848-
logger.info(f"[PREVIEW DEADLINES] Updated attributes: {updated_attributes}")
849-
logger.info(f"[PREVIEW DEADLINES] Confirmed fields: {confirmed_fields}")
850-
851824
confirmed_fields = confirmed_fields or []
852825

853826
# Use request values over DB values to avoid stale data
@@ -861,10 +834,8 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
861834
# Use updated value from request if available, otherwise use database value
862835
if deadline.attribute and deadline.attribute.identifier in updated_attributes:
863836
project_dls[deadline] = updated_attributes[deadline.attribute.identifier]
864-
logger.info(f"[PREVIEW DEADLINES] Using request value for {deadline.attribute.identifier}: {updated_attributes[deadline.attribute.identifier]}")
865837
else:
866838
project_dls[deadline] = dl.date
867-
logger.info(f"[PREVIEW DEADLINES] Loaded {len(project_dls)} existing project deadlines")
868839

869840
# List deadlines that would be created
870841
new_dls = {
@@ -875,14 +846,11 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
875846
)
876847
if dl not in project_dls
877848
}
878-
logger.info(f"[PREVIEW DEADLINES] Found {len(new_dls)} new deadlines to create")
879849

880850
project_dls = {**new_dls, **project_dls}
881-
logger.info(f"[PREVIEW DEADLINES] Total deadlines to process: {len(project_dls)}")
882851

883852
# Update attribute-based deadlines
884853
updated_attribute_data = {**self.attribute_data, **updated_attributes}
885-
logger.info(f"[PREVIEW DEADLINES] Combined attribute data has {len(updated_attribute_data)} keys")
886854

887855
# Auto-enable visibility for new deadlines
888856
for dl in new_dls.keys():
@@ -948,18 +916,15 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
948916
}
949917

950918
actually_changed = set()
951-
logger.info("[PREVIEW DEADLINES] Detecting actually changed attributes...")
952919
for key, new_value in updated_attributes.items():
953920
# Skip auto-calculated deadlines - they should always be recalculated
954921
if key in auto_calculated_identifiers:
955-
logger.info(f"[PREVIEW DEADLINES] Skipping auto-calculated: {key}")
956922
continue
957923
old_value = self.attribute_data.get(key)
958924
old_coerced = self._coerce_date_value(old_value) if old_value else None
959925
new_coerced = self._coerce_date_value(new_value) if new_value else None
960926
if old_coerced != new_coerced:
961927
actually_changed.add(key)
962-
logger.info(f"[PREVIEW DEADLINES] Changed: {key} from {old_coerced} to {new_coerced}")
963928

964929
# When visibility bool changes False→True (group re-add), treat dates as "changed"
965930
# to ensure distance enforcement happens for re-added groups
@@ -1261,7 +1226,6 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
12611226
}
12621227

12631228
for convergence_iteration in range(1, max_convergence_iterations + 1):
1264-
log.info(f"[CONVERGENCE] Iteration {convergence_iteration} starting")
12651229
iteration_changes = set()
12661230

12671231
# Step 1: Recalculate phase boundaries
@@ -1292,7 +1256,6 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
12921256
# Update if different or if new value is set for the first time
12931257
if (new_date_coerced and old_date != new_date_coerced) or \
12941258
(new_date_coerced and not old_date):
1295-
log.info(f"[CONVERGENCE] {identifier} recalculated: {old_date} -> {new_date_coerced}")
12961259
updated_attribute_data[identifier] = new_date_coerced
12971260
iteration_changes.add(identifier)
12981261

@@ -1331,12 +1294,10 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
13311294
if confirmed_fields and identifier in confirmed_fields:
13321295
continue
13331296

1334-
log.info(f"[CONVERGENCE CHECK] {identifier} violates distance rule (prev={prev_date}, min={min_target})")
13351297
cascade_queue.add(identifier)
13361298
break
13371299

13381300
if cascade_queue:
1339-
log.info(f"[CONVERGENCE] Processing cascade queue: {len(cascade_queue)} items")
13401301
for cascade_iter in range(1, 11):
13411302
new_cascade_changes = set()
13421303

@@ -1362,9 +1323,7 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
13621323
min_target = self._min_distance_target_date(prev_date, distance, changed_dl)
13631324
if min_target and current_date < min_target:
13641325
if confirmed_fields and changed_id in confirmed_fields:
1365-
log.info(f"[CONVERGENCE ENFORCE] Skipping confirmed {changed_id}")
13661326
continue
1367-
log.info(f"[CONVERGENCE ENFORCE] {changed_id} from {current_date} to {min_target}")
13681327
enforced = self._enforce_distance_requirements(changed_dl, min_target, updated_attribute_data)
13691328
if enforced and enforced != current_date:
13701329
updated_attribute_data[changed_id] = enforced
@@ -1399,9 +1358,7 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
13991358
min_target = self._min_distance_target_date(current_date, distance, next_dl)
14001359
if min_target and next_date < min_target:
14011360
if confirmed_fields and next_id in confirmed_fields:
1402-
log.info(f"[CONVERGENCE PUSH] Skipping confirmed {next_id}")
14031361
continue
1404-
log.info(f"[CONVERGENCE PUSH] Pushing {next_id} from {next_date} to {min_target} (because {changed_id} moved)")
14051362
new_cascade_changes.add(next_id)
14061363
iteration_changes.add(next_id)
14071364
updated_attribute_data[next_id] = min_target
@@ -1413,7 +1370,6 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
14131370

14141371
# Check if converged
14151372
if not iteration_changes:
1416-
log.info(f"[CONVERGENCE] Converged after {convergence_iteration} iteration(s)")
14171373
break
14181374

14191375
if convergence_iteration >= max_convergence_iterations:
@@ -1424,22 +1380,6 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
14241380
if type(value) == bool:
14251381
project_dls[identifier] = value
14261382

1427-
logger.info("="*80)
1428-
logger.info(f"[PREVIEW DEADLINES] get_preview_deadlines completed")
1429-
logger.info(f"[PREVIEW DEADLINES] Returning {len(project_dls)} items")
1430-
# Log first 10 deadline results for debugging
1431-
deadline_count = 0
1432-
for key, value in project_dls.items():
1433-
if hasattr(key, 'attribute') and key.attribute:
1434-
logger.info(f"[PREVIEW DEADLINES] Result: {key.attribute.identifier} = {value}")
1435-
deadline_count += 1
1436-
if deadline_count >= 10:
1437-
logger.info(f"[PREVIEW DEADLINES] ... and {len(project_dls) - 10} more items")
1438-
break
1439-
elif isinstance(key, str):
1440-
logger.info(f"[PREVIEW DEADLINES] Result (bool): {key} = {value}")
1441-
logger.info("="*80)
1442-
14431383
return project_dls
14441384

14451385
@property

0 commit comments

Comments
 (0)