diff --git a/kobo/apps/project_views/views.py b/kobo/apps/project_views/views.py index e218d39bb9..a00f972a08 100644 --- a/kobo/apps/project_views/views.py +++ b/kobo/apps/project_views/views.py @@ -20,12 +20,12 @@ from kpi.permissions import IsAuthenticated from kpi.serializers.v2.asset import AssetMetadataListSerializer from kpi.serializers.v2.user import UserListSerializer +from kpi.tasks import export_task_in_background from kpi.utils.object_permission import get_database_user from kpi.utils.project_views import ( get_region_for_view, user_has_view_perms, ) -from kpi.tasks import project_view_export_in_background from .models.project_view import ProjectView from .serializers import ProjectViewSerializer @@ -110,9 +110,10 @@ def export(self, request, uid, obj_type): ) # Have Celery run the export in the background - project_view_export_in_background.delay( + export_task_in_background.delay( export_task_uid=export_task.uid, username=user.username, + export_task_name='kpi.ProjectViewExportTask', ) return Response({'status': export_task.status}) diff --git a/kpi/tasks.py b/kpi/tasks.py index 380b6fe939..050b73108a 100644 --- a/kpi/tasks.py +++ b/kpi/tasks.py @@ -3,8 +3,9 @@ import constance import requests +from django.apps import apps from django.conf import settings -from django.core.mail import send_mail +from django.core import mail from django.core.management import call_command from kobo.apps.kobo_auth.shortcuts import User @@ -13,7 +14,7 @@ from kpi.constants import LIMIT_HOURS_23 from kpi.maintenance_tasks import remove_old_asset_snapshots, remove_old_import_tasks from kpi.models.asset import Asset -from kpi.models.import_export_task import ExportTask, ImportTask, ProjectViewExportTask +from kpi.models.import_export_task import ExportTask, ImportTask @celery_app.task @@ -29,12 +30,12 @@ def export_in_background(export_task_uid): @celery_app.task -def project_view_export_in_background( - export_task_uid: str, username: str +def export_task_in_background( + export_task_uid: str, username: str, export_task_name: str ) -> None: user = User.objects.get(username=username) - - export_task = ProjectViewExportTask.objects.get(uid=export_task_uid) + export_task_class = apps.get_model(export_task_name) + export_task = export_task_class.objects.get(uid=export_task_uid) export = export_task.run() if export.status == 'complete' and export.result: file_url = f'{settings.KOBOFORM_URL}{export.result.url}' @@ -44,7 +45,7 @@ def project_view_export_in_background( 'Regards,\n' 'KoboToolbox' ) - send_mail( + mail.send_mail( subject='Project View Report Complete', message=msg, from_email=constance.config.SUPPORT_EMAIL, diff --git a/kpi/tests/test_export_tasks.py b/kpi/tests/test_export_tasks.py new file mode 100644 index 0000000000..ceb089f385 --- /dev/null +++ b/kpi/tests/test_export_tasks.py @@ -0,0 +1,105 @@ +import datetime +from unittest.mock import Mock, patch + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.test import TestCase + +from kobo.apps.kobo_auth.shortcuts import User +from kpi.models.import_export_task import ProjectViewExportTask +from kpi.tasks import export_task_in_background + + +@patch('django.core.mail.send_mail') +@patch('kobo.apps.project_views.models.project_view.ProjectView.objects.get') +class ExportTaskInBackgroundTests(TestCase): + def setUp(self): + self.user = User.objects.create(username='someuser', email='test@example.com') + self.mock_data = {'type': 'assets', 'view': 'summary'} + self.task = ProjectViewExportTask.objects.create( + uid='test_uid', data=self.mock_data, user=self.user + ) + self.project_view = Mock() + self.project_view.get_countries.return_value = [] + + def test_export_task_success(self, mock_get_project_view, mock_send_mail): + mock_get_project_view.return_value = self.project_view + self.task.run = Mock(return_value=self.task) + + export_task_in_background( + self.task.uid, self.user.username, 'kpi.ProjectViewExportTask' + ) + + self.task.refresh_from_db() + self.assertEqual(self.task.status, 'complete') + + root_url = settings.KOBOFORM_URL + expected_message = ( + 'Hello {},\n\n' + 'Your report is complete: {}' + '/private-media/{}/exports/' + 'assets-{}-view_summary-{}.csv\n\n' + 'Regards,\n' + 'KoboToolbox' + ).format( + self.user.username, + root_url, + self.user.username, + self.user.username, + datetime.datetime.now().strftime('%Y-%m-%dT%H%M%SZ'), + ) + mock_send_mail.assert_called_once_with( + subject='Project View Report Complete', + message=expected_message, + from_email='help@kobotoolbox.org', + recipient_list=['test@example.com'], + fail_silently=False, + ) + + def test_invalid_export_task_uid(self, mock_get_project_view, mock_send_mail): + mock_get_project_view.side_effect = ObjectDoesNotExist + with self.assertRaisesMessage( + ObjectDoesNotExist, 'ProjectViewExportTask matching query does not exist.' + ): + export_task_in_background( + 'invalid_uid', self.user.username, 'kpi.ProjectViewExportTask' + ) + + mock_send_mail.assert_not_called() + + def test_invalid_username(self, mock_get_project_view, mock_send_mail): + with self.assertRaisesMessage( + ObjectDoesNotExist, 'User matching query does not exist.' + ): + export_task_in_background( + self.task.uid, 'invalid_username', 'kpi.ProjectViewExportTask' + ) + + mock_send_mail.assert_not_called() + + @patch('kpi.models.ProjectViewExportTask._run_task') + def test_export_task_error( + self, mock_run_task, mock_get_project_view, mock_send_mail + ): + mock_get_project_view.return_value = self.project_view + mock_run_task.side_effect = Exception('Simulated task failure') + + export_task_in_background( + self.task.uid, self.user.username, 'kpi.ProjectViewExportTask' + ) + + self.task.refresh_from_db() + self.assertEqual(self.task.status, 'error') + + @patch('kpi.models.ProjectViewExportTask._run_task') + def test_email_not_sent_if_export_errors( + self, mock_run_task, mock_get_project_view, mock_send_mail + ): + mock_get_project_view.return_value = self.project_view + mock_run_task.side_effect = Exception('Simulated task failure') + + export_task_in_background( + self.task.uid, self.user.username, 'kpi.ProjectViewExportTask' + ) + + mock_send_mail.assert_not_called()