Repository: akvo/akvo-rsr

Issue:
support saving nested comments at indicator_period_data_framework endpoint



Discussion:

Resolving patch:
diff --git a/akvo/rest/serializers/indicator_period_data.py b/akvo/rest/serializers/indicator_period_data.py
--- a/akvo/rest/serializers/indicator_period_data.py
+++ b/akvo/rest/serializers/indicator_period_data.py
@@ -26,6 +26,15 @@ class Meta:
         read_only_fields = ['user']
 
 
+class IndicatorPeriodDataCommentNestedSerializer(BaseRSRSerializer):
+    id = serializers.IntegerField(required=False)
+
+    class Meta:
+        model = IndicatorPeriodDataComment
+        fields = '__all__'
+        read_only_fields = ('id', 'data', 'user')
+
+
 class IndicatorPeriodDataFileSerializer(BaseRSRSerializer):
     class Meta:
         model = IndicatorPeriodDataFile
@@ -79,7 +88,7 @@ class Meta:
 class IndicatorPeriodDataFrameworkSerializer(BaseRSRSerializer):
 
     period = serializers.PrimaryKeyRelatedField(queryset=IndicatorPeriod.objects.all())
-    comments = IndicatorPeriodDataCommentSerializer(read_only=True, many=True, required=False)
+    comments = IndicatorPeriodDataCommentNestedSerializer(many=True, required=False)
     disaggregations = DisaggregationSerializer(many=True, required=False)
     user_details = UserDetailsSerializer(read_only=True, source='user')
     approver_details = UserDetailsSerializer(read_only=True, source='approved_by')
@@ -107,6 +116,7 @@ def create(self, validated_data):
         """Over-ridden to handle nested writes."""
         files = validated_data.pop('files', [])
         photos = validated_data.pop('photos', [])
+        comments = validated_data.pop('comments', [])
         update = super(IndicatorPeriodDataFrameworkSerializer, self).create(validated_data)
         for disaggregation in self._disaggregations_data:
             disaggregation['update'] = update.id
@@ -119,6 +129,8 @@ def create(self, validated_data):
             IndicatorPeriodDataFile.objects.create(update=update, file=file)
         for photo in photos:
             IndicatorPeriodDataPhoto.objects.create(update=update, photo=photo)
+        for comment in comments:
+            IndicatorPeriodDataComment.objects.create(data=update, user=update.user, comment=comment['comment'])
 
         return update
 
@@ -133,6 +145,7 @@ def update(self, instance, validated_data):
         """Over-ridden to handle nested updates."""
         files = validated_data.pop('files', [])
         photos = validated_data.pop('photos', [])
+        comments = validated_data.pop('comments', [])
         super(IndicatorPeriodDataFrameworkSerializer, self).update(instance, validated_data)
         for disaggregation in self._disaggregations_data:
             disaggregation['update'] = instance.id
@@ -147,6 +160,18 @@ def update(self, instance, validated_data):
             IndicatorPeriodDataFile.objects.create(update=instance, file=file)
         for photo in photos:
             IndicatorPeriodDataPhoto.objects.create(update=instance, photo=photo)
+        for comment in comments:
+            comment_id = int(comment.get('id', 0))
+            comment_txt = str(comment.get('comment', ''))
+            if not comment_id:
+                IndicatorPeriodDataComment.objects.create(data=instance, user=instance.user, comment=comment['comment'])
+            else:
+                comment_obj = IndicatorPeriodDataComment.objects.get(id=comment_id)
+                if not comment_txt:
+                    comment_obj.delete()
+                else:
+                    comment_obj.comment = comment_txt
+                    comment_obj.save()
 
         return instance._meta.model.objects.select_related(
             'period',


Test patch:
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
@@ -15,7 +15,8 @@
 from akvo.rsr.models import (
     Partnership, Result, Indicator, IndicatorPeriod,
     IndicatorDimensionName, IndicatorDimensionValue,
-    IndicatorPeriodData, IndicatorPeriodDataFile, IndicatorPeriodDataPhoto
+    IndicatorPeriodData, IndicatorPeriodDataFile, IndicatorPeriodDataPhoto,
+    IndicatorPeriodDataComment,
 )
 from akvo.rsr.models.result.utils import QUANTITATIVE, PERCENTAGE_MEASURE
 from akvo.rsr.tests.base import BaseTestCase
@@ -752,6 +753,136 @@ def test_percentage_type_indicator_edit_update_invalid_numerator_case(self):
         self.assertEqual(400, response.status_code)
 
 
+class IndicatorPeriodDataWithCommentsTestCase(BaseTestCase):
+
+    def test_post_update_with_comment(self):
+        # Given
+        username, password = 'test@akvo.org', 'password'
+        org, _ = create_org_user(username, password)
+        project = ProjectFixtureBuilder()\
+            .with_partner(org, Partnership.IATI_REPORTING_ORGANISATION)\
+            .with_results([{
+                'title': 'Result #1',
+                'indicators': [{
+                    'title': 'Indicator #1',
+                    'periods': [{
+                        'period_start': date(2010, 1, 1),
+                        'period_end': date(2010, 12, 31),
+                    }]
+                }]
+            }]).build()
+        period = project.get_period(period_start=date(2010, 1, 1))
+
+        # When
+        self.c.login(username=username, password=password)
+        url = '/rest/v1/indicator_period_data_framework/?format=json'
+        data = {
+            'period': period.id,
+            'comments': [{'comment': 'Test comment'}],
+        }
+        response = self.c.post(url, json.dumps(data), content_type='application/json')
+
+        # Then
+        self.assertEqual(201, response.status_code)
+        self.assertEqual(1, IndicatorPeriodDataComment.objects.count())
+
+    def test_patch_update_with_new_comment(self):
+        # Given
+        username, password = 'test@akvo.org', 'password'
+        org, user = create_org_user(username, password)
+        project = ProjectFixtureBuilder()\
+            .with_partner(org, Partnership.IATI_REPORTING_ORGANISATION)\
+            .with_results([{
+                'title': 'Result #1',
+                'indicators': [{
+                    'title': 'Indicator #1',
+                    'periods': [{
+                        'period_start': date(2010, 1, 1),
+                        'period_end': date(2010, 12, 31),
+                    }]
+                }]
+            }]).build()
+        period = project.get_period(period_start=date(2010, 1, 1))
+        update = period.add_update(user)
+
+        # When
+        self.c.login(username=username, password=password)
+        url = '/rest/v1/indicator_period_data_framework/{}/?format=json'
+        data = {
+            'comments': [{'comment': 'Test comment'}]
+        }
+        response = self.c.patch(url.format(update.id), data=json.dumps(data), content_type='application/json')
+
+        # Then
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(1, IndicatorPeriodDataComment.objects.count())
+
+    def test_patch_update_overwrites_comment(self):
+        # Given
+        username, password = 'test@akvo.org', 'password'
+        org, user = create_org_user(username, password)
+        project = ProjectFixtureBuilder()\
+            .with_partner(org, Partnership.IATI_REPORTING_ORGANISATION)\
+            .with_results([{
+                'title': 'Result #1',
+                'indicators': [{
+                    'title': 'Indicator #1',
+                    'periods': [{
+                        'period_start': date(2010, 1, 1),
+                        'period_end': date(2010, 12, 31),
+                    }]
+                }]
+            }]).build()
+        period = project.get_period(period_start=date(2010, 1, 1))
+        update = period.add_update(user)
+        comment = update.comments.create(user=user, comment='Initial comment')
+
+        # When
+        self.c.login(username=username, password=password)
+        url = '/rest/v1/indicator_period_data_framework/{}/?format=json'
+        data = {
+            'comments': [{'id': comment.id, 'comment': 'Adjusted comment'}]
+        }
+        response = self.c.patch(url.format(update.id), data=json.dumps(data), content_type='application/json')
+
+        # Then
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(1, IndicatorPeriodDataComment.objects.count())
+        self.assertEqual('Adjusted comment', IndicatorPeriodDataComment.objects.first().comment)
+
+    def test_patch_update_with_empty_comment(self):
+        # Given
+        username, password = 'test@akvo.org', 'password'
+        org, user = create_org_user(username, password)
+        project = ProjectFixtureBuilder()\
+            .with_partner(org, Partnership.IATI_REPORTING_ORGANISATION)\
+            .with_results([{
+                'title': 'Result #1',
+                'indicators': [{
+                    'title': 'Indicator #1',
+                    'periods': [{
+                        'period_start': date(2010, 1, 1),
+                        'period_end': date(2010, 12, 31),
+                    }]
+                }]
+            }]).build()
+        period = project.get_period(period_start=date(2010, 1, 1))
+        update = period.add_update(user)
+        comment = update.comments.create(user=user, comment='Initial comment')
+
+        # When
+        self.c.login(username=username, password=password)
+        url = '/rest/v1/indicator_period_data_framework/{}/?format=json'
+        data = {
+            'comments': [{'id': comment.id, 'comment': ''}]
+        }
+        response = self.c.patch(url.format(update.id), data=json.dumps(data), content_type='application/json')
+
+        # Then
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(0, IndicatorPeriodDataComment.objects.count())
+
+
 class IndicatorPeriodDataAttachmentsTestCase(BaseTestCase):
 
     def test_create_update_with_multiple_photo_and_file(self):