@@ -536,6 +536,10 @@ def _set_calculated_deadline(self, deadline, date, user, preview, preview_attrib
536536 identifier = deadline .attribute .identifier
537537 if identifier in confirmed_fields :
538538 # Get the original confirmed value - don't let calculation overwrite it
539+ # KAAV-3492 FIX: If previewing, prefer the value from the request (preview_attribute_data)
540+ # because self.attribute_data might be stale (e.g. if user just edited it)
541+ if preview and preview_attribute_data and identifier in preview_attribute_data :
542+ return preview_attribute_data .get (identifier )
539543 return self .attribute_data .get (identifier )
540544
541545 # NOTE: Removed line that prioritized preview_attribute_data over calculated value
@@ -881,14 +885,80 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
881885 log .info (f"[KAAV-3492 BACKEND] Visibility bool enabled: { key } " )
882886
883887 # For each deadline, if its visibility bool was just enabled, mark its date as "changed"
884- for dl in project_dls .keys ():
888+ for dl in sorted ( project_dls .keys (), key = lambda x : x . index ):
885889 if not dl .deadlinegroup or not dl .attribute :
886890 continue
887891 vis_bool = get_dl_vis_bool_name (dl .deadlinegroup )
888892 if vis_bool and vis_bool in vis_bools_enabled :
893+ identifier = dl .attribute .identifier
894+
895+ # Check for stale date: Group enabled, but date matches stored value
896+ current_val = updated_attribute_data .get (identifier )
897+ stored_val = self .attribute_data .get (identifier )
898+
899+ current_date = self ._coerce_date_value (current_val )
900+ stored_date = self ._coerce_date_value (stored_val )
901+
902+ if current_date and current_date == stored_date :
903+ # Calculate target date based on predecessors
904+ max_target = None
905+ for distance in dl .distances_to_previous .all ():
906+ combined = {** self .attribute_data , ** updated_attribute_data }
907+ if not distance .check_conditions (combined ):
908+ continue
909+ prev_date = self ._resolve_deadline_date (distance .previous_deadline , updated_attribute_data )
910+ prev_date = self ._coerce_date_value (prev_date )
911+ if not prev_date :
912+ continue
913+ target = self ._min_distance_target_date (prev_date , distance , dl )
914+ if target and (not max_target or target > max_target ):
915+ max_target = target
916+
917+ # SPECIAL CASE (AT1.5.3): Opinions deadline ("viimeistaan_mielipiteet")
918+ # defaults to matching "esillaolo_paattyy" if no distance rules exist
919+ if not max_target and "viimeistaan_mielipiteet" in identifier :
920+ # Find sibling "esillaolo_paattyy" in same group
921+ sibling = next ((d for d in project_dls .keys () if d .deadlinegroup == dl .deadlinegroup and "esillaolo_paattyy" in (d .attribute .identifier if d .attribute else "" )), None )
922+ if sibling and sibling .attribute :
923+ # Use updated_attribute_data if present (already snapped), else stored
924+ sib_id = sibling .attribute .identifier
925+ sib_val = updated_attribute_data .get (sib_id ) or self .attribute_data .get (sib_id )
926+ if sib_val :
927+ max_target = self ._coerce_date_value (sib_val )
928+ log .info (f"[KAAV-3492 BACKEND] Derived target for { identifier } from { sib_id } : { max_target } " )
929+
930+ # If current date creates a gap (is later than target), snap it back
931+ if max_target and current_date > max_target :
932+ log .info (f"[KAAV-3492 BACKEND] Snapping stale date { identifier } from { current_date } to { max_target } " )
933+ updated_attribute_data [identifier ] = max_target
934+
935+ # UX80.4.2.3.7: When adding element row, update phase boundaries
936+ # so cascade calculates from the correct snapped position
937+ if "_paattyy" in identifier :
938+ # Map identifier patterns to phase boundary pairs
939+ PHASE_BOUNDARY_MAP = {
940+ "oas" : ("oasvaihe_paattyy_pvm" , "ehdotusvaihe_alkaa_pvm" ),
941+ "periaatteet" : ("periaatteetvaihe_paattyy_pvm" , "oasvaihe_alkaa_pvm" ),
942+ "luonnos" : ("luonnosvaihe_paattyy_pvm" , "ehdotusvaihe_alkaa_pvm" ),
943+ }
944+ # Special case: ehdotus but not tarkistettu
945+ if "ehdotus" in identifier and "tarkistettu" not in identifier :
946+ boundaries = ("ehdotusvaihe_paattyy_pvm" , "tarkistettuehdotusvaihe_alkaa_pvm" )
947+ else :
948+ boundaries = next ((v for k , v in PHASE_BOUNDARY_MAP .items () if k in identifier ), None )
949+
950+ if boundaries :
951+ phase_end_id , next_phase_start_id = boundaries
952+ current_phase_end = self ._coerce_date_value (updated_attribute_data .get (phase_end_id ))
953+ if current_phase_end and max_target != current_phase_end :
954+ log .info (f"[KAAV-3492 BACKEND] Updating phase boundary { phase_end_id } to { max_target } " )
955+ updated_attribute_data [phase_end_id ] = max_target
956+ updated_attribute_data [next_phase_start_id ] = max_target
957+ actually_changed .update ([phase_end_id , next_phase_start_id ])
958+
889959 # This deadline's group was just re-enabled - treat its date as changed
890- actually_changed .add (dl . attribute . identifier )
891- log .info (f"[KAAV-3492 BACKEND] Marking { dl . attribute . identifier } as changed due to vis_bool { vis_bool } " )
960+ actually_changed .add (identifier )
961+ log .info (f"[KAAV-3492 BACKEND] Marking { identifier } as changed due to vis_bool { vis_bool } " )
892962
893963 log .info (f"[KAAV-3492 BACKEND] actually_changed set: { actually_changed } " )
894964
@@ -1159,6 +1229,10 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
11591229 continue
11601230 min_target = self ._min_distance_target_date (prev_date , distance , dl )
11611231 if min_target and current_date < min_target :
1232+ # KAAV-3492: Do not try to enforce confirmed fields
1233+ if confirmed_fields and identifier in confirmed_fields :
1234+ continue
1235+
11621236 log .info (f"[CONVERGENCE CHECK] { identifier } violates distance rule (prev={ prev_date } , min={ min_target } )" )
11631237 cascade_queue .add (identifier )
11641238 break
@@ -1189,6 +1263,9 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
11891263 continue
11901264 min_target = self ._min_distance_target_date (prev_date , distance , changed_dl )
11911265 if min_target and current_date < min_target :
1266+ if confirmed_fields and changed_id in confirmed_fields :
1267+ log .info (f"[CONVERGENCE ENFORCE] Skipping confirmed { changed_id } " )
1268+ continue
11921269 log .info (f"[CONVERGENCE ENFORCE] { changed_id } from { current_date } to { min_target } " )
11931270 enforced = self ._enforce_distance_requirements (changed_dl , min_target , updated_attribute_data )
11941271 if enforced and enforced != current_date :
@@ -1223,6 +1300,9 @@ def get_preview_deadlines(self, updated_attributes, subtype, confirmed_fields=No
12231300
12241301 min_target = self ._min_distance_target_date (current_date , distance , next_dl )
12251302 if min_target and next_date < min_target :
1303+ if confirmed_fields and next_id in confirmed_fields :
1304+ log .info (f"[CONVERGENCE PUSH] Skipping confirmed { next_id } " )
1305+ continue
12261306 log .info (f"[CONVERGENCE PUSH] Pushing { next_id } from { next_date } to { min_target } (because { changed_id } moved)" )
12271307 new_cascade_changes .add (next_id )
12281308 iteration_changes .add (next_id )
0 commit comments