diff --git a/dashboard/framework/models.py b/dashboard/framework/models.py index f2741cc..0c09bf5 100644 --- a/dashboard/framework/models.py +++ b/dashboard/framework/models.py @@ -166,18 +166,45 @@ def save(self, *args, **kwargs): from projects.models import ( ProjectObjective, ProjectObjectiveCondition, + Commitment ) # avoids circular import - super().save(*args, **kwargs) + # Check if the objective is being changed for an existing condition + old_objective_id = None + if self.pk: + try: + old_objective_id = Condition.objects.values_list( + "objective_id", flat=True + ).get(pk=self.pk) + except Condition.DoesNotExist: + pass - projectobjectives = ProjectObjective.objects.filter(objective=self.objective) + super().save(*args, **kwargs) - for projectobjective in projectobjectives: - ProjectObjectiveCondition.objects.get_or_create( - project=projectobjective.project, - objective=projectobjective.objective, - condition=self, - ) + if old_objective_id is not None and old_objective_id != self.objective_id: + # Condition moved to a new objective: update existing POCs to the new + # objective instead of creating new ones, so that status is preserved + ProjectObjectiveCondition.objects.filter( + condition=self, objective_id=old_objective_id + ).update(objective=self.objective) + else: + # New condition or objective unchanged: propagate to all existing + # ProjectObjectives + projectobjectives = ProjectObjective.objects.filter(objective=self.objective) + + for projectobjective in projectobjectives: + ProjectObjectiveCondition.objects.get_or_create( + project=projectobjective.project, + objective=projectobjective.objective, + condition=self, + ) + for work_cycle in WorkCycle.objects.all(): + Commitment.objects.get_or_create( + work_cycle=work_cycle, + project=projectobjective.project, + objective=projectobjective.objective, + level=self.level, + ) class Meta: ordering = ["objective__name", "level__value"] diff --git a/dashboard/framework/test_models.py b/dashboard/framework/test_models.py index 0647fff..167b0ea 100644 --- a/dashboard/framework/test_models.py +++ b/dashboard/framework/test_models.py @@ -153,3 +153,73 @@ def test_new_objective_means_new_commitments( assert project.commitment_set.count() == 2 assert work_cycle.commitment_set.count() == 2 + + +@pytest.mark.django_db +def test_new_condition_with_new_level_backfills_commitment( + project, objective, condition, work_cycle +): + # A new condition at a new level should create a matching commitment + # for existing rows. + + assert ( + Commitment.objects.filter( + project=project, objective=objective, work_cycle=work_cycle + ).count() + == 1 + ) + + new_level = Level.objects.create(name="test_level_2", value=2) + Condition.objects.create( + name="test_condition_2", objective=objective, level=new_level + ) + + # Expected behaviour: creating a new condition/level backfills + # commitments. + assert Commitment.objects.filter( + project=project, + objective=objective, + work_cycle=work_cycle, + level=new_level, + ).exists() + + +@pytest.mark.django_db +def test_moving_condition_updates_poc_not_creates_duplicate( + project, objective, objective_group, level +): + """Moving a condition to a different objective should update the existing POC, + preserving the status, rather than creating a duplicate POC.""" + + objective_b = Objective.objects.create( + name="test_objective_b", group=objective_group, weight=1 + ) + + condition = Condition.objects.create( + name="test_condition", level=level, objective=objective + ) + + # Set the POC's status to "DO" before moving + poc = ProjectObjectiveCondition.objects.get( + project=project, objective=objective, condition=condition + ) + poc.status = "DO" + poc.save() + + # Move the condition to objective_b + condition.objective = objective_b + condition.save() + + # There should be exactly one POC for this condition across all projects + all_pocs = ProjectObjectiveCondition.objects.filter(condition=condition) + assert all_pocs.count() == 1, ( + f"Expected 1 POC but got {all_pocs.count()}. " + "Moving a condition should update the existing POC, not create a duplicate." + ) + + # The POC should point to objective_b + updated_poc = all_pocs.first() + assert updated_poc.objective == objective_b + + # The POC should retain its completion status + assert updated_poc.status == "DO"