From 9668dc06aaf74bc0eb3f83bd13519e7a81c2879e Mon Sep 17 00:00:00 2001 From: WinnyTroy Date: Wed, 9 Oct 2019 11:47:42 +0300 Subject: [PATCH] Add deleted_at Field to Attachments. Create soft_delete action that will populate deleted_at field for attachments. Fixes #1696 Call soft_delete action when replacing attachments --- .../tests/viewsets/test_attachment_viewset.py | 65 +++++++++++++++++++ .../apps/api/viewsets/attachment_viewset.py | 3 +- onadata/apps/logger/models/attachment.py | 10 +++ onadata/apps/logger/models/instance.py | 5 +- onadata/libs/utils/logger_tools.py | 6 ++ 5 files changed, 87 insertions(+), 2 deletions(-) diff --git a/onadata/apps/api/tests/viewsets/test_attachment_viewset.py b/onadata/apps/api/tests/viewsets/test_attachment_viewset.py index 40a0f18097..60fa2c8ffa 100644 --- a/onadata/apps/api/tests/viewsets/test_attachment_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_attachment_viewset.py @@ -188,6 +188,71 @@ def test_list_view(self): self.assertTrue(isinstance(response.data, list)) self.assertEqual(len(response.data), 0) + def test_list_view_surfaces_current_attachment(self): + self._submit_transport_instance_w_attachment() + filename = "1335783522564.JPG" + path = os.path.join(self.main_directory, 'fixtures', 'transportation', + 'instances', self.surveys[0], filename) + media_file = django_file(path, 'video2', 'image/jpeg') + + Attachment.objects.create( + instance=self.xform.instances.first(), + mimetype='video/mp4', + extension='MP4', + name=filename, + media_file=media_file) + + Attachment.objects.create( + instance=self.xform.instances.first(), + mimetype='application/pdf', + extension='PDF', + name=filename, + media_file=media_file) + Attachment.objects.create( + instance=self.xform.instances.first(), + mimetype='text/plain', + extension='TXT', + name=filename, + media_file=media_file) + Attachment.objects.create( + instance=self.xform.instances.first(), + mimetype='audio/mp3', + extension='MP3', + name=filename, + media_file=media_file) + + request = self.factory.get('/', **self.extra) + response = self.list_view(request) + self.assertNotEqual(response.get('Cache-Control'), None) + self.assertEqual(response.status_code, 200) + self.assertTrue(isinstance(response.data, list)) + self.assertEqual(len(response.data), 5) + + # test when the attachment is soft deleted + self.attachment.soft_delete(user=self.user) + + request = self.factory.get('/', **self.extra) + response = self.list_view(request) + + self.assertEqual(len(response.data), 4) + + def test_soft_delete_action_returns_correct_user(self): + self._submit_transport_instance_w_attachment() + + request = self.factory.get('/', **self.extra) + response = self.list_view(request) + self.assertEqual(len(response.data), 1) + + # test when the attachment is soft deleted + self.attachment.soft_delete(user=self.user) + + # Test that deleted_by field captures the right user + self.assertTrue(self.attachment.deleted_by, self.user) + + request = self.factory.get('/', **self.extra) + response = self.list_view(request) + self.assertEqual(len(response.data), 0) + def test_data_list_with_xform_in_delete_async(self): self._submit_transport_instance_w_attachment() diff --git a/onadata/apps/api/viewsets/attachment_viewset.py b/onadata/apps/api/viewsets/attachment_viewset.py index 0d63153ca6..af38671c40 100644 --- a/onadata/apps/api/viewsets/attachment_viewset.py +++ b/onadata/apps/api/viewsets/attachment_viewset.py @@ -47,7 +47,8 @@ class AttachmentViewSet(AuthenticateHeaderMixin, CacheControlMixin, ETagsMixin, content_negotiation_class = MediaFileContentNegotiation filter_backends = (filters.AttachmentFilter, filters.AttachmentTypeFilter) lookup_field = 'pk' - queryset = Attachment.objects.filter(instance__deleted_at__isnull=True) + queryset = Attachment.objects.filter( + instance__deleted_at__isnull=True, deleted_at__isnull=True) permission_classes = (AttachmentObjectPermissions,) serializer_class = AttachmentSerializer pagination_class = StandardPageNumberPagination diff --git a/onadata/apps/logger/models/attachment.py b/onadata/apps/logger/models/attachment.py index 72f6399305..7e9fb3fda4 100644 --- a/onadata/apps/logger/models/attachment.py +++ b/onadata/apps/logger/models/attachment.py @@ -3,6 +3,7 @@ from hashlib import md5 from django.db import models +from django.utils import timezone def get_original_filename(filename): @@ -86,3 +87,12 @@ def file_hash(self): def filename(self): if self.media_file: return os.path.basename(self.media_file.name) + + def soft_delete(self, user=None): + """ + Soft deletes an attachment by adding a deleted_at timestamp. + """ + self.deleted_at = timezone.now() + if user is not None: + self.deleted_by = user + self.save() diff --git a/onadata/apps/logger/models/instance.py b/onadata/apps/logger/models/instance.py index eba834e846..c4a07e81b7 100644 --- a/onadata/apps/logger/models/instance.py +++ b/onadata/apps/logger/models/instance.py @@ -66,7 +66,7 @@ def get_attachment_url(attachment, suffix=None): def _get_attachments_from_instance(instance): attachments = [] - for a in instance.attachments.all(): + for a in instance.attachments.filter(deleted_at__isnull=True): attachment = dict() attachment['download_url'] = get_attachment_url(a) attachment['small_download_url'] = get_attachment_url(a, 'small') @@ -79,6 +79,9 @@ def _get_attachments_from_instance(instance): attachment['id'] = a.id attachments.append(attachment) + if isinstance(a.deleted_at, datetime): + attachment['deleted_at'] = a.deleted_at.strftime(MONGO_STRFTIME) + return attachments diff --git a/onadata/libs/utils/logger_tools.py b/onadata/libs/utils/logger_tools.py index 2c3b1dcc6d..2c3c11a1ba 100644 --- a/onadata/libs/utils/logger_tools.py +++ b/onadata/libs/utils/logger_tools.py @@ -220,6 +220,7 @@ def save_attachments(xform, instance, media_files): """ # upload_path = os.path.join(instance.xform.user.username, 'attachments') + filenames = [] for f in media_files: filename, extension = os.path.splitext(f.name) extension = extension.replace('.', '') @@ -229,6 +230,7 @@ def save_attachments(xform, instance, media_files): xform.instances_with_osm = True xform.save() filename = os.path.basename(f.name) + filenames.append(filename) media_in_submission = ( filename in instance.get_expected_media() or [instance.xml.decode('utf-8').find(filename) != -1 if @@ -241,6 +243,10 @@ def save_attachments(xform, instance, media_files): mimetype=content_type, name=filename, extension=extension) + Attachment.objects.filter(instance=instance).filter( + ~Q(name__in=instance.get_expected_media())).update( + deleted_at=timezone.now()) + update_attachment_tracking(instance)