Repository: akvo/akvo-rsr

Issue:
Bulk update status change
Need something like:

POST `/bulk-value-updates` 
```
updates: [list of ids],
status: 'A' // 'R'
```



Discussion:

Resolving patch:
diff --git a/akvo/rest/urls.py b/akvo/rest/urls.py
--- a/akvo/rest/urls.py
+++ b/akvo/rest/urls.py
@@ -181,6 +181,9 @@
     url(r'v1/set-periods-locked/(?P<project_pk>[0-9]+)/$',
         views.set_periods_locked,
         name='set_periods_locked'),
+    url(r'v1/set-updates-status/(?P<project_pk>[0-9]+)/$',
+        views.set_updates_status,
+        name='set_updates_status'),
     url(r'v1/right_now_in_akvo/$', views.right_now_in_akvo_view, name='right_now_in_akvo'),
     url(r'v1/server_info/$', views.server_info, name='server_info'),
     url(r'v1/title-and-status/(?P<project_pk>[0-9]+)/$', views.project_title, name='project_title'),
diff --git a/akvo/rest/views/__init__.py b/akvo/rest/views/__init__.py
--- a/akvo/rest/views/__init__.py
+++ b/akvo/rest/views/__init__.py
@@ -34,7 +34,7 @@
 from .indicator_period import IndicatorPeriodViewSet, IndicatorPeriodFrameworkViewSet, set_periods_locked
 from .indicator_period_data import (IndicatorPeriodDataViewSet, IndicatorPeriodDataFrameworkViewSet,
                                     IndicatorPeriodDataCommentViewSet, indicator_upload_file,
-                                    period_update_files, period_update_photos)
+                                    period_update_files, period_update_photos, set_updates_status)
 from .indicator_period_disaggregation import IndicatorPeriodDisaggregationViewSet
 from .disaggregation import DisaggregationViewSet
 from .disaggregation_target import DisaggregationTargetViewSet
@@ -154,6 +154,7 @@
     'indicator_upload_file',
     'period_update_files',
     'period_update_photos',
+    'set_updates_status',
     'InternalOrganisationIDViewSet',
     'invite_user',
     'KeywordViewSet',
diff --git a/akvo/rest/views/indicator_period_data.py b/akvo/rest/views/indicator_period_data.py
--- a/akvo/rest/views/indicator_period_data.py
+++ b/akvo/rest/views/indicator_period_data.py
@@ -5,7 +5,7 @@
 # For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
 
 
-from akvo.rsr.models import IndicatorPeriodData, IndicatorPeriodDataComment
+from akvo.rsr.models import IndicatorPeriodData, IndicatorPeriodDataComment, Project
 from akvo.rest.models import TastyTokenAuthentication, JWTAuthentication
 
 from ..serializers import (IndicatorPeriodDataSerializer, IndicatorPeriodDataFrameworkSerializer,
@@ -14,6 +14,7 @@
 
 
 from django.shortcuts import get_object_or_404
+from django.http import HttpResponseBadRequest, HttpResponseForbidden
 from rest_framework import status
 from rest_framework.authentication import SessionAuthentication
 from rest_framework.decorators import api_view, authentication_classes
@@ -172,3 +173,25 @@ def indicator_upload_file(request, pk=None):
                 return Response({'file': update.file.url})
         except Exception as e:
             return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(['POST'])
+@authentication_classes([SessionAuthentication, TastyTokenAuthentication])
+def set_updates_status(request, project_pk):
+    """Bulk update IndicatorPeriodData.status attributes of a project.
+    """
+    update_ids = request.data.get('updates', [])
+    status = request.data.get('status', None)
+    if len(update_ids) < 1 or status is None:
+        return HttpResponseBadRequest()
+
+    user = request.user
+    project = get_object_or_404(Project, pk=project_pk)
+    if not user.has_perm('rsr.change_project', project):
+        return HttpResponseForbidden()
+
+    IndicatorPeriodData.objects\
+        .filter(id__in=update_ids, period__indicator__result__project=project)\
+        .update(status=status)
+
+    return Response({'success': True})


Test patch:
diff --git a/akvo/rsr/tests/rest/test_set_updates_status.py b/akvo/rsr/tests/rest/test_set_updates_status.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/tests/rest/test_set_updates_status.py
@@ -0,0 +1,89 @@
+# -*- 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 >.
+
+import json
+from datetime import date
+
+from akvo.rsr.models import Partnership, IndicatorPeriodData
+from akvo.rsr.tests.base import BaseTestCase
+from akvo.rsr.tests.utils import ProjectFixtureBuilder
+
+
+class SetUpdatesStatusTestCase(BaseTestCase):
+    """Test REST endpoint for bulk change period update status."""
+
+    def post_request(self, project, data, username, password):
+        self.c.login(username=username, password=password)
+        return self.c.post(
+            '/rest/v1/set-updates-status/{}/'.format(project.id),
+            data=json.dumps(data),
+            content_type='application/json'
+        )
+
+    def test_incomplete_request_data(self):
+        self.create_user('test@akvo.org', 'password')
+        project = self.create_project('Test project')
+        response = self.post_request(
+            project, data={}, username='test@akvo.org', password='password')
+        self.assertEqual(400, response.status_code)
+
+    def test_should_not_be_able_to_change_status_without_having_correct_permission(self):
+        user = self.create_user('test@akvo.org', 'password')
+        project = ProjectFixtureBuilder()\
+            .with_results([{
+                'title': 'Result #1',
+                'indicators': [{
+                    'title': 'Indicator #1',
+                    'periods': [{
+                        'period_start': date(2020, 1, 1),
+                        'period_end': date(2020, 12, 31)
+                    }]
+                }]
+            }]).build()
+        update = project.get_period(period_start=date(2020, 1, 1))\
+            .add_update(user, value=10, status=IndicatorPeriodData.STATUS_PENDING_CODE)
+
+        response = self.post_request(
+            project.object,
+            data={'updates': [update.id], 'status': IndicatorPeriodData.STATUS_APPROVED_CODE},
+            username='test@akvo.org',
+            password='password'
+        )
+
+        update.refresh_from_db()
+        self.assertEqual(IndicatorPeriodData.STATUS_PENDING_CODE, update.status)
+        self.assertEqual(403, response.status_code)
+
+    def test_should_be_able_to_change_status_without_having_correct_permission(self):
+        user = self.create_user('test@akvo.org', 'password')
+        org = self.create_organisation('Acme Org')
+        self.make_employment(user, org, 'M&E Managers')
+        project = ProjectFixtureBuilder()\
+            .with_partner(org, Partnership.IATI_REPORTING_ORGANISATION)\
+            .with_results([{
+                'title': 'Result #1',
+                'indicators': [{
+                    'title': 'Indicator #1',
+                    'periods': [{
+                        'period_start': date(2020, 1, 1),
+                        'period_end': date(2020, 12, 31)
+                    }]
+                }]
+            }]).build()
+        update = project.get_period(period_start=date(2020, 1, 1))\
+            .add_update(user, value=10, status=IndicatorPeriodData.STATUS_PENDING_CODE)
+
+        response = self.post_request(
+            project.object,
+            data={'updates': [update.id], 'status': IndicatorPeriodData.STATUS_APPROVED_CODE},
+            username='test@akvo.org',
+            password='password'
+        )
+
+        update.refresh_from_db()
+        self.assertEqual(IndicatorPeriodData.STATUS_APPROVED_CODE, update.status)
+        self.assertEqual(200, response.status_code)