diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 67c3b105..5984e842 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,8 @@ Added - Python v3.11 support (#1922, #1978) - **Landingzones** - REST API list view pagination (#1994) + - ``notify_email_zone_status`` user app setting (#1939) + - Tests for taskflow tasks (#1916) - **Samplesheets** - REST API list view pagination (#1994) - ``notify_email_irods_request`` user app setting (#1939) diff --git a/landingzones/plugins.py b/landingzones/plugins.py index 482a2e5e..cd4285ea 100644 --- a/landingzones/plugins.py +++ b/landingzones/plugins.py @@ -72,7 +72,16 @@ class ProjectAppPlugin( 'new files are uploaded from landing zones', 'user_modifiable': True, 'default': True, - } + }, + 'notify_email_zone_status': { + 'scope': SODAR_CONSTANTS['APP_SETTING_SCOPE_USER'], + 'type': 'BOOLEAN', + 'default': True, + 'label': 'Receive email for landing zone status updates', + 'description': 'Receive email notifications for status changes in ' + 'your landing zones', + 'user_modifiable': True, + }, } #: Iconify icon diff --git a/landingzones/tasks_taskflow.py b/landingzones/tasks_taskflow.py index 875fee5b..a41e4c4d 100644 --- a/landingzones/tasks_taskflow.py +++ b/landingzones/tasks_taskflow.py @@ -287,6 +287,9 @@ def set_status(cls, zone, flow_name, status, status_info, extra_data=None): settings.PROJECTROLES_SEND_EMAIL and flow_name == 'landing_zone_move' and not validate_only + and app_settings.get( + APP_NAME, 'notify_email_zone_status', user=zone.user + ) ): try: cls._send_owner_move_email(zone) @@ -304,16 +307,10 @@ def set_status(cls, zone, flow_name, status, status_info, extra_data=None): and zone.status == ZONE_STATUS_MOVED and file_count > 0 ): - members = list( - set( - [ - r.user - for r in zone.project.get_roles() - if r.user != zone.user - ] - ) - ) - for member in members: + members = [ + r.user for r in zone.project.get_roles() if r.user != zone.user + ] + for member in list(set(members)): if app_alerts: try: cls._add_member_move_alert( diff --git a/landingzones/tests/test_models.py b/landingzones/tests/test_models.py index eed8016a..950aed34 100644 --- a/landingzones/tests/test_models.py +++ b/landingzones/tests/test_models.py @@ -67,6 +67,7 @@ def make_landing_zone( 'description': description, 'user_message': user_message, 'status': status, + 'status_info': DEFAULT_STATUS_INFO[status], 'configuration': configuration, 'config_data': config_data, } diff --git a/landingzones/tests/test_tasks_taskflow.py b/landingzones/tests/test_tasks_taskflow.py new file mode 100644 index 00000000..bb2fb753 --- /dev/null +++ b/landingzones/tests/test_tasks_taskflow.py @@ -0,0 +1,229 @@ +"""Taskflow task tests for the landingzones app""" + +# NOTE: These do not NOT require running with taskflow and iRODS + +from django.core import mail +from django.test import override_settings + +# Projectroles dependency +from projectroles.app_settings import AppSettingAPI + +# Appalerts dependency +from appalerts.models import AppAlert + +import landingzones.constants as lc +from landingzones.tasks_taskflow import SetLandingZoneStatusTask +from landingzones.tests.test_views import TestViewsBase + + +app_settings = AppSettingAPI() + + +# Local constants +APP_NAME = 'landingzones' +TASK_NAME = 'set landing zone status' + + +class TestSetLandingZoneStatusTask(TestViewsBase): + """Tests for SetLandingZoneStatusTask""" + + def _get_task(self, force_fail=False): + """Initialize and return task""" + return SetLandingZoneStatusTask(TASK_NAME, self.project, force_fail) + + def _assert_owner_alert(self, count, name='zone_move'): + """Assert owner alert count""" + self.assertEqual( + AppAlert.objects.filter( + app_plugin__name=APP_NAME, + alert_name=name, + user=self.landing_zone.user, + project=self.project, + ).count(), + count, + ) + + def _assert_member_alerts(self, count): + """Assert member alert count""" + self.assertEqual( + AppAlert.objects.filter( + app_plugin__name=APP_NAME, + alert_name='zone_move_member', + project=self.project, + ).count(), + count, + ) + + def setUp(self): + super().setUp() + self.task_kw = { + 'landing_zone': self.landing_zone, + 'flow_name': 'landing_zone_move', + 'status': lc.ZONE_STATUS_MOVED, + 'status_info': lc.DEFAULT_STATUS_INFO[lc.ZONE_STATUS_MOVED], + 'extra_data': {'file_count': 1}, + } + + def test_execute(self): + """Test SetLandingZoneStatusTask execute()""" + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_ACTIVE) + self.assertEqual( + self.landing_zone.status_info, + lc.DEFAULT_STATUS_INFO[lc.ZONE_STATUS_ACTIVE], + ) + self._assert_owner_alert(0) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) + + self._get_task().execute(**self.task_kw) + self.landing_zone.refresh_from_db() + + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_MOVED) + self.assertEqual( + self.landing_zone.status_info, + lc.DEFAULT_STATUS_INFO[lc.ZONE_STATUS_MOVED], + ) + self._assert_owner_alert(1) + self._assert_member_alerts(1) + self.assertEqual(len(mail.outbox), 2) + self.assertEqual( + AppAlert.objects.filter(alert_name='zone_move').first().level, + 'SUCCESS', + ) + + @override_settings(PROJECTROLES_SEND_EMAIL=False) + def test_execute_disable_email(self): + """Test execute() with email disabled""" + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_ACTIVE) + self._assert_owner_alert(0) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) + self._get_task().execute(**self.task_kw) + self.landing_zone.refresh_from_db() + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_MOVED) + self._assert_owner_alert(1) + self._assert_member_alerts(1) + self.assertEqual(len(mail.outbox), 0) + + def test_execute_disable_member_nofify(self): + """Test execute() with member notify disabled""" + app_settings.set( + APP_NAME, 'member_notify_move', False, project=self.project + ) + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_ACTIVE) + self._assert_owner_alert(0) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) + self._get_task().execute(**self.task_kw) + self.landing_zone.refresh_from_db() + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_MOVED) + self._assert_owner_alert(1) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].recipients(), [self.user.email]) + + def test_execute_disable_owner_nofify(self): + """Test execute() with owner notify disabled""" + app_settings.set( + APP_NAME, + 'notify_email_zone_status', + False, + user=self.landing_zone.user, + ) + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_ACTIVE) + self._assert_owner_alert(0) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) + self._get_task().execute(**self.task_kw) + self.landing_zone.refresh_from_db() + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_MOVED) + self._assert_owner_alert(1) + self._assert_member_alerts(1) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual( + mail.outbox[0].recipients(), [self.user_contributor.email] + ) + + def test_execute_moved_no_files(self): + """Test execute() with a busy status""" + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_ACTIVE) + self._assert_owner_alert(0) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) + self.task_kw['extra_data'] = {'file_count': 0} + self._get_task().execute(**self.task_kw) + self.landing_zone.refresh_from_db() + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_MOVED) + # No alerts or emails should be set + self._assert_owner_alert(0) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) + + def test_execute_busy(self): + """Test execute() with busy status""" + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_ACTIVE) + self._assert_owner_alert(0) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) + self.task_kw['status'] = lc.ZONE_STATUS_MOVING + self.task_kw['status_info'] = lc.DEFAULT_STATUS_INFO[ + lc.ZONE_STATUS_MOVING + ] + self._get_task().execute(**self.task_kw) + self.landing_zone.refresh_from_db() + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_MOVING) + self.assertEqual( + self.landing_zone.status_info, + lc.DEFAULT_STATUS_INFO[lc.ZONE_STATUS_MOVING], + ) + # No alerts or emails should be set + self._assert_owner_alert(0) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) + + def test_execute_failed(self): + """Test execute() with FAILED status""" + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_ACTIVE) + self._assert_owner_alert(0) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) + self.task_kw['status'] = lc.ZONE_STATUS_FAILED + self.task_kw['status_info'] = lc.DEFAULT_STATUS_INFO[ + lc.ZONE_STATUS_FAILED + ] + self._get_task().execute(**self.task_kw) + self.landing_zone.refresh_from_db() + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_FAILED) + self.assertEqual( + self.landing_zone.status_info, + lc.DEFAULT_STATUS_INFO[lc.ZONE_STATUS_FAILED], + ) + self._assert_owner_alert(1) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual( + AppAlert.objects.filter(alert_name='zone_move').first().level, + 'DANGER', + ) + + def test_execute_validate(self): + """Test execute() in validate mode""" + self.assertEqual(self.landing_zone.status, lc.ZONE_STATUS_ACTIVE) + self._assert_owner_alert(0) + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) + self.task_kw['status'] = lc.ZONE_STATUS_ACTIVE + self.task_kw['status_info'] = [ + lc.DEFAULT_STATUS_INFO[lc.ZONE_STATUS_ACTIVE] + ] + self.task_kw['extra_data'] = {'validate_only': True} + self._get_task().execute(**self.task_kw) + self.landing_zone.refresh_from_db() + self.task_kw['status'] = lc.ZONE_STATUS_ACTIVE + self.task_kw['status_info'] = [ + lc.DEFAULT_STATUS_INFO[lc.ZONE_STATUS_ACTIVE] + ] + self._assert_owner_alert(0) + self._assert_owner_alert(1, name='zone_validate') + self._assert_member_alerts(0) + self.assertEqual(len(mail.outbox), 0) # No email sent on validate diff --git a/samplesheets/plugins.py b/samplesheets/plugins.py index e11c15e5..9179bc8c 100644 --- a/samplesheets/plugins.py +++ b/samplesheets/plugins.py @@ -64,6 +64,9 @@ PROJECT_ACTION_UPDATE = SODAR_CONSTANTS['PROJECT_ACTION_UPDATE'] APP_SETTING_SCOPE_PROJECT = SODAR_CONSTANTS['APP_SETTING_SCOPE_PROJECT'] APP_SETTING_SCOPE_USER = SODAR_CONSTANTS['APP_SETTING_SCOPE_USER'] +APP_SETTING_SCOPE_PROJECT_USER = SODAR_CONSTANTS[ + 'APP_SETTING_SCOPE_PROJECT_USER' +] # Local constants SHEETS_INFO_SETTINGS = [ @@ -124,7 +127,7 @@ class ProjectAppPlugin( 'default': True, }, 'display_config': { - 'scope': APP_SETTING_SCOPE_USER, + 'scope': APP_SETTING_SCOPE_PROJECT_USER, 'type': 'JSON', 'label': 'Sample sheet display configuration', 'description': 'User specific JSON configuration for column '