Repository: akvo/akvo-rsr

Issue:
Create new Results and DimensionGroups  & Labels in child projects automatically



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
@@ -1208,6 +1208,10 @@ def keyword_logos(self):
     def keyword_labels(self):
         return [keyword.label for keyword in self.keywords.all()]
 
+    def has_imported_results(self):
+        Result = apps.get_model('rsr', 'Result')
+        return Result.objects.filter(project=self).exclude(parent_result=None).count() > 0
+
     ###################################
     # RSR Impact projects #############
     ###################################
@@ -1217,6 +1221,9 @@ def import_results(self):
         import_failed = 0
         import_success = 1
 
+        if self.has_imported_results():
+            return import_failed, 'Project has already imported results'
+
         if self.parents_all().count() == 1:
             parent_project = self.parents_all()[0]
         elif self.parents_all().count() == 0:
@@ -1298,6 +1305,14 @@ def copy_results(self, source_project):
         for result in source_project.results.all():
             self.copy_result(result, set_parent=False)
 
+    def copy_dimension_name_to_children(self, dimension_name):
+        """Copy dimension_name to all children that imported from this project."""
+
+        for child in self.children_all():
+            if not child.has_imported_results():
+                continue
+            child.copy_dimension_name(dimension_name, set_parent=True)
+
     def copy_dimension_name(self, source_dimension_name, set_parent=True):
         defaults = dict(parent_dimension_name=source_dimension_name)
         data = dict(project=self, name=source_dimension_name.name, defaults=defaults)
@@ -1329,6 +1344,14 @@ def copy_dimension_value(self, dimension_name, source_dimension_value, set_paren
             dimension_value.parent_dimension_value = source_dimension_value
             dimension_value.save(update_fields=['parent_dimension_value'])
 
+    def copy_result_to_children(self, result):
+        """Copy result to all children that imported results from this project."""
+
+        for child in self.children_all():
+            if not child.has_imported_results():
+                continue
+            child.copy_result(result, set_parent=True)
+
     def copy_result(self, source_result, set_parent=True):
         """Copy the source_result to this project, setting it as parent if specified."""
         data = dict(
diff --git a/akvo/rsr/models/result/indicator_dimension.py b/akvo/rsr/models/result/indicator_dimension.py
--- a/akvo/rsr/models/result/indicator_dimension.py
+++ b/akvo/rsr/models/result/indicator_dimension.py
@@ -40,7 +40,12 @@ def __unicode__(self):
         return self.name
 
     def save(self, *args, **kwargs):
+        is_new_dimension_name = not self.pk
         super(IndicatorDimensionName, self).save(*args, **kwargs)
+
+        if is_new_dimension_name:
+            self.project.copy_dimension_name_to_children(self)
+
         for child_dimension_name in self.child_dimension_names.all():
             child_dimension_name.name = self.name
             child_dimension_name.save()
diff --git a/akvo/rsr/models/result/result.py b/akvo/rsr/models/result/result.py
--- a/akvo/rsr/models/result/result.py
+++ b/akvo/rsr/models/result/result.py
@@ -61,6 +61,9 @@ def __unicode__(self):
 
     def save(self, *args, **kwargs):
         """Update the values of child results, if a parent result is updated."""
+
+        is_new_result = not self.pk
+
         for child_result in self.child_results.all():
             # Always copy title, type and aggregation status. They should be the same as the parent.
             child_result.title = self.title
@@ -73,13 +76,16 @@ def save(self, *args, **kwargs):
 
             child_result.save()
 
-        if not self.pk and Result.objects.filter(project_id=self.project.id).exists():
+        if is_new_result and Result.objects.filter(project_id=self.project.id).exists():
             prev_result = Result.objects.filter(project_id=self.project.id).reverse()[0]
             if prev_result.order:
                 self.order = prev_result.order + 1
 
         super(Result, self).save(*args, **kwargs)
 
+        if is_new_result:
+            self.project.copy_result_to_children(self)
+
     def clean(self):
         validation_errors = {}
 


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
@@ -100,6 +100,26 @@ def test_import_in_child(self):
             self.indicator.dimension_names.count()
         )
 
+    def test_new_result_cloned_to_child(self):
+        """Test that new results are cloned in children that have imported results."""
+        # Given
+        self.assertEqual(self.import_status, 1)
+        self.assertEqual(self.import_message, "Results imported")
+
+        # When
+        result = Result.objects.create(project=self.parent_project)
+
+        # Then
+        self.assertEqual(
+            self.parent_project.results.count(),
+            self.child_project.results.count(),
+        )
+        child_result = Result.objects.get(project=self.child_project, parent_result=result)
+        self.assertEqual(child_result.title, result.title)
+        self.assertEqual(child_result.type, result.type)
+        self.assertEqual(child_result.aggregation_status, result.aggregation_status)
+        self.assertEqual(child_result.description, result.description)
+
     def test_new_indicator_cloned_to_child(self):
         """Test that new indicators are cloned in children that have imported results."""
         # Given
@@ -267,29 +287,6 @@ def test_child_period_state_not_overwritten_after_change(self):
         self.assertEqual(child_period.target_comment, comment)
         self.assertEqual(child_period.actual_comment, comment)
 
-    def test_import_does_not_create_deleted_indicators(self):
-        """Test that import does not create indicators that have been deleted from child."""
-        # Given
-        title = 'Indicator #2'
-        result = self.parent_project.results.first()
-        child_result = result.child_results.first()
-        # New indicator created (also cloned to child)
-        Indicator.objects.create(result=result, title=title, measure='1')
-
-        # When
-        # Import results framework into child
-        child_result.indicators.get(title=title).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,
-            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
@@ -311,24 +308,6 @@ def test_indicator_update_does_not_create_deleted_indicators(self):
         # 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
@@ -351,6 +330,7 @@ def test_period_update_does_not_create_deleted_periods(self):
 
     def test_manually_created_dimension_names_should_not_break_import(self):
         # Given
+        self.child_project.results.all().delete()
         parent_dimension_name = IndicatorDimensionName.objects.create(
             name='Colour', project=self.parent_project)
         child_dimension_name = IndicatorDimensionName.objects.create(
@@ -381,6 +361,24 @@ def test_child_dimension_name_state_updates_after_change(self):
         child_dimension_name = self.child_project.dimension_names.first()
         self.assertEqual(child_dimension_name.name, parent_dimension_name.name)
 
+    def test_new_dimension_name_cloned_to_child(self):
+        """Test that new results are cloned in children that have imported results."""
+        # Given
+        self.assertEqual(self.import_status, 1)
+        self.assertEqual(self.import_message, "Results imported")
+
+        # When
+        name = IndicatorDimensionName.objects.create(project=self.parent_project)
+
+        # Then
+        self.assertEqual(
+            self.parent_project.dimension_names.count(),
+            self.child_project.dimension_names.count(),
+        )
+        child_name = IndicatorDimensionName.objects.get(project=self.child_project,
+                                                        parent_dimension_name=name)
+        self.assertEqual(name.name, child_name.name)
+
     def test_new_dimension_value_cloned_to_child(self):
         """Test that new dimension values are cloned in children that have imported results."""
         # Given