We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Collaborate draft update bug


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `akvo/rest/views/indicator_period_data.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 
3 # Akvo RSR is covered by the GNU Affero General Public License.
4 # See more details in the license.txt file located at the root folder of the Akvo RSR module.
5 # For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
6 
7 import json
8 import os
9 
10 from akvo.rsr.models import IndicatorPeriodData, IndicatorPeriodDataComment, Project
11 from akvo.rest.models import TastyTokenAuthentication, JWTAuthentication
12 from akvo.rsr.models.result.utils import QUANTITATIVE, PERCENTAGE_MEASURE
13 
14 from ..serializers import (IndicatorPeriodDataSerializer, IndicatorPeriodDataFrameworkSerializer,
15                            IndicatorPeriodDataCommentSerializer)
16 from ..viewsets import PublicProjectViewSet
17 
18 from django.shortcuts import get_object_or_404
19 from django.http import HttpResponseBadRequest, HttpResponseForbidden
20 from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
21 from django.contrib.contenttypes.models import ContentType
22 from rest_framework import status
23 from rest_framework.authentication import SessionAuthentication
24 from rest_framework.decorators import api_view, authentication_classes
25 from rest_framework.response import Response
26 from rest_framework.utils.encoders import JSONEncoder
27 
28 
29 class IndicatorPeriodDataViewSet(PublicProjectViewSet):
30     """
31     """
32     queryset = IndicatorPeriodData.objects.select_related('user', 'approved_by').all()
33     serializer_class = IndicatorPeriodDataSerializer
34 
35     project_relation = 'period__indicator__result__project__'
36 
37     def filter_queryset(self, queryset):
38         queryset = super(IndicatorPeriodDataViewSet, self).filter_queryset(queryset)
39         return IndicatorPeriodData.get_user_viewable_updates(
40             queryset, self.request.user
41         )
42 
43     def perform_create(self, serializer):
44         serializer.save(user=self.request.user)
45 
46 
47 class IndicatorPeriodDataFrameworkViewSet(PublicProjectViewSet):
48     """
49     """
50     authentication_classes = (SessionAuthentication, TastyTokenAuthentication, JWTAuthentication)
51 
52     queryset = IndicatorPeriodData.objects.select_related(
53         'period',
54         'user',
55         'approved_by',
56     ).prefetch_related(
57         'comments',
58         'disaggregations',
59     ).all()
60     serializer_class = IndicatorPeriodDataFrameworkSerializer
61     project_relation = 'period__indicator__result__project__'
62 
63     def get_queryset(self):
64         queryset = getattr(self, '_c_queryset', None)
65         if queryset is None:
66             queryset = super(IndicatorPeriodDataFrameworkViewSet, self).get_queryset()
67             queryset = IndicatorPeriodData.get_user_viewable_updates(
68                 queryset, self.request.user
69             )
70             self._c_queryset = queryset
71 
72         return queryset
73 
74     def perform_create(self, serializer):
75         data = {key: value for key, value in serializer.validated_data.items() if key not in ['period', 'files', 'photos', 'approved_by']}
76         if len(serializer._disaggregations_data) > 0:
77             data['disaggregations'] = [
78                 {key: value for key, value in dsg.items() if key in ['id', 'dimension_value', 'value', 'numerator', 'denominator']}
79                 for dsg in serializer._disaggregations_data
80             ]
81         user = self.request.user
82         serializer.save(user=user)
83         instance = serializer.instance
84         log_data = {'audit_trail': True, 'data': data}
85         LogEntry.objects.log_action(
86             user_id=user.id,
87             content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,
88             object_id=instance.id,
89             object_repr=str(instance),
90             action_flag=ADDITION,
91             change_message=json.dumps(log_data, cls=JSONEncoder)
92         )
93 
94     def perform_update(self, serializer):
95         instance = serializer.instance
96         data = {
97             key: value
98             for key, value in serializer.validated_data.items()
99             if key not in ['period', 'files', 'photos', 'approved_by'] and (key == 'comments' or getattr(instance, key) != value)
100         }
101         if len(serializer._disaggregations_data) > 0:
102             indicator = instance.period.indicator
103             is_percentage = indicator.type == QUANTITATIVE and indicator.measure == PERCENTAGE_MEASURE
104             dsg_attrs = ['id', 'dimension_value', 'numerator', 'denominator'] if is_percentage else ['id', 'dimension_value', 'value']
105             data['disaggregations'] = [
106                 {key: value for key, value in dsg.items() if key in dsg_attrs}
107                 for dsg in serializer._disaggregations_data
108             ]
109         user = self.request.user
110         status = data.get('status', None)
111         if status == 'R' or status == 'A':
112             serializer.save()
113         else:
114             serializer.save(user=user)
115         log_data = {'audit_trail': True, 'data': data}
116         LogEntry.objects.log_action(
117             user_id=user.id,
118             content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,
119             object_id=instance.id,
120             object_repr=str(instance),
121             action_flag=CHANGE,
122             change_message=json.dumps(log_data, cls=JSONEncoder)
123         )
124 
125     def perform_destroy(self, instance):
126         object_id = instance.id
127         object_repr = str(instance)
128         super().perform_destroy(instance)
129         LogEntry.objects.log_action(
130             user_id=self.request.user.id,
131             content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,
132             object_id=object_id,
133             object_repr=object_repr,
134             action_flag=DELETION,
135             change_message=json.dumps({'audit_trail': True})
136         )
137 
138 
139 class IndicatorPeriodDataCommentViewSet(PublicProjectViewSet):
140     """
141     """
142     authentication_classes = (SessionAuthentication, TastyTokenAuthentication, JWTAuthentication)
143 
144     # TODO: Is there more optimization possible?
145     queryset = IndicatorPeriodDataComment.objects.select_related(
146         'user'
147     ).prefetch_related(
148         'user__employers', 'user__employers__organisation'
149     )
150     serializer_class = IndicatorPeriodDataCommentSerializer
151     project_relation = 'data__period__indicator__result__project__'
152 
153     def perform_create(self, serializer):
154         serializer.save(user=self.request.user)
155 
156 
157 @api_view(['POST', 'DELETE'])
158 @authentication_classes([SessionAuthentication, TastyTokenAuthentication, JWTAuthentication])
159 def period_update_files(request, update_pk, file_pk=None):
160     update = get_object_or_404(IndicatorPeriodData, pk=update_pk)
161     user = request.user
162     if not user.has_perm('rsr.change_indicatorperioddata', update):
163         return Response({'error': 'User has no permission to add/remove files'}, status=status.HTTP_403_FORBIDDEN)
164 
165     if request.method == 'POST' and not file_pk:
166         serializer = IndicatorPeriodDataFrameworkSerializer(instance=update, data=request.data, partial=True)
167         serializer.is_valid(raise_exception=True)
168         files = [f"Uploaded file \"{file.name}\"" for file in serializer.validated_data.get('files', [])]
169         serializer.save(user=user)
170         log_data = {'audit_trail': True, 'data': {'files': files}}
171         LogEntry.objects.log_action(
172             user_id=user.id,
173             content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,
174             object_id=update.id,
175             object_repr=str(update),
176             action_flag=CHANGE,
177             change_message=json.dumps(log_data)
178         )
179         return Response(serializer.data['file_set'])
180 
181     if request.method == 'DELETE' and file_pk:
182         file = update.indicatorperioddatafile_set.get(pk=file_pk)
183         filename = os.path.basename(file.file.name)
184         file.delete()
185         update.user = user
186         update.save(update_fields=['user'])
187         log_data = {'audit_trail': True, 'data': {'files': [f"Removed file \"{filename}\""]}}
188         LogEntry.objects.log_action(
189             user_id=user.id,
190             content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,
191             object_id=update.id,
192             object_repr=str(update),
193             action_flag=CHANGE,
194             change_message=json.dumps(log_data)
195         )
196         return Response(status=status.HTTP_204_NO_CONTENT)
197 
198     return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
199 
200 
201 @api_view(['POST', 'DELETE'])
202 @authentication_classes([SessionAuthentication, TastyTokenAuthentication, JWTAuthentication])
203 def period_update_photos(request, update_pk, photo_pk=None):
204     update = get_object_or_404(IndicatorPeriodData, pk=update_pk)
205     user = request.user
206     if user != update.user:
207         return Response({'error': 'User has no permission to add/remove photos'}, status=status.HTTP_403_FORBIDDEN)
208 
209     if request.method == 'POST' and not photo_pk:
210         serializer = IndicatorPeriodDataFrameworkSerializer(instance=update, data=request.data, partial=True)
211         serializer.is_valid(raise_exception=True)
212         serializer.save(user=user)
213         return Response(serializer.data['photo_set'])
214 
215     if request.method == 'DELETE' and photo_pk:
216         photo = update.indicatorperioddataphoto_set.get(pk=photo_pk)
217         photo.delete()
218         return Response(status=status.HTTP_204_NO_CONTENT)
219 
220     return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
221 
222 
223 @api_view(['POST', 'DELETE'])
224 def indicator_upload_file(request, pk=None):
225     """
226     Special API call for directly uploading a file.
227 
228     :param request; A Django request object.
229     :param pk; The primary key of an IndicatorPeriodData instance.
230     """
231     # Permissions
232     user = getattr(request, 'user', None)
233     if not user:
234         return Response({'error': 'User is not logged in'}, status=status.HTTP_403_FORBIDDEN)
235     # TODO: Check if user is allowed to upload a file
236     # if not user.has_perm('rsr.change_project', update.period.indicator.result.project):
237     #     return Response({'error': 'User has no permission to place an update'},
238     #                     status=status.HTTP_403_FORBIDDEN)
239 
240     update = IndicatorPeriodData.objects.get(pk=pk)
241     if request.method == 'DELETE':
242         try:
243             if request.data['type'] == 'photo':
244                 update.photo = ''
245                 update.save(update_fields=['photo'])
246                 return Response({}, status=status.HTTP_204_NO_CONTENT)
247             elif request.data['type'] == 'file':
248                 update.file = ''
249                 update.save(update_fields=['file'])
250                 return Response({}, status=status.HTTP_204_NO_CONTENT)
251         except Exception as e:
252             return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
253     else:  # POST
254         upload_file = request.data['file']
255         try:
256             file_type = request.POST.copy()['type']
257             if file_type == 'photo':
258                 update.photo = upload_file
259                 update.save(update_fields=['photo'])
260                 # Add photo member to be able to distinguish from file URL in new results version
261                 # while keeping the old API
262                 return Response({'file': update.photo.url, 'photo': update.photo.url})
263             elif file_type == 'file':
264                 update.file = upload_file
265                 update.save(update_fields=['file'])
266                 return Response({'file': update.file.url})
267         except Exception as e:
268             return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
269 
270 
271 @api_view(['POST'])
272 @authentication_classes([SessionAuthentication, TastyTokenAuthentication])
273 def set_updates_status(request, project_pk):
274     """Bulk update IndicatorPeriodData.status attributes of a project.
275     """
276     update_ids = request.data.get('updates', [])
277     status = request.data.get('status', None)
278     if len(update_ids) < 1 or status is None:
279         return HttpResponseBadRequest()
280     user = request.user
281     project = get_object_or_404(Project, pk=project_pk)
282     if not user.has_perm('rsr.change_project', project):
283         return HttpResponseForbidden()
284     IndicatorPeriodData.objects\
285         .filter(id__in=update_ids, period__indicator__result__project=project)\
286         .update(status=status)
287     log_data = {'audit_trail': True, 'data': {'status': status}}
288     for update_id in update_ids:
289         LogEntry.objects.log_action(
290             user_id=user.id,
291             content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,
292             object_id=update_id,
293             object_repr='IndicatorPeriodData',
294             action_flag=CHANGE,
295             change_message=json.dumps(log_data)
296         )
297     return Response({'success': True})
298
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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
@@ -60,15 +60,22 @@
     serializer_class = IndicatorPeriodDataFrameworkSerializer
     project_relation = 'period__indicator__result__project__'
 
-    def get_queryset(self):
-        queryset = getattr(self, '_c_queryset', None)
-        if queryset is None:
-            queryset = super(IndicatorPeriodDataFrameworkViewSet, self).get_queryset()
-            queryset = IndicatorPeriodData.get_user_viewable_updates(
-                queryset, self.request.user
-            )
-            self._c_queryset = queryset
+    def get_object(self):
+        obj = get_object_or_404(self.get_queryset(), pk=self.kwargs['pk'])
+        # check whether the user has permission
+        viewables = IndicatorPeriodData.get_user_viewable_updates(
+            self.get_queryset().filter(pk=self.kwargs['pk']),
+            self.request.user
+        )
+        if viewables.count() == 0:
+            self.permission_denied(self.request)
+        return obj
 
+    def filter_queryset(self, queryset):
+        queryset = super().filter_queryset(queryset)
+        queryset = IndicatorPeriodData.get_user_viewable_updates(
+            queryset, self.request.user
+        )
         return queryset
 
     def perform_create(self, serializer):

{"golden_diff": "diff --git a/akvo/rest/views/indicator_period_data.py b/akvo/rest/views/indicator_period_data.py\n--- a/akvo/rest/views/indicator_period_data.py\n+++ b/akvo/rest/views/indicator_period_data.py\n@@ -60,15 +60,22 @@\n     serializer_class = IndicatorPeriodDataFrameworkSerializer\n     project_relation = 'period__indicator__result__project__'\n \n-    def get_queryset(self):\n-        queryset = getattr(self, '_c_queryset', None)\n-        if queryset is None:\n-            queryset = super(IndicatorPeriodDataFrameworkViewSet, self).get_queryset()\n-            queryset = IndicatorPeriodData.get_user_viewable_updates(\n-                queryset, self.request.user\n-            )\n-            self._c_queryset = queryset\n+    def get_object(self):\n+        obj = get_object_or_404(self.get_queryset(), pk=self.kwargs['pk'])\n+        # check whether the user has permission\n+        viewables = IndicatorPeriodData.get_user_viewable_updates(\n+            self.get_queryset().filter(pk=self.kwargs['pk']),\n+            self.request.user\n+        )\n+        if viewables.count() == 0:\n+            self.permission_denied(self.request)\n+        return obj\n \n+    def filter_queryset(self, queryset):\n+        queryset = super().filter_queryset(queryset)\n+        queryset = IndicatorPeriodData.get_user_viewable_updates(\n+            queryset, self.request.user\n+        )\n         return queryset\n \n     def perform_create(self, serializer):\n", "issue": "Collaborate draft update bug\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\n# Akvo RSR is covered by the GNU Affero General Public License.\n# See more details in the license.txt file located at the root folder of the Akvo RSR module.\n# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.\n\nimport json\nimport os\n\nfrom akvo.rsr.models import IndicatorPeriodData, IndicatorPeriodDataComment, Project\nfrom akvo.rest.models import TastyTokenAuthentication, JWTAuthentication\nfrom akvo.rsr.models.result.utils import QUANTITATIVE, PERCENTAGE_MEASURE\n\nfrom ..serializers import (IndicatorPeriodDataSerializer, IndicatorPeriodDataFrameworkSerializer,\n                           IndicatorPeriodDataCommentSerializer)\nfrom ..viewsets import PublicProjectViewSet\n\nfrom django.shortcuts import get_object_or_404\nfrom django.http import HttpResponseBadRequest, HttpResponseForbidden\nfrom django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION\nfrom django.contrib.contenttypes.models import ContentType\nfrom rest_framework import status\nfrom rest_framework.authentication import SessionAuthentication\nfrom rest_framework.decorators import api_view, authentication_classes\nfrom rest_framework.response import Response\nfrom rest_framework.utils.encoders import JSONEncoder\n\n\nclass IndicatorPeriodDataViewSet(PublicProjectViewSet):\n    \"\"\"\n    \"\"\"\n    queryset = IndicatorPeriodData.objects.select_related('user', 'approved_by').all()\n    serializer_class = IndicatorPeriodDataSerializer\n\n    project_relation = 'period__indicator__result__project__'\n\n    def filter_queryset(self, queryset):\n        queryset = super(IndicatorPeriodDataViewSet, self).filter_queryset(queryset)\n        return IndicatorPeriodData.get_user_viewable_updates(\n            queryset, self.request.user\n        )\n\n    def perform_create(self, serializer):\n        serializer.save(user=self.request.user)\n\n\nclass IndicatorPeriodDataFrameworkViewSet(PublicProjectViewSet):\n    \"\"\"\n    \"\"\"\n    authentication_classes = (SessionAuthentication, TastyTokenAuthentication, JWTAuthentication)\n\n    queryset = IndicatorPeriodData.objects.select_related(\n        'period',\n        'user',\n        'approved_by',\n    ).prefetch_related(\n        'comments',\n        'disaggregations',\n    ).all()\n    serializer_class = IndicatorPeriodDataFrameworkSerializer\n    project_relation = 'period__indicator__result__project__'\n\n    def get_queryset(self):\n        queryset = getattr(self, '_c_queryset', None)\n        if queryset is None:\n            queryset = super(IndicatorPeriodDataFrameworkViewSet, self).get_queryset()\n            queryset = IndicatorPeriodData.get_user_viewable_updates(\n                queryset, self.request.user\n            )\n            self._c_queryset = queryset\n\n        return queryset\n\n    def perform_create(self, serializer):\n        data = {key: value for key, value in serializer.validated_data.items() if key not in ['period', 'files', 'photos', 'approved_by']}\n        if len(serializer._disaggregations_data) > 0:\n            data['disaggregations'] = [\n                {key: value for key, value in dsg.items() if key in ['id', 'dimension_value', 'value', 'numerator', 'denominator']}\n                for dsg in serializer._disaggregations_data\n            ]\n        user = self.request.user\n        serializer.save(user=user)\n        instance = serializer.instance\n        log_data = {'audit_trail': True, 'data': data}\n        LogEntry.objects.log_action(\n            user_id=user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=instance.id,\n            object_repr=str(instance),\n            action_flag=ADDITION,\n            change_message=json.dumps(log_data, cls=JSONEncoder)\n        )\n\n    def perform_update(self, serializer):\n        instance = serializer.instance\n        data = {\n            key: value\n            for key, value in serializer.validated_data.items()\n            if key not in ['period', 'files', 'photos', 'approved_by'] and (key == 'comments' or getattr(instance, key) != value)\n        }\n        if len(serializer._disaggregations_data) > 0:\n            indicator = instance.period.indicator\n            is_percentage = indicator.type == QUANTITATIVE and indicator.measure == PERCENTAGE_MEASURE\n            dsg_attrs = ['id', 'dimension_value', 'numerator', 'denominator'] if is_percentage else ['id', 'dimension_value', 'value']\n            data['disaggregations'] = [\n                {key: value for key, value in dsg.items() if key in dsg_attrs}\n                for dsg in serializer._disaggregations_data\n            ]\n        user = self.request.user\n        status = data.get('status', None)\n        if status == 'R' or status == 'A':\n            serializer.save()\n        else:\n            serializer.save(user=user)\n        log_data = {'audit_trail': True, 'data': data}\n        LogEntry.objects.log_action(\n            user_id=user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=instance.id,\n            object_repr=str(instance),\n            action_flag=CHANGE,\n            change_message=json.dumps(log_data, cls=JSONEncoder)\n        )\n\n    def perform_destroy(self, instance):\n        object_id = instance.id\n        object_repr = str(instance)\n        super().perform_destroy(instance)\n        LogEntry.objects.log_action(\n            user_id=self.request.user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=object_id,\n            object_repr=object_repr,\n            action_flag=DELETION,\n            change_message=json.dumps({'audit_trail': True})\n        )\n\n\nclass IndicatorPeriodDataCommentViewSet(PublicProjectViewSet):\n    \"\"\"\n    \"\"\"\n    authentication_classes = (SessionAuthentication, TastyTokenAuthentication, JWTAuthentication)\n\n    # TODO: Is there more optimization possible?\n    queryset = IndicatorPeriodDataComment.objects.select_related(\n        'user'\n    ).prefetch_related(\n        'user__employers', 'user__employers__organisation'\n    )\n    serializer_class = IndicatorPeriodDataCommentSerializer\n    project_relation = 'data__period__indicator__result__project__'\n\n    def perform_create(self, serializer):\n        serializer.save(user=self.request.user)\n\n\n@api_view(['POST', 'DELETE'])\n@authentication_classes([SessionAuthentication, TastyTokenAuthentication, JWTAuthentication])\ndef period_update_files(request, update_pk, file_pk=None):\n    update = get_object_or_404(IndicatorPeriodData, pk=update_pk)\n    user = request.user\n    if not user.has_perm('rsr.change_indicatorperioddata', update):\n        return Response({'error': 'User has no permission to add/remove files'}, status=status.HTTP_403_FORBIDDEN)\n\n    if request.method == 'POST' and not file_pk:\n        serializer = IndicatorPeriodDataFrameworkSerializer(instance=update, data=request.data, partial=True)\n        serializer.is_valid(raise_exception=True)\n        files = [f\"Uploaded file \\\"{file.name}\\\"\" for file in serializer.validated_data.get('files', [])]\n        serializer.save(user=user)\n        log_data = {'audit_trail': True, 'data': {'files': files}}\n        LogEntry.objects.log_action(\n            user_id=user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=update.id,\n            object_repr=str(update),\n            action_flag=CHANGE,\n            change_message=json.dumps(log_data)\n        )\n        return Response(serializer.data['file_set'])\n\n    if request.method == 'DELETE' and file_pk:\n        file = update.indicatorperioddatafile_set.get(pk=file_pk)\n        filename = os.path.basename(file.file.name)\n        file.delete()\n        update.user = user\n        update.save(update_fields=['user'])\n        log_data = {'audit_trail': True, 'data': {'files': [f\"Removed file \\\"{filename}\\\"\"]}}\n        LogEntry.objects.log_action(\n            user_id=user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=update.id,\n            object_repr=str(update),\n            action_flag=CHANGE,\n            change_message=json.dumps(log_data)\n        )\n        return Response(status=status.HTTP_204_NO_CONTENT)\n\n    return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)\n\n\n@api_view(['POST', 'DELETE'])\n@authentication_classes([SessionAuthentication, TastyTokenAuthentication, JWTAuthentication])\ndef period_update_photos(request, update_pk, photo_pk=None):\n    update = get_object_or_404(IndicatorPeriodData, pk=update_pk)\n    user = request.user\n    if user != update.user:\n        return Response({'error': 'User has no permission to add/remove photos'}, status=status.HTTP_403_FORBIDDEN)\n\n    if request.method == 'POST' and not photo_pk:\n        serializer = IndicatorPeriodDataFrameworkSerializer(instance=update, data=request.data, partial=True)\n        serializer.is_valid(raise_exception=True)\n        serializer.save(user=user)\n        return Response(serializer.data['photo_set'])\n\n    if request.method == 'DELETE' and photo_pk:\n        photo = update.indicatorperioddataphoto_set.get(pk=photo_pk)\n        photo.delete()\n        return Response(status=status.HTTP_204_NO_CONTENT)\n\n    return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)\n\n\n@api_view(['POST', 'DELETE'])\ndef indicator_upload_file(request, pk=None):\n    \"\"\"\n    Special API call for directly uploading a file.\n\n    :param request; A Django request object.\n    :param pk; The primary key of an IndicatorPeriodData instance.\n    \"\"\"\n    # Permissions\n    user = getattr(request, 'user', None)\n    if not user:\n        return Response({'error': 'User is not logged in'}, status=status.HTTP_403_FORBIDDEN)\n    # TODO: Check if user is allowed to upload a file\n    # if not user.has_perm('rsr.change_project', update.period.indicator.result.project):\n    #     return Response({'error': 'User has no permission to place an update'},\n    #                     status=status.HTTP_403_FORBIDDEN)\n\n    update = IndicatorPeriodData.objects.get(pk=pk)\n    if request.method == 'DELETE':\n        try:\n            if request.data['type'] == 'photo':\n                update.photo = ''\n                update.save(update_fields=['photo'])\n                return Response({}, status=status.HTTP_204_NO_CONTENT)\n            elif request.data['type'] == 'file':\n                update.file = ''\n                update.save(update_fields=['file'])\n                return Response({}, status=status.HTTP_204_NO_CONTENT)\n        except Exception as e:\n            return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)\n    else:  # POST\n        upload_file = request.data['file']\n        try:\n            file_type = request.POST.copy()['type']\n            if file_type == 'photo':\n                update.photo = upload_file\n                update.save(update_fields=['photo'])\n                # Add photo member to be able to distinguish from file URL in new results version\n                # while keeping the old API\n                return Response({'file': update.photo.url, 'photo': update.photo.url})\n            elif file_type == 'file':\n                update.file = upload_file\n                update.save(update_fields=['file'])\n                return Response({'file': update.file.url})\n        except Exception as e:\n            return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)\n\n\n@api_view(['POST'])\n@authentication_classes([SessionAuthentication, TastyTokenAuthentication])\ndef set_updates_status(request, project_pk):\n    \"\"\"Bulk update IndicatorPeriodData.status attributes of a project.\n    \"\"\"\n    update_ids = request.data.get('updates', [])\n    status = request.data.get('status', None)\n    if len(update_ids) < 1 or status is None:\n        return HttpResponseBadRequest()\n    user = request.user\n    project = get_object_or_404(Project, pk=project_pk)\n    if not user.has_perm('rsr.change_project', project):\n        return HttpResponseForbidden()\n    IndicatorPeriodData.objects\\\n        .filter(id__in=update_ids, period__indicator__result__project=project)\\\n        .update(status=status)\n    log_data = {'audit_trail': True, 'data': {'status': status}}\n    for update_id in update_ids:\n        LogEntry.objects.log_action(\n            user_id=user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=update_id,\n            object_repr='IndicatorPeriodData',\n            action_flag=CHANGE,\n            change_message=json.dumps(log_data)\n        )\n    return Response({'success': True})\n", "path": "akvo/rest/views/indicator_period_data.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\n# Akvo RSR is covered by the GNU Affero General Public License.\n# See more details in the license.txt file located at the root folder of the Akvo RSR module.\n# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.\n\nimport json\nimport os\n\nfrom akvo.rsr.models import IndicatorPeriodData, IndicatorPeriodDataComment, Project\nfrom akvo.rest.models import TastyTokenAuthentication, JWTAuthentication\nfrom akvo.rsr.models.result.utils import QUANTITATIVE, PERCENTAGE_MEASURE\n\nfrom ..serializers import (IndicatorPeriodDataSerializer, IndicatorPeriodDataFrameworkSerializer,\n                           IndicatorPeriodDataCommentSerializer)\nfrom ..viewsets import PublicProjectViewSet\n\nfrom django.shortcuts import get_object_or_404\nfrom django.http import HttpResponseBadRequest, HttpResponseForbidden\nfrom django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION\nfrom django.contrib.contenttypes.models import ContentType\nfrom rest_framework import status\nfrom rest_framework.authentication import SessionAuthentication\nfrom rest_framework.decorators import api_view, authentication_classes\nfrom rest_framework.response import Response\nfrom rest_framework.utils.encoders import JSONEncoder\n\n\nclass IndicatorPeriodDataViewSet(PublicProjectViewSet):\n    \"\"\"\n    \"\"\"\n    queryset = IndicatorPeriodData.objects.select_related('user', 'approved_by').all()\n    serializer_class = IndicatorPeriodDataSerializer\n\n    project_relation = 'period__indicator__result__project__'\n\n    def filter_queryset(self, queryset):\n        queryset = super(IndicatorPeriodDataViewSet, self).filter_queryset(queryset)\n        return IndicatorPeriodData.get_user_viewable_updates(\n            queryset, self.request.user\n        )\n\n    def perform_create(self, serializer):\n        serializer.save(user=self.request.user)\n\n\nclass IndicatorPeriodDataFrameworkViewSet(PublicProjectViewSet):\n    \"\"\"\n    \"\"\"\n    authentication_classes = (SessionAuthentication, TastyTokenAuthentication, JWTAuthentication)\n\n    queryset = IndicatorPeriodData.objects.select_related(\n        'period',\n        'user',\n        'approved_by',\n    ).prefetch_related(\n        'comments',\n        'disaggregations',\n    ).all()\n    serializer_class = IndicatorPeriodDataFrameworkSerializer\n    project_relation = 'period__indicator__result__project__'\n\n    def get_object(self):\n        obj = get_object_or_404(self.get_queryset(), pk=self.kwargs['pk'])\n        # check whether the user has permission\n        viewables = IndicatorPeriodData.get_user_viewable_updates(\n            self.get_queryset().filter(pk=self.kwargs['pk']),\n            self.request.user\n        )\n        if viewables.count() == 0:\n            self.permission_denied(self.request)\n        return obj\n\n    def filter_queryset(self, queryset):\n        queryset = super().filter_queryset(queryset)\n        queryset = IndicatorPeriodData.get_user_viewable_updates(\n            queryset, self.request.user\n        )\n        return queryset\n\n    def perform_create(self, serializer):\n        data = {key: value for key, value in serializer.validated_data.items() if key not in ['period', 'files', 'photos', 'approved_by']}\n        if len(serializer._disaggregations_data) > 0:\n            data['disaggregations'] = [\n                {key: value for key, value in dsg.items() if key in ['id', 'dimension_value', 'value', 'numerator', 'denominator']}\n                for dsg in serializer._disaggregations_data\n            ]\n        user = self.request.user\n        serializer.save(user=user)\n        instance = serializer.instance\n        log_data = {'audit_trail': True, 'data': data}\n        LogEntry.objects.log_action(\n            user_id=user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=instance.id,\n            object_repr=str(instance),\n            action_flag=ADDITION,\n            change_message=json.dumps(log_data, cls=JSONEncoder)\n        )\n\n    def perform_update(self, serializer):\n        instance = serializer.instance\n        data = {\n            key: value\n            for key, value in serializer.validated_data.items()\n            if key not in ['period', 'files', 'photos', 'approved_by'] and (key == 'comments' or getattr(instance, key) != value)\n        }\n        if len(serializer._disaggregations_data) > 0:\n            indicator = instance.period.indicator\n            is_percentage = indicator.type == QUANTITATIVE and indicator.measure == PERCENTAGE_MEASURE\n            dsg_attrs = ['id', 'dimension_value', 'numerator', 'denominator'] if is_percentage else ['id', 'dimension_value', 'value']\n            data['disaggregations'] = [\n                {key: value for key, value in dsg.items() if key in dsg_attrs}\n                for dsg in serializer._disaggregations_data\n            ]\n        user = self.request.user\n        status = data.get('status', None)\n        if status == 'R' or status == 'A':\n            serializer.save()\n        else:\n            serializer.save(user=user)\n        log_data = {'audit_trail': True, 'data': data}\n        LogEntry.objects.log_action(\n            user_id=user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=instance.id,\n            object_repr=str(instance),\n            action_flag=CHANGE,\n            change_message=json.dumps(log_data, cls=JSONEncoder)\n        )\n\n    def perform_destroy(self, instance):\n        object_id = instance.id\n        object_repr = str(instance)\n        super().perform_destroy(instance)\n        LogEntry.objects.log_action(\n            user_id=self.request.user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=object_id,\n            object_repr=object_repr,\n            action_flag=DELETION,\n            change_message=json.dumps({'audit_trail': True})\n        )\n\n\nclass IndicatorPeriodDataCommentViewSet(PublicProjectViewSet):\n    \"\"\"\n    \"\"\"\n    authentication_classes = (SessionAuthentication, TastyTokenAuthentication, JWTAuthentication)\n\n    # TODO: Is there more optimization possible?\n    queryset = IndicatorPeriodDataComment.objects.select_related(\n        'user'\n    ).prefetch_related(\n        'user__employers', 'user__employers__organisation'\n    )\n    serializer_class = IndicatorPeriodDataCommentSerializer\n    project_relation = 'data__period__indicator__result__project__'\n\n    def perform_create(self, serializer):\n        serializer.save(user=self.request.user)\n\n\n@api_view(['POST', 'DELETE'])\n@authentication_classes([SessionAuthentication, TastyTokenAuthentication, JWTAuthentication])\ndef period_update_files(request, update_pk, file_pk=None):\n    update = get_object_or_404(IndicatorPeriodData, pk=update_pk)\n    user = request.user\n    if not user.has_perm('rsr.change_indicatorperioddata', update):\n        return Response({'error': 'User has no permission to add/remove files'}, status=status.HTTP_403_FORBIDDEN)\n\n    if request.method == 'POST' and not file_pk:\n        serializer = IndicatorPeriodDataFrameworkSerializer(instance=update, data=request.data, partial=True)\n        serializer.is_valid(raise_exception=True)\n        files = [f\"Uploaded file \\\"{file.name}\\\"\" for file in serializer.validated_data.get('files', [])]\n        serializer.save(user=user)\n        log_data = {'audit_trail': True, 'data': {'files': files}}\n        LogEntry.objects.log_action(\n            user_id=user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=update.id,\n            object_repr=str(update),\n            action_flag=CHANGE,\n            change_message=json.dumps(log_data)\n        )\n        return Response(serializer.data['file_set'])\n\n    if request.method == 'DELETE' and file_pk:\n        file = update.indicatorperioddatafile_set.get(pk=file_pk)\n        filename = os.path.basename(file.file.name)\n        file.delete()\n        update.user = user\n        update.save(update_fields=['user'])\n        log_data = {'audit_trail': True, 'data': {'files': [f\"Removed file \\\"{filename}\\\"\"]}}\n        LogEntry.objects.log_action(\n            user_id=user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=update.id,\n            object_repr=str(update),\n            action_flag=CHANGE,\n            change_message=json.dumps(log_data)\n        )\n        return Response(status=status.HTTP_204_NO_CONTENT)\n\n    return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)\n\n\n@api_view(['POST', 'DELETE'])\n@authentication_classes([SessionAuthentication, TastyTokenAuthentication, JWTAuthentication])\ndef period_update_photos(request, update_pk, photo_pk=None):\n    update = get_object_or_404(IndicatorPeriodData, pk=update_pk)\n    user = request.user\n    if user != update.user:\n        return Response({'error': 'User has no permission to add/remove photos'}, status=status.HTTP_403_FORBIDDEN)\n\n    if request.method == 'POST' and not photo_pk:\n        serializer = IndicatorPeriodDataFrameworkSerializer(instance=update, data=request.data, partial=True)\n        serializer.is_valid(raise_exception=True)\n        serializer.save(user=user)\n        return Response(serializer.data['photo_set'])\n\n    if request.method == 'DELETE' and photo_pk:\n        photo = update.indicatorperioddataphoto_set.get(pk=photo_pk)\n        photo.delete()\n        return Response(status=status.HTTP_204_NO_CONTENT)\n\n    return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)\n\n\n@api_view(['POST', 'DELETE'])\ndef indicator_upload_file(request, pk=None):\n    \"\"\"\n    Special API call for directly uploading a file.\n\n    :param request; A Django request object.\n    :param pk; The primary key of an IndicatorPeriodData instance.\n    \"\"\"\n    # Permissions\n    user = getattr(request, 'user', None)\n    if not user:\n        return Response({'error': 'User is not logged in'}, status=status.HTTP_403_FORBIDDEN)\n    # TODO: Check if user is allowed to upload a file\n    # if not user.has_perm('rsr.change_project', update.period.indicator.result.project):\n    #     return Response({'error': 'User has no permission to place an update'},\n    #                     status=status.HTTP_403_FORBIDDEN)\n\n    update = IndicatorPeriodData.objects.get(pk=pk)\n    if request.method == 'DELETE':\n        try:\n            if request.data['type'] == 'photo':\n                update.photo = ''\n                update.save(update_fields=['photo'])\n                return Response({}, status=status.HTTP_204_NO_CONTENT)\n            elif request.data['type'] == 'file':\n                update.file = ''\n                update.save(update_fields=['file'])\n                return Response({}, status=status.HTTP_204_NO_CONTENT)\n        except Exception as e:\n            return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)\n    else:  # POST\n        upload_file = request.data['file']\n        try:\n            file_type = request.POST.copy()['type']\n            if file_type == 'photo':\n                update.photo = upload_file\n                update.save(update_fields=['photo'])\n                # Add photo member to be able to distinguish from file URL in new results version\n                # while keeping the old API\n                return Response({'file': update.photo.url, 'photo': update.photo.url})\n            elif file_type == 'file':\n                update.file = upload_file\n                update.save(update_fields=['file'])\n                return Response({'file': update.file.url})\n        except Exception as e:\n            return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)\n\n\n@api_view(['POST'])\n@authentication_classes([SessionAuthentication, TastyTokenAuthentication])\ndef set_updates_status(request, project_pk):\n    \"\"\"Bulk update IndicatorPeriodData.status attributes of a project.\n    \"\"\"\n    update_ids = request.data.get('updates', [])\n    status = request.data.get('status', None)\n    if len(update_ids) < 1 or status is None:\n        return HttpResponseBadRequest()\n    user = request.user\n    project = get_object_or_404(Project, pk=project_pk)\n    if not user.has_perm('rsr.change_project', project):\n        return HttpResponseForbidden()\n    IndicatorPeriodData.objects\\\n        .filter(id__in=update_ids, period__indicator__result__project=project)\\\n        .update(status=status)\n    log_data = {'audit_trail': True, 'data': {'status': status}}\n    for update_id in update_ids:\n        LogEntry.objects.log_action(\n            user_id=user.id,\n            content_type_id=ContentType.objects.get_for_model(IndicatorPeriodData).id,\n            object_id=update_id,\n            object_repr='IndicatorPeriodData',\n            action_flag=CHANGE,\n            change_message=json.dumps(log_data)\n        )\n    return Response({'success': True})\n", "path": "akvo/rest/views/indicator_period_data.py"}]}