Repository: akvo/akvo-rsr

Issue:
Update of indicator creates it in child
EUTF reports that when one of their "core" indicators, in the result named **EUTF indicators**, was updated, was updated, the indicator appeared in all child projects that had imported the framework, _even in those where the indicator had previously been deleted_.

This is the wrong behaviour I think, and it's also surprising. We even have a test that makes sure deleted indicators are not re-instated, but that test is only for when _re-importing_ a framework, not when a parent indicator is "merely" updated.

After a quick look at the code I think that Indicator.save() needs check if the indicator is new and only then run the loop calling child_result.project.add_indicator(child_result, self) [here](https://github.com/akvo/akvo-rsr/blob/develop/akvo/rsr/models/result/indicator.py#L107). this is just a quick assessment, and I may well be wrong in part in in all 😉 




Discussion:

Resolving patch:
diff --git a/akvo/rsr/models/project.py b/akvo/rsr/models/project.py
--- a/akvo/rsr/models/project.py
+++ b/akvo/rsr/models/project.py
@@ -1408,6 +1408,26 @@ def add_indicator(self, result, indicator):
         for dimension in indicator.dimensions.all():
             self.add_dimension(child_indicator, dimension)
 
+    def update_indicator(self, result, parent_indicator):
+        """Update an indicator based on parent indicator attributes."""
+        Indicator = get_model('rsr', 'Indicator')
+        try:
+            child_indicator = Indicator.objects.get(
+                result=result,
+                parent_indicator=parent_indicator,
+            )
+
+        except Indicator.DoesNotExist:
+            return
+
+        child_indicator.title = parent_indicator.title
+        child_indicator.measure = parent_indicator.measure
+        child_indicator.ascending = parent_indicator.ascending
+        child_indicator.save()
+
+        fields = ['title', 'description', 'baseline_year', 'baseline_value', 'baseline_comment']
+        self._update_fields_if_not_child_updated(parent_indicator, child_indicator, fields)
+
     def add_period(self, indicator, period):
         """Add a new period to the indicator as a child of period.
 
@@ -1430,6 +1450,29 @@ def add_period(self, indicator, period):
         fields = ['target_value', 'target_comment', 'actual_comment']
         self._update_fields_if_not_child_updated(period, child_period, fields)
 
+    def update_period(self, indicator, parent_period):
+        """Update a period based on the parent period attributes."""
+
+        IndicatorPeriod = get_model('rsr', 'IndicatorPeriod')
+        try:
+            child_period = IndicatorPeriod.objects.select_related(
+                'indicator',
+                'indicator__result',
+            ).get(
+                indicator=indicator,
+                parent_period=parent_period,
+            )
+
+        except IndicatorPeriod.DoesNotExist:
+            return
+
+        child_period.period_start = parent_period.period_start
+        child_period.period_end = parent_period.period_end
+        child_period.save()
+
+        fields = ['target_value', 'target_comment', 'actual_comment']
+        self._update_fields_if_not_child_updated(parent_period, child_period, fields)
+
     def add_dimension(self, indicator, dimension):
         get_model('rsr', 'IndicatorDimension').objects.create(
             indicator=indicator,
diff --git a/akvo/rsr/models/result/indicator.py b/akvo/rsr/models/result/indicator.py
--- a/akvo/rsr/models/result/indicator.py
+++ b/akvo/rsr/models/result/indicator.py
@@ -92,19 +92,24 @@ def __unicode__(self):
     def save(self, *args, **kwargs):
         """Update the values of child indicators, if a parent indicator is updated."""
 
-        if not self.pk and Indicator.objects.filter(result_id=self.result.id).exists():
+        new_indicator = not self.pk
+
+        if new_indicator and Indicator.objects.filter(result_id=self.result.id).exists():
             prev_indicator = Indicator.objects.filter(result_id=self.result.id).reverse()[0]
             if prev_indicator.order:
                 self.order = prev_indicator.order + 1
 
         # HACK: Delete IndicatorLabels on non-qualitative indicators
-        if self.pk and self.type != QUALITATIVE:
+        if new_indicator and self.type != QUALITATIVE:
             IndicatorLabel.objects.filter(indicator=self).delete()
 
         super(Indicator, self).save(*args, **kwargs)
 
         for child_result in self.result.child_results.all():
-            child_result.project.add_indicator(child_result, self)
+            if new_indicator:
+                child_result.project.add_indicator(child_result, self)
+            else:
+                child_result.project.update_indicator(child_result, self)
 
     def clean(self):
         validation_errors = {}
diff --git a/akvo/rsr/models/result/indicator_period.py b/akvo/rsr/models/result/indicator_period.py
--- a/akvo/rsr/models/result/indicator_period.py
+++ b/akvo/rsr/models/result/indicator_period.py
@@ -85,6 +85,7 @@ def __unicode__(self):
 
     def save(self, *args, **kwargs):
         actual_value_changed = False
+        new_period = not self.pk
 
         if (
             self.indicator.measure == PERCENTAGE_MEASURE and
@@ -94,7 +95,7 @@ def save(self, *args, **kwargs):
             percentage = calculate_percentage(self.numerator, self.denominator)
             self.actual_value = str(percentage)
 
-        if self.pk:
+        if not new_period:
             # Check if the actual value has changed
             orig_period = IndicatorPeriod.objects.get(pk=self.pk)
             if orig_period.actual_value != self.actual_value:
@@ -108,7 +109,10 @@ def save(self, *args, **kwargs):
         )
 
         for child_indicator in child_indicators.all():
-            child_indicator.result.project.add_period(child_indicator, self)
+            if new_period:
+                child_indicator.result.project.add_period(child_indicator, self)
+            else:
+                child_indicator.result.project.update_period(child_indicator, self)
 
         # If the actual value has changed, the period has a parent period and aggregations are on,
         # then the the parent should be updated as well


Test patch:
diff --git a/akvo/rsr/tests/results_framework/test_results_framework.py b/akvo/rsr/tests/results_framework/test_results_framework.py
--- a/akvo/rsr/tests/results_framework/test_results_framework.py
+++ b/akvo/rsr/tests/results_framework/test_results_framework.py
@@ -142,7 +142,7 @@ def test_child_indicator_state_updates_after_change(self):
         """Test that updating indicator propagates to children."""
 
         # Given
-        self.indicator.tile = "Indicator #200"
+        self.indicator.title = "Indicator #200"
         self.indicator.measure = "2"
         # # these properties were not set already, and hence should be updated too
         self.indicator.baseline_year = 2010
@@ -291,6 +291,65 @@ def test_import_does_not_create_deleted_indicators(self):
             Indicator.objects.filter(result__project=self.child_project).count()
         )
 
+    def test_indicator_update_does_not_create_deleted_indicators(self):
+        """Test that indicator update doesn't create indicators deleted from child."""
+        # Given
+        title = 'Indicator #2'
+        result = self.parent_project.results.first()
+        # New indicator created (also cloned to child)
+        indicator = Indicator.objects.create(result=result, title=title, measure='1')
+        # Assert indicator has been cloned to child
+        self.assertEqual(indicator.child_indicators.count(), 1)
+
+        # When
+        # Delete the child indicator
+        indicator.child_indicators.first().delete()
+        self.assertEqual(0, indicator.child_indicators.count())
+        # Modify the parent indicator
+        indicator.title = 'Indicator number two'
+        indicator.save()
+
+        # Then
+        self.assertEqual(0, indicator.child_indicators.count())
+
+    def test_import_does_not_create_deleted_periods(self):
+        """Test that import does not create periods deleted from child."""
+        # Given
+        indicator = self.indicator
+        child_indicator = indicator.child_indicators.first()
+        # New indicator period created (also cloned to child)
+        IndicatorPeriod.objects.create(indicator=indicator)
+
+        # When
+        # Import results framework into child
+        child_indicator.periods.last().delete()
+        import_status, import_message = self.child_project.import_results()
+
+        # Then
+        self.assertEqual(import_status, 1)
+        self.assertEqual(import_message, "Results imported")
+        self.assertEqual(1, child_indicator.periods.count())
+
+    def test_period_update_does_not_create_deleted_periods(self):
+        """Test that period update does not create periods deleted from child."""
+        # Given
+        # result = self.parent_project.results.first()
+        # child_result = result.child_results.first()
+        indicator = self.indicator
+        child_indicator = self.indicator.child_indicators.first()
+        # New indicator period created (also cloned to child)
+        indicator_period = IndicatorPeriod.objects.create(indicator=indicator)
+
+        # When
+        # Import results framework into child
+        child_indicator.periods.last().delete()
+        # Update indicator period
+        indicator_period.target_value = 100
+        indicator_period.save()
+
+        # Then
+        self.assertEqual(1, child_indicator.periods.count())
+
     def test_update(self):
         """
         Test if placing updates will update the actual value of the period.