Repository: akvo/akvo-rsr

Issue:
Prevent deleting Results, Indicators and periods if updates are present
To prevent data loss, we should prevent users from deleting results, indicators, periods, if updates are present on them. Or at least warn them about potential loss of data. 


Discussion:

Resolving patch:
diff --git a/akvo/rsr/migrations/0158_indicator_period_data_on_delete_protect.py b/akvo/rsr/migrations/0158_indicator_period_data_on_delete_protect.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/migrations/0158_indicator_period_data_on_delete_protect.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.22 on 2019-07-12 09:05
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('rsr', '0157_partnersite_tagline'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='indicatorperioddata',
+            name='period',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='data', to='rsr.IndicatorPeriod', verbose_name='indicator period'),
+        ),
+    ]
diff --git a/akvo/rsr/models/result/indicator_period_data.py b/akvo/rsr/models/result/indicator_period_data.py
--- a/akvo/rsr/models/result/indicator_period_data.py
+++ b/akvo/rsr/models/result/indicator_period_data.py
@@ -50,7 +50,7 @@ class IndicatorPeriodData(TimestampsMixin, models.Model):
     )
 
     period = models.ForeignKey('IndicatorPeriod', verbose_name=_(u'indicator period'),
-                               related_name='data')
+                               related_name='data', on_delete=models.PROTECT)
     # TODO: rename to created_by when old results framework page is no longer in use
     user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_(u'user'), db_index=True,
                              related_name='created_period_updates')


Test patch:
diff --git a/akvo/rsr/tests/models/test_custom_field.py b/akvo/rsr/tests/models/test_custom_field.py
--- a/akvo/rsr/tests/models/test_custom_field.py
+++ b/akvo/rsr/tests/models/test_custom_field.py
@@ -13,10 +13,6 @@
 class CustomFieldTestCase(TestCase):
     """Tests for the custom field models."""
 
-    def tearDown(self):
-        Project.objects.all().delete()
-        ProjectCustomField.objects.all().delete()
-
     def test_unicode_value_custom_field(self):
         """Test that unicode value custom field doesn't raise unicode errors."""
 
diff --git a/akvo/rsr/tests/models/test_indicator.py b/akvo/rsr/tests/models/test_indicator.py
--- a/akvo/rsr/tests/models/test_indicator.py
+++ b/akvo/rsr/tests/models/test_indicator.py
@@ -6,13 +6,10 @@
 
 import datetime
 
-from unittest import TestCase
-
 from django.contrib.auth import get_user_model
 from django.core.exceptions import ValidationError
 
-from akvo.rsr.models import (Indicator, IndicatorPeriod, IndicatorPeriodData, Project, Result,
-                             RelatedProject)
+from akvo.rsr.models import Indicator, IndicatorPeriod, Project, Result, RelatedProject
 from akvo.rsr.tests.base import BaseTestCase
 from akvo.rsr.models.result.utils import QUALITATIVE
 
@@ -127,114 +124,3 @@ def test_indicator_type_update_propogates(self):
         child_indicator = parent_indicator.child_indicators.first()
         self.assertEqual(QUALITATIVE, parent_indicator.type)
         self.assertEqual(parent_indicator.type, child_indicator.type)
-
-
-class IndicatorPeriodModelTestCase(TestCase):
-    """Tests for the indicator period model"""
-
-    def setUp(self):
-        # Clear all projects, users since some tests may not tear down!
-        self.tearDown()
-
-        # Setup a project with results framework and a user
-        self.project = Project.objects.create(title="Test project 1")
-        self.result = Result.objects.create(project=self.project, title='Test Result')
-        self.indicator = Indicator.objects.create(result=self.result, title='Test Indicator')
-        self.period = IndicatorPeriod.objects.create(indicator=self.indicator,
-                                                     actual_comment='initial actual comment')
-        self.user = User.objects.create(username='user1@com.com', email='user1@com.com')
-
-    def tearDown(self):
-        Project.objects.all().delete()
-        User.objects.all().delete()
-
-    def test_unapproved_period_data_does_not_update_actual_comment(self):
-        """Unapproved IndicatorPeriodData doesn't update IndicatorPeriod.actual_comment."""
-
-        # Given
-        period = self.period
-        user = self.user
-
-        # When
-        data = IndicatorPeriodData.objects.create(text='period data comment',
-                                                  period=period,
-                                                  user=user)
-
-        # Then
-        period = IndicatorPeriod.objects.get(id=period.id)
-        self.assertNotIn(data.text, period.actual_comment)
-
-    def test_period_data_updates_actual_comment(self):
-        """Adding IndicatorPeriodData updates IndicatorPeriod.actual_comment."""
-
-        # Given
-        period = self.period
-        user = self.user
-
-        # When
-        data = IndicatorPeriodData.objects.create(text='period data comment',
-                                                  period=period,
-                                                  user=user,
-                                                  status=IndicatorPeriodData.STATUS_APPROVED_CODE)
-
-        # Then
-        period = IndicatorPeriod.objects.get(id=period.id)
-        self.assertIn(data.text, period.actual_comment)
-
-    def test_period_data_non_ascii_updates_actual_comment(self):
-        """Test that adding IndicatorPeriodData with unicode text works."""
-
-        # Given
-        period = self.period
-        user = self.user
-
-        # When
-        data = IndicatorPeriodData.objects.create(text=u'perïöd data comment',
-                                                  period=period,
-                                                  user=user,
-                                                  status=IndicatorPeriodData.STATUS_APPROVED_CODE)
-
-        # Then
-        period = IndicatorPeriod.objects.get(id=period.id)
-        self.assertIn(data.text, period.actual_comment)
-
-    def test_multiple_period_data_updates_actual_comment(self):
-        """Test that adding multiple IndicatorPeriodData updates actual_comment."""
-
-        # Given
-        period = self.period
-        user = self.user
-
-        # When
-        data_1 = IndicatorPeriodData.objects.create(text=u'perïöd data_1 comment',
-                                                    period=period,
-                                                    user=user,
-                                                    status=IndicatorPeriodData.STATUS_APPROVED_CODE)
-        data_2 = IndicatorPeriodData.objects.create(text=u'perïöd data_2 comment',
-                                                    period=period,
-                                                    user=user,
-                                                    status=IndicatorPeriodData.STATUS_APPROVED_CODE)
-
-        # Then
-        period = IndicatorPeriod.objects.get(id=period.id)
-        self.assertIn(data_2.text, period.actual_comment)
-        self.assertIn(data_1.text, period.actual_comment)
-        # newer update's text appears before older one's
-        self.assertLess(period.actual_comment.index(data_2.text), period.actual_comment.index(data_1.text))
-
-    def test_period_data_deletion_updates_actual_comment(self):
-
-        # Given
-        period = self.period
-        user = self.user
-        data = IndicatorPeriodData.objects.create(text='period data comment',
-                                                  period=period,
-                                                  user=user,
-                                                  status=IndicatorPeriodData.STATUS_APPROVED_CODE)
-
-        # When
-        data.delete()
-
-        # Then
-        period = IndicatorPeriod.objects.get(id=period.id)
-        self.assertNotIn(data.text, period.actual_comment)
diff --git a/akvo/rsr/tests/models/test_indicator_period.py b/akvo/rsr/tests/models/test_indicator_period.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/tests/models/test_indicator_period.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+
+# Akvo RSR is covered by the GNU Affero General Public License.
+
+# See more details in the license.txt file located at the root folder of the Akvo RSR module.
+# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
+
+
+from django.contrib.auth import get_user_model
+from django.db.models import ProtectedError
+
+from akvo.rsr.models import Indicator, IndicatorPeriod, IndicatorPeriodData, Project, Result
+from akvo.rsr.tests.base import BaseTestCase
+
+
+User = get_user_model()
+
+
+class IndicatorPeriodModelTestCase(BaseTestCase):
+    """Tests for the indicator period model"""
+
+    def setUp(self):
+        # Setup a project with results framework and a user
+        self.project = Project.objects.create(title="Test project 1")
+        self.result = Result.objects.create(project=self.project, title='Test Result')
+        self.indicator = Indicator.objects.create(result=self.result, title='Test Indicator')
+        self.period = IndicatorPeriod.objects.create(indicator=self.indicator,
+                                                     actual_comment='initial actual comment')
+        self.user = User.objects.create(username='user1@com.com', email='user1@com.com')
+
+    def test_unapproved_period_data_does_not_update_actual_comment(self):
+        """Unapproved IndicatorPeriodData doesn't update IndicatorPeriod.actual_comment."""
+
+        # Given
+        period = self.period
+        user = self.user
+
+        # When
+        data = IndicatorPeriodData.objects.create(text='period data comment',
+                                                  period=period,
+                                                  user=user)
+
+        # Then
+        period = IndicatorPeriod.objects.get(id=period.id)
+        self.assertNotIn(data.text, period.actual_comment)
+
+    def test_period_data_updates_actual_comment(self):
+        """Adding IndicatorPeriodData updates IndicatorPeriod.actual_comment."""
+
+        # Given
+        period = self.period
+        user = self.user
+
+        # When
+        data = IndicatorPeriodData.objects.create(text='period data comment',
+                                                  period=period,
+                                                  user=user,
+                                                  status=IndicatorPeriodData.STATUS_APPROVED_CODE)
+
+        # Then
+        period = IndicatorPeriod.objects.get(id=period.id)
+        self.assertIn(data.text, period.actual_comment)
+
+    def test_period_data_non_ascii_updates_actual_comment(self):
+        """Test that adding IndicatorPeriodData with unicode text works."""
+
+        # Given
+        period = self.period
+        user = self.user
+
+        # When
+        data = IndicatorPeriodData.objects.create(text=u'perïöd data comment',
+                                                  period=period,
+                                                  user=user,
+                                                  status=IndicatorPeriodData.STATUS_APPROVED_CODE)
+
+        # Then
+        period = IndicatorPeriod.objects.get(id=period.id)
+        self.assertIn(data.text, period.actual_comment)
+
+    def test_multiple_period_data_updates_actual_comment(self):
+        """Test that adding multiple IndicatorPeriodData updates actual_comment."""
+
+        # Given
+        period = self.period
+        user = self.user
+
+        # When
+        data_1 = IndicatorPeriodData.objects.create(text=u'perïöd data_1 comment',
+                                                    period=period,
+                                                    user=user,
+                                                    status=IndicatorPeriodData.STATUS_APPROVED_CODE)
+        data_2 = IndicatorPeriodData.objects.create(text=u'perïöd data_2 comment',
+                                                    period=period,
+                                                    user=user,
+                                                    status=IndicatorPeriodData.STATUS_APPROVED_CODE)
+
+        # Then
+        period = IndicatorPeriod.objects.get(id=period.id)
+        self.assertIn(data_2.text, period.actual_comment)
+        self.assertIn(data_1.text, period.actual_comment)
+        # newer update's text appears before older one's
+        self.assertLess(period.actual_comment.index(data_2.text), period.actual_comment.index(data_1.text))
+
+    def test_period_data_deletion_updates_actual_comment(self):
+
+        # Given
+        period = self.period
+        user = self.user
+        data = IndicatorPeriodData.objects.create(text='period data comment',
+                                                  period=period,
+                                                  user=user,
+                                                  status=IndicatorPeriodData.STATUS_APPROVED_CODE)
+
+        # When
+        data.delete()
+
+        # Then
+        period = IndicatorPeriod.objects.get(id=period.id)
+        self.assertNotIn(data.text, period.actual_comment)
+
+    def test_indicator_period_data_prevents_deletion(self):
+        # Given
+        period = self.period
+        user = self.user
+        IndicatorPeriodData.objects.create(text='period data comment',
+                                           period=period,
+                                           user=user,
+                                           status=IndicatorPeriodData.STATUS_APPROVED_CODE)
+
+        # When/Then
+        with self.assertRaises(ProtectedError):
+            period.delete()
+        IndicatorPeriod.objects.get(pk=period.pk)
+
+        with self.assertRaises(ProtectedError):
+            self.indicator.delete()
+        Indicator.objects.get(pk=self.indicator.pk)
+
+        with self.assertRaises(ProtectedError):
+            self.result.delete()
+        Result.objects.get(pk=self.result.pk)
+
+        with self.assertRaises(ProtectedError):
+            self.project.delete()
+        Project.objects.get(pk=self.project.pk)
diff --git a/akvo/rsr/tests/models/test_results_aggregation.py b/akvo/rsr/tests/models/test_results_aggregation.py
--- a/akvo/rsr/tests/models/test_results_aggregation.py
+++ b/akvo/rsr/tests/models/test_results_aggregation.py
@@ -24,23 +24,19 @@ class PeriodActualValueTestCase(TestCase):
     """Tests for the indicator period model"""
 
     def setUp(self):
-        # Clear all projects, users since some tests may not tear down!
-        self.tearDown()
-
         # Setup a project with results framework and a user
         self.project = Project.objects.create(title="Test project 1")
         self.result = Result.objects.create(project=self.project, title='Test Result')
         self.indicator = Indicator.objects.create(result=self.result, title='Test Indicator')
         self.period = IndicatorPeriod.objects.create(indicator=self.indicator,
                                                      actual_comment='initial actual comment')
-        self.user = User.objects.create(username='user1@com.com', email='user1@com.com')
+        self.user, _ = User.objects.get_or_create(username='user1@com.com', email='user1@com.com')
 
         #  set up PG views
         vs = ViewSyncer()
         vs.run(True, True)
 
     def tearDown(self):
-        Project.objects.all().delete()
         User.objects.all().delete()
 
     def test_actual_value_with_two_approved_updates(self):
@@ -113,9 +109,6 @@ class PeriodDisaggregationTestCase(TestCase):
     """Tests for the indicator period model"""
 
     def setUp(self):
-        # Clear all projects, users since some tests may not tear down!
-        self.tearDown()
-
         # Setup a project with results framework and a user
         self.user = User.objects.create(username='user1@com.com', email='user1@com.com')
         self.project = Project.objects.create(title="Test project 1")
@@ -165,7 +158,6 @@ def setUp(self):
         vs.run(True, True)
 
     def tearDown(self):
-        Project.objects.all().delete()
         User.objects.all().delete()
 
     def test_disaggregated_values_aggregated_over_period(self):
diff --git a/akvo/rsr/tests/rest/test_indicator_period_data.py b/akvo/rsr/tests/rest/test_indicator_period_data.py
--- a/akvo/rsr/tests/rest/test_indicator_period_data.py
+++ b/akvo/rsr/tests/rest/test_indicator_period_data.py
@@ -74,12 +74,6 @@ def setUp(self):
         self.c = Client(HTTP_HOST=settings.RSR_DOMAIN)
         self.setup_results_framework()
 
-    def tearDown(self):
-        Project.objects.all().delete()
-        User.objects.all().delete()
-        Organisation.objects.all().delete()
-        Group.objects.all().delete()
-
     def _create_new_user(self, group, is_admin=False, is_superuser=False):
         self.username2 = "username2"
         self.password2 = "password2"