diff --git a/onadata/apps/api/tests/viewsets/test_project_viewset.py b/onadata/apps/api/tests/viewsets/test_project_viewset.py index 166dd1bfae..5e337a800d 100644 --- a/onadata/apps/api/tests/viewsets/test_project_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_project_viewset.py @@ -42,6 +42,10 @@ XFormVersion, ) from onadata.apps.main.models import MetaData +from onadata.apps.messaging.constants import ( + XFORM, PROJECT, USER, PROJECT_CREATED, + FORM_CREATED, PROJECT_DELETED +) from onadata.libs import permissions as role from onadata.libs.models.share_project import ShareProject from onadata.libs.permissions import ( @@ -108,8 +112,10 @@ def tearDown(self): super().tearDown() # pylint: disable=invalid-name + @patch("onadata.apps.api.viewsets.project_viewset.send_message") @patch("onadata.apps.main.forms.requests") - def test_publish_xlsform_using_url_upload(self, mock_requests): + def test_publish_xlsform_using_url_upload( + self, mock_requests, mock_send_message): with HTTMock(enketo_mock): self._project_create() view = ProjectViewSet.as_view({"post": "forms"}) @@ -137,6 +143,7 @@ def test_publish_xlsform_using_url_upload(self, mock_requests): post_data = {"xls_url": xls_url} request = self.factory.post("/", data=post_data, **self.extra) response = view(request, pk=project_id) + xform = self.user.xforms.get(id_string="transportation_2015_01_07") mock_requests.get.assert_called_with(xls_url) xls_file.close() @@ -149,6 +156,16 @@ def test_publish_xlsform_using_url_upload(self, mock_requests): 1, ) + # send message upon form deletion + self.assertTrue(mock_send_message.called) + mock_send_message.assert_called_with( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user, + message_verb=FORM_CREATED + ) + @override_settings(TIME_ZONE="UTC") def test_projects_list(self): self._publish_xls_form_to_project() @@ -499,7 +516,8 @@ def test_projects_tags(self): self.assertEqual(response.data, []) self.assertEqual(get_latest_tags(self.project), []) - def test_projects_create(self): + @patch("onadata.apps.api.viewsets.project_viewset.send_message") + def test_projects_create(self, mock_send_message): self._project_create() self.assertIsNotNone(self.project_data) @@ -510,6 +528,16 @@ def test_projects_create(self): self.assertEqual(self.user, project.created_by) self.assertEqual(self.user, project.organization) + # send message upon project creation + self.assertTrue(mock_send_message.called) + mock_send_message.assert_called_with( + instance_id=self.project.id, + target_id=self.project.id, + target_type=PROJECT, + user=self.user, + message_verb=PROJECT_CREATED + ) + def test_project_create_other_account(self): # pylint: disable=invalid-name """ Test that a user cannot create a project in a different user account @@ -2611,7 +2639,8 @@ def test_project_list_by_owner(self): users, ) - def test_projects_soft_delete(self): + @patch("onadata.apps.api.viewsets.project_viewset.send_message") + def test_projects_soft_delete(self, mock_send_message): self._project_create() view = ProjectViewSet.as_view({"get": "list", "delete": "destroy"}) @@ -2632,6 +2661,17 @@ def test_projects_soft_delete(self): request = self.factory.delete("/", **self.extra) request.user = self.user response = view(request, pk=project_id) + + # send message upon project creation + self.assertTrue(mock_send_message.called) + mock_send_message.assert_called_with( + instance_id=self.project.id, + target_id=self.user.id, + target_type=USER, + user=self.user, + message_verb=PROJECT_DELETED + ) + self.assertEqual(response.status_code, 204) self.project = Project.objects.get(pk=project_id) diff --git a/onadata/apps/api/tests/viewsets/test_xform_viewset.py b/onadata/apps/api/tests/viewsets/test_xform_viewset.py index 33a1cd9dce..c5ae3f913a 100644 --- a/onadata/apps/api/tests/viewsets/test_xform_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_xform_viewset.py @@ -60,7 +60,10 @@ from onadata.apps.logger.views import delete_xform from onadata.apps.logger.xform_instance_parser import XLSFormError from onadata.apps.main.models import MetaData -from onadata.apps.messaging.constants import FORM_UPDATED, XFORM +from onadata.apps.messaging.constants import ( + FORM_UPDATED, XFORM, PROJECT, FORM_DELETED, FORM_CREATED, + FORM_RENAMED, FORM_ACTIVE +) from onadata.apps.viewer.models import Export from onadata.libs.permissions import ( ROLES_ORDERED, @@ -1915,7 +1918,8 @@ def test_return_400_on_xlsform_error_on_list_action(self, mock_set_title): self.assertEqual(response.status_code, 400) self.assertEqual(response.content.decode("utf-8"), error_msg) - def test_partial_update(self): + @patch("onadata.apps.api.viewsets.xform_viewset.send_message") + def test_partial_update(self, mock_send_message): with HTTMock(enketo_mock): self._publish_xls_form_to_project() view = XFormViewSet.as_view({"patch": "partial_update"}) @@ -1939,6 +1943,38 @@ def test_partial_update(self): request = self.factory.patch("/", data=data, **self.extra) response = view(request, pk=self.xform.id) self.assertEqual(response.status_code, 200) + + # send messages upon form update + self.assertTrue(mock_send_message.called) + + # check calls to send_message triggered by patch request + mock_calls = mock_send_message.call_args_list + args, kwargs = mock_calls[0] + # test form rename message + message_kwargs = { + "instance_id": self.xform.id, + "target_id": self.xform.id, + "target_type": XFORM, + "user": self.xform.user, + "message_verb": FORM_RENAMED, + "custom_message": { + "old_title": "transportation_2011_07_25", + "new_title": "Hello & World!" + } + } + self.assertEqual(kwargs, message_kwargs) + + # test form status message + args, kwargs = mock_calls[1] + message_kwargs = { + "instance_id": self.xform.id, + "target_id": self.xform.id, + "target_type": XFORM, + "user": self.xform.user, + "message_verb": FORM_ACTIVE, + } + self.assertEqual(kwargs, message_kwargs) + xform_old_hash = self.xform.hash self.xform.refresh_from_db() self.assertTrue(self.xform.downloadable) @@ -2074,7 +2110,8 @@ def test_form_add_project_cache(self): cache.get(f"{PROJ_FORMS_CACHE}{self.project.pk}"), cleared_cache_content ) - def test_form_delete(self): + @patch("onadata.apps.api.viewsets.xform_viewset.send_message") + def test_form_delete(self, mock_send_message): with HTTMock(enketo_mock): self._publish_xls_form_to_project() @@ -2094,6 +2131,17 @@ def test_form_delete(self): formid = self.xform.pk request = self.factory.delete("/", **self.extra) response = view(request, pk=formid) + + # send message upon form deletion + self.assertTrue(mock_send_message.called) + mock_send_message.assert_called_with( + instance_id=self.xform.id, + target_id=self.xform.project.pk, + target_type=PROJECT, + user=request.user, + message_verb=FORM_DELETED + ) + self.assertEqual(response.data, None) self.assertEqual(response.status_code, 204) @@ -3624,8 +3672,9 @@ def test_survey_preview_endpoint(self): self.assertEqual(response.data.get("detail"), error_message) @override_settings(CELERY_TASK_ALWAYS_EAGER=True) + @patch("onadata.apps.api.viewsets.xform_viewset.send_message") @patch("onadata.apps.api.tasks.get_async_status") - def test_delete_xform_async(self, mock_get_status): + def test_delete_xform_async(self, mock_get_status, mock_send_msg): with HTTMock(enketo_mock): mock_get_status.return_value = {"job_status": "PENDING"} self._publish_xls_form_to_project() @@ -3654,6 +3703,16 @@ def test_delete_xform_async(self, mock_get_status): self.assertEqual(response.status_code, 202) self.assertEqual(response.data, {"job_status": "PENDING"}) + # send message upon form deletion + self.assertTrue(mock_send_msg.called) + mock_send_msg.assert_called_with( + instance_id=self.xform.id, + target_id=self.xform.project.pk, + target_type=XFORM, + user=request.user, + message_verb=FORM_DELETED + ) + xform = XForm.objects.get(pk=formid) self.assertIsNotNone(xform.deleted_at) diff --git a/onadata/apps/api/viewsets/export_viewset.py b/onadata/apps/api/viewsets/export_viewset.py index cb6230c61f..944010f9c6 100644 --- a/onadata/apps/api/viewsets/export_viewset.py +++ b/onadata/apps/api/viewsets/export_viewset.py @@ -6,11 +6,15 @@ """ import os +from rest_framework import status from rest_framework.mixins import DestroyModelMixin +from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.viewsets import ReadOnlyModelViewSet from onadata.apps.api.permissions import ExportDjangoObjectPermission +from onadata.apps.messaging.constants import EXPORT_DELETED, XFORM +from onadata.apps.messaging.serializers import send_message from onadata.apps.viewer.models.export import Export from onadata.libs import filters from onadata.libs.authentication import TempTokenURLParameterAuthentication @@ -57,3 +61,17 @@ def retrieve(self, request, *args, **kwargs): file_path=export.filepath, show_date=False, ) + + def destroy(self, request, *args, **kwargs): + """Deletes Export Object""" + export = self.get_object() + export_id = export.id + export.delete() + send_message( + instance_id=export_id, + target_id=export.xform.id, + target_type=XFORM, + user=request.user, + message_verb=EXPORT_DELETED, + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/onadata/apps/api/viewsets/project_viewset.py b/onadata/apps/api/viewsets/project_viewset.py index c8e6797ce7..30e528beb3 100644 --- a/onadata/apps/api/viewsets/project_viewset.py +++ b/onadata/apps/api/viewsets/project_viewset.py @@ -18,6 +18,12 @@ from onadata.apps.logger.models import Project, XForm, ProjectInvitation from onadata.apps.main.models import UserProfile from onadata.apps.main.models.meta_data import MetaData +from onadata.apps.messaging.constants import ( + USER, PROJECT, PROJECT_EDITED, XFORM, FORM_CREATED, + USER_ADDED_TO_PROJECT, USER_REMOVED_FROM_PROJECT, PROJECT_DELETED, + PROJECT_CREATED +) +from onadata.apps.messaging.serializers import send_message from onadata.libs.data import strtobool from onadata.libs.filters import AnonUserProjectFilter, ProjectOwnerFilter, TagFilter from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin @@ -102,11 +108,39 @@ def get_queryset(self): return super().get_queryset() + def create(self, request, *args, **kwargs): + """Creates new project""" + response = super().create(request, *args, **kwargs) + project = response.data + project_id = project.get("projectid") + cache.set(f"{PROJ_OWNER_CACHE}{project_id}", response.data) + + # send notification upon creating new project + send_message( + instance_id=project_id, + target_id=project_id, + target_type=PROJECT, + user=request.user, + message_verb=PROJECT_CREATED, + ) + + return response + def update(self, request, *args, **kwargs): """Updates project properties and set's cache with the updated records.""" project_id = kwargs.get("pk") response = super().update(request, *args, **kwargs) cache.set(f"{PROJ_OWNER_CACHE}{project_id}", response.data) + + # send notification upon updating project details + send_message( + instance_id=project_id, + target_id=project_id, + target_type=PROJECT, + user=request.user, + message_verb=PROJECT_EDITED, + ) + return response def retrieve(self, request, *args, **kwargs): @@ -157,6 +191,14 @@ def forms(self, request, **kwargs): propagate_project_permissions_async.apply_async( args=[project.id], countdown=30 ) + # send form publish notification + send_message( + instance_id=survey.id, + target_id=survey.id, + target_type=XFORM, + user=request.user, + message_verb=FORM_CREATED, + ) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -181,26 +223,30 @@ def share(self, request, *args, **kwargs): remove = strtobool(remove) if remove: - serializer = RemoveUserFromProjectSerializer(data={**data, remove: True}) + serializer = RemoveUserFromProjectSerializer(data=data) + message_verb = USER_REMOVED_FROM_PROJECT else: serializer = ShareProjectSerializer(data=data) + message_verb = USER_ADDED_TO_PROJECT if serializer.is_valid(): serializer.save() email_msg = data.get("email_msg") - if email_msg: - # send out email message. - try: - user = serializer.instance.user - except AttributeError: - for instance in serializer.instance: - user = instance.user + try: + user = serializer.instance.user + except AttributeError: + for instance in serializer.instance: + user = instance.user + if email_msg: + # send email if email_msg is present in payload send_mail( SHARE_PROJECT_SUBJECT.format(self.object.name), email_msg, DEFAULT_FROM_EMAIL, (user.email,), ) - else: + else: + if email_msg: + # send email if email_msg is present in payload send_mail( SHARE_PROJECT_SUBJECT.format(self.object.name), email_msg, @@ -212,6 +258,14 @@ def share(self, request, *args, **kwargs): # clear cache safe_delete(f"{PROJ_OWNER_CACHE}{self.object.pk}") + # send message upon sharing/unsharing project with user + send_message( + instance_id=self.object.pk, + target_id=self.object.pk, + target_type=PROJECT, + user=user, + message_verb=message_verb, + ) return Response(status=status.HTTP_204_NO_CONTENT) @@ -313,4 +367,13 @@ def destroy(self, request, *args, **kwargs): user = request.user project.soft_delete(user) + # send notification to user target upon project deletion + send_message( + instance_id=project.pk, + target_id=user.id, + target_type=USER, + user=user, + message_verb=PROJECT_DELETED, + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/onadata/apps/api/viewsets/xform_viewset.py b/onadata/apps/api/viewsets/xform_viewset.py index acd7608798..2b92a724ff 100644 --- a/onadata/apps/api/viewsets/xform_viewset.py +++ b/onadata/apps/api/viewsets/xform_viewset.py @@ -52,7 +52,17 @@ from onadata.apps.logger.models.xform import XForm, XFormUserObjectPermission from onadata.apps.logger.models.xform_version import XFormVersion from onadata.apps.logger.xform_instance_parser import XLSFormError -from onadata.apps.messaging.constants import FORM_UPDATED, XFORM +from onadata.apps.messaging.constants import ( + FORM_UPDATED, + EXPORT_CREATED, + XFORM, + FORM_DELETED, + FORM_CREATED, + PROJECT, + FORM_ACTIVE, + FORM_INACTIVE, + FORM_RENAMED +) from onadata.apps.messaging.serializers import send_message from onadata.apps.viewer.models.export import Export from onadata.libs import authentication, filters @@ -83,7 +93,7 @@ response_for_format, ) from onadata.libs.utils.cache_tools import PROJ_OWNER_CACHE, safe_delete -from onadata.libs.utils.common_tools import json_stream +from onadata.libs.utils.common_tools import json_stream, str_to_bool from onadata.libs.utils.csv_import import ( get_async_csv_submission_status, submission_xls_to_csv, @@ -229,7 +239,7 @@ def parse_webform_return_url(return_url, request): return None -# pylint: disable=too-many-ancestors +# pylint: disable=too-many-ancestors, too-many-lines class XFormViewSet( AnonymousUserPublicFormsMixin, CacheControlMixin, @@ -346,6 +356,15 @@ def create(self, request, *args, **kwargs): serializer = XFormCreateSerializer(survey, context={"request": request}) headers = self.get_success_headers(serializer.data) + # send form creation notification + send_message( + instance_id=survey.id, + target_id=survey.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_CREATED, + ) + return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) @@ -389,19 +408,24 @@ def create_async(self, request, *args, **kwargs): ) else: xls_file_path = request.FILES.get("xls_file").temporary_file_path() - - resp.update( - { - "job_uuid": tasks.publish_xlsform_async.delay( - request.user.id, - request.POST, - owner.id, - {"name": fname, "path": xls_file_path}, - ).task_id - } + survey = tasks.publish_xlsform_async.delay( + request.user.id, + request.POST, + owner.id, + {"name": fname, "path": xls_file_path}, ) + resp.update({"job_uuid": survey.task_id}) resp_code = status.HTTP_202_ACCEPTED + # send form creation notification + send_message( + instance_id=survey.id, + target_id=survey.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_CREATED, + ) + return Response(data=resp, status=resp_code, headers=headers) @action(methods=["GET", "HEAD"], detail=True) @@ -817,6 +841,33 @@ def partial_update(self, request, *args, **kwargs): return _try_update_xlsform(request, self.object, owner) try: + # send notification for each form activity + if request.POST.get("title"): + # send form update notification + send_message( + instance_id=self.object.pk, + target_id=self.object.pk, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_RENAMED, + custom_message={ + "old_title": self.object.title, + "new_title": request.POST.get("title"), + }, + ) + if request.POST.get("downloadable"): + downloadable = request.POST.get("downloadable") + message_verb = ( + FORM_ACTIVE if str_to_bool(downloadable) else FORM_INACTIVE + ) + # send form status notification + send_message( + instance_id=self.object.pk, + target_id=self.object.pk, + target_type=XFORM, + user=request.user or owner, + message_verb=message_verb, + ) return super().partial_update(request, *args, **kwargs) except XLSFormError as e: raise ParseError(str(e)) from e @@ -837,6 +888,15 @@ def delete_async(self, request, *args, **kwargs): safe_delete(f"{PROJ_OWNER_CACHE}{xform.project.pk}") resp_code = status.HTTP_202_ACCEPTED + # send form deletion notification to project target + send_message( + instance_id=xform.id, + target_id=xform.project.id, + target_type=XFORM, + user=request.user, + message_verb=FORM_DELETED, + ) + elif request.method == "GET": job_uuid = request.query_params.get("job_uuid") resp = tasks.get_async_status(job_uuid) @@ -852,6 +912,15 @@ def destroy(self, request, *args, **kwargs): user = request.user xform.soft_delete(user=user) + # send form deletion notification to project target + send_message( + instance_id=xform.id, + target_id=xform.project.pk, + target_type=PROJECT, + user=user, + message_verb=FORM_DELETED, + ) + return Response(status=status.HTTP_204_NO_CONTENT) @action(methods=["GET"], detail=True) @@ -941,6 +1010,14 @@ def export_async(self, request, *args, **kwargs): # pylint: disable=attribute-defined-outside-init self.etag_data = f"{timezone.now()}" + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user, + message_verb=EXPORT_CREATED, + ) + return Response( data=resp, status=status.HTTP_202_ACCEPTED, diff --git a/onadata/apps/main/views.py b/onadata/apps/main/views.py index a2c19d9775..8953459f81 100644 --- a/onadata/apps/main/views.py +++ b/onadata/apps/main/views.py @@ -57,6 +57,8 @@ UserProfileForm, ) from onadata.apps.main.models import AuditLog, MetaData, UserProfile +from onadata.apps.messaging.constants import FORM_UPDATED, XFORM +from onadata.apps.messaging.serializers import send_message from onadata.apps.sms_support.autodoc import get_autodoc_for from onadata.apps.sms_support.providers import providers_doc from onadata.apps.sms_support.tools import check_form_sms_compatibility, is_sms_related @@ -699,6 +701,16 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) + xform.description = request.POST["description"] elif request.POST.get("title"): audit = {"xform": xform.id_string} @@ -718,6 +730,16 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) + xform.title = request.POST["title"] elif request.POST.get("toggle_shared"): if request.POST["toggle_shared"] == "data": @@ -735,6 +757,16 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) + xform.shared_data = not xform.shared_data elif request.POST["toggle_shared"] == "form": audit = {"xform": xform.id_string} @@ -751,6 +783,16 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) + xform.shared = not xform.shared elif request.POST["toggle_shared"] == "active": audit = {"xform": xform.id_string} @@ -767,6 +809,16 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) + xform.downloadable = not xform.downloadable elif request.POST.get("form-license"): audit = {"xform": xform.id_string} @@ -782,6 +834,14 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) MetaData.form_license(xform, request.POST["form-license"]) elif request.POST.get("data-license"): audit = {"xform": xform.id_string} @@ -797,6 +857,16 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) + MetaData.data_license(xform, request.POST["data-license"]) elif request.POST.get("source") or request.FILES.get("source"): audit = {"xform": xform.id_string} @@ -809,6 +879,16 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) + MetaData.source( xform, request.POST.get("source"), request.FILES.get("source") ) @@ -831,6 +911,14 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) # stored previous states to be able to rollback form status # in case we can't save. previous_allow_sms = xform.allows_sms @@ -861,6 +949,16 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) + for media_file in request.FILES.getlist("media"): MetaData.media_upload(xform, media_file) elif request.POST.get("map_name"): @@ -886,6 +984,16 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) + MetaData.supporting_docs(xform, request.FILES.get("doc")) elif request.POST.get("template_token") and request.POST.get("template_token"): template_name = request.POST.get("template_name") @@ -899,6 +1007,16 @@ def edit(request, username, id_string): # noqa C901 audit, request, ) + + # send form update notification + send_message( + instance_id=xform.id, + target_id=xform.id, + target_type=XFORM, + user=request.user or owner, + message_verb=FORM_UPDATED, + ) + merged = template_name + "|" + template_token MetaData.external_export(xform, merged) elif request.POST.get("external_url") and request.FILES.get("xls_template"): diff --git a/onadata/apps/messaging/constants.py b/onadata/apps/messaging/constants.py index dfe7599ddf..0f2f86cf2d 100644 --- a/onadata/apps/messaging/constants.py +++ b/onadata/apps/messaging/constants.py @@ -12,6 +12,7 @@ XFORM = text('xform') PROJECT = text('project') USER = text('user') +EXPORT = text('export') APP_LABEL_MAPPING = { XFORM: 'logger', @@ -21,18 +22,50 @@ MESSAGE = 'message' UNKNOWN_TARGET = _("Unknown target.") +USER_ADDED_TO_PROJECT = "user_added_to_project" +USER_REMOVED_FROM_PROJECT = "user_removed_from_project" +PROJECT_CREATED = "project_created" +PROJECT_EDITED = "project_edited" +PROJECT_SHARED = "project_shared" +PROJECT_DELETED = "project_deleted" SUBMISSION_CREATED = "submission_created" SUBMISSION_EDITED = "submission_edited" SUBMISSION_DELETED = "submission_deleted" SUBMISSION_REVIEWED = "submission_reviewed" FORM_UPDATED = "form_updated" +FORM_CREATED = "form_created" +FORM_DELETED = "form_deleted" +FORM_INACTIVE = "form_inactive" +FORM_ACTIVE = "form_active" +FORM_RENAMED = "form_renamed" +EXPORT_CREATED = "export_created" +EXPORT_DELETED = "export_deleted" +PERMISSION_GRANTED = "permission_granted" +PERMISSION_REVOKED = "permission_revoked" MESSAGE_VERBS = [ MESSAGE, SUBMISSION_REVIEWED, SUBMISSION_CREATED, SUBMISSION_EDITED, - SUBMISSION_DELETED, FORM_UPDATED] + SUBMISSION_DELETED, FORM_UPDATED, FORM_CREATED, FORM_DELETED, + EXPORT_CREATED, EXPORT_DELETED, PROJECT_EDITED, PROJECT_SHARED, + PROJECT_CREATED, USER_ADDED_TO_PROJECT, USER_REMOVED_FROM_PROJECT, + PROJECT_DELETED, FORM_INACTIVE, FORM_ACTIVE +] VERB_TOPIC_DICT = { SUBMISSION_CREATED: "submission/created", SUBMISSION_EDITED: "submission/edited", SUBMISSION_DELETED: "submission/deleted", SUBMISSION_REVIEWED: "submission/reviewed", - FORM_UPDATED: "form/updated" + FORM_UPDATED: "form/updated", + FORM_CREATED: "form/created", + FORM_DELETED: "form/deleted", + FORM_ACTIVE: "form/active", + FORM_RENAMED: "form/renamed", + FORM_INACTIVE: "form/inactive", + EXPORT_CREATED: "export/created", + EXPORT_DELETED: "export/deleted", + PROJECT_CREATED: "project/created", + PROJECT_EDITED: "project/edited", + PROJECT_SHARED: "project/shared", + PROJECT_DELETED: "project/deleted", + USER_ADDED_TO_PROJECT: "user/added", + USER_REMOVED_FROM_PROJECT: "user/removed", } diff --git a/onadata/apps/messaging/serializers.py b/onadata/apps/messaging/serializers.py index 7356252c5d..148f344952 100644 --- a/onadata/apps/messaging/serializers.py +++ b/onadata/apps/messaging/serializers.py @@ -147,12 +147,14 @@ def create(self, validated_data): return instance +# pylint: disable=too-many-arguments def send_message( instance_id: Union[list, int], target_id: int, target_type: str, user: User, message_verb: str, + custom_message: dict = None ): """ Send a message. @@ -182,12 +184,21 @@ def send_message( ids = instance_id while len(ids) > 0: data["message"] = json.dumps({"id": ids[:message_id_limit]}) + if custom_message: + message_dict = json.loads(data["message"]) + message_dict.update(custom_message) + data["message"] = json.dumps(message_dict) message = MessageSerializer(data=data, context={"request": request}) del ids[:message_id_limit] if message.is_valid(): message.save() else: data["message"] = json.dumps({"id": instance_id}) + if custom_message: + # update default message dict with extra fields + message_dict = json.loads(data["message"]) + message_dict.update(custom_message) + data["message"] = json.dumps(message_dict) message = MessageSerializer(data=data, context={"request": request}) if message.is_valid(): message.save() diff --git a/onadata/apps/messaging/tests/test_utils.py b/onadata/apps/messaging/tests/test_utils.py index 11e0336270..0652362583 100644 --- a/onadata/apps/messaging/tests/test_utils.py +++ b/onadata/apps/messaging/tests/test_utils.py @@ -1,12 +1,18 @@ """ Tests messaging app utils """ -from django.test.utils import override_settings +import json from unittest.mock import patch +from django.http.request import HttpRequest +from django.test.utils import override_settings + + from onadata.apps.main.tests.test_base import TestBase from onadata.apps.messaging.serializers import send_message -from onadata.apps.messaging.constants import SUBMISSION_DELETED +from onadata.apps.messaging.constants import ( + SUBMISSION_DELETED, FORM_RENAMED +) class TestMessagingUtils(TestBase): @@ -32,3 +38,43 @@ def is_valid(): self.xform.id, 'xform', self.user, SUBMISSION_DELETED) self.assertTrue(message_serializer_mock.called) self.assertEqual(message_serializer_mock.call_count, 2) + + @patch('onadata.apps.messaging.serializers.MessageSerializer') + def test_custom_message(self, message_serializer_mock): + """ + Test custom_message passed into send_message function + """ + def is_valid(): + return True + message_serializer_mock.is_valid.side_effect = is_valid + self._create_user_and_login() + self._publish_transportation_form() + instance_id = [1] + custom_message = { + "old_title": "first title", + "new_title": "second title" + } + send_message( + instance_id, + self.xform.id, + 'xform', + self.user, + FORM_RENAMED, + custom_message + ) + self.assertTrue(message_serializer_mock.called) + self.assertEqual(message_serializer_mock.call_count, 1) + request = HttpRequest() + data = { + "target_id": self.xform.id, + "target_type": 'xform', + "verb": FORM_RENAMED, + "message": json.dumps({ + "id": [self.xform.id], + "old_title": "first title", + "new_title": "second title" + }) + } + message_serializer_mock.called_with( + data=data, context={"request": request} + )