From b562ec2b02a04743996b0a9fe0e056371970d40b Mon Sep 17 00:00:00 2001 From: Mikko Nieminen Date: Mon, 20 Jan 2025 13:07:49 +0100 Subject: [PATCH] add project delete ui view (#1090) --- CHANGELOG.rst | 1 + docs/source/major_changes.rst | 1 + projectroles/rules.py | 6 + .../projectroles/project_confirm_delete.html | 40 ++ .../templates/projectroles/project_form.html | 8 +- projectroles/tests/test_permissions.py | 100 ++++ projectroles/tests/test_views.py | 498 +++++++++++------- projectroles/urls.py | 5 + projectroles/views.py | 60 ++- 9 files changed, 519 insertions(+), 200 deletions(-) create mode 100644 projectroles/templates/projectroles/project_confirm_delete.html diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 77b7a41d..ddddbe6c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -29,6 +29,7 @@ Added - ``SiteAppSettingsFormView`` view (#1304) - ``SODARAppSettingFormMixin`` form helper mixin (#1545) - Old owner "remove role" option in ``RoleAssignmentOwnerTransferForm`` (#836) + - Project deletion (#1090) Changed ------- diff --git a/docs/source/major_changes.rst b/docs/source/major_changes.rst index ee025b0f..7edb58a9 100644 --- a/docs/source/major_changes.rst +++ b/docs/source/major_changes.rst @@ -16,6 +16,7 @@ v1.1.0 (WIP) Release Highlights ================== +- Add project deletion - Add site read-only mode - Add siteappsettings site app plugin - Add removeroles management command diff --git a/projectroles/rules.py b/projectroles/rules.py index 401ec986..b4f4d621 100644 --- a/projectroles/rules.py +++ b/projectroles/rules.py @@ -206,6 +206,12 @@ def is_site_writable(): is_project_update_user & is_site_writable, ) +# Allow project deletion +rules.add_perm( + 'projectroles.delete_project', + is_project_update_user & is_site_writable, +) + # Allow creation of projects rules.add_perm( 'projectroles.create_project', is_project_create_user & can_create_projects diff --git a/projectroles/templates/projectroles/project_confirm_delete.html b/projectroles/templates/projectroles/project_confirm_delete.html new file mode 100644 index 00000000..bdf1826d --- /dev/null +++ b/projectroles/templates/projectroles/project_confirm_delete.html @@ -0,0 +1,40 @@ +{% extends 'projectroles/project_base.html' %} + +{% load rules %} +{% load projectroles_tags %} +{% load projectroles_common_tags %} + +{% block title %} + Confirm Deletion of {% get_display_name object.type True %} +{% endblock title %} + +{% block projectroles_extend %} + +
+

Confirm Deletion of {% get_display_name object.type True %}

+ + + + {# TODO: Add host name verification here #} + +
+ {% csrf_token %} +
+ + Cancel + + +
+
+
+ +{% endblock projectroles_extend %} diff --git a/projectroles/templates/projectroles/project_form.html b/projectroles/templates/projectroles/project_form.html index f5872a6c..88db9e7f 100644 --- a/projectroles/templates/projectroles/project_form.html +++ b/projectroles/templates/projectroles/project_form.html @@ -42,7 +42,13 @@

Update {% get_display_name object.type title=True %}

{% if object.type == 'PROJECT' %} -
+
+ {# TODO: Add logic to disable button with tooltip #} + + Delete + diff --git a/projectroles/tests/test_permissions.py b/projectroles/tests/test_permissions.py index 142fa6fa..f33b6115 100644 --- a/projectroles/tests/test_permissions.py +++ b/projectroles/tests/test_permissions.py @@ -908,6 +908,106 @@ def test_get_category_anon(self): self.assert_response(self.url_cat, self.user_no_roles, 302) +class TestProjectDeleteView(ProjectPermissionTestBase): + """Tests for ProjectDeleteView permissions""" + + def setUp(self): + super().setUp() + self.url = reverse( + 'projectroles:delete', kwargs={'project': self.project.sodar_uuid} + ) + self.url_cat = reverse( + 'projectroles:delete', kwargs={'project': self.category.sodar_uuid} + ) + self.good_users = [ + self.superuser, + self.user_owner_cat, + self.user_delegate_cat, + self.user_owner, + self.user_delegate, + ] + self.bad_users = [ + self.user_contributor_cat, + self.user_guest_cat, + self.user_finder_cat, + self.user_contributor, + self.user_guest, + self.user_no_roles, + self.anonymous, + ] + + def test_get(self): + """Test ProjectDeleteView GET""" + self.assert_response(self.url, self.good_users, 200) + self.assert_response(self.url, self.bad_users, 302) + self.project.set_public() + self.assert_response(self.url, self.user_no_roles, 302) + + @override_settings(PROJECTROLES_ALLOW_ANONYMOUS=True) + def test_get_anon(self): + """Test GET with anonymous access""" + self.project.set_public() + self.assert_response(self.url, self.user_no_roles, 302) + + def test_get_archive(self): + """Test GET with archived project""" + self.project.set_archive() + self.assert_response(self.url, self.good_users, 200) + self.assert_response(self.url, self.bad_users, 302) + self.project.set_public() + self.assert_response(self.url, self.user_no_roles, 302) + + def test_get_read_only(self): + """Test GET with site read-only mode""" + self.set_site_read_only() + self.assert_response(self.url, self.superuser, 200) + self.assert_response(self.url, self.non_superusers, 302) + + # TODO: Test category with and without children once checks are implemented + ''' + def test_get_category(self): + """Test GET with category""" + bad_users_cat = [ + self.superuser, + self.user_owner_cat, + self.user_delegate_cat, + ] + bad_users_non_cat = [ + self.user_contributor_cat, + self.user_guest_cat, + self.user_finder_cat, + self.user_owner, + self.user_delegate, + self.user_contributor, + self.user_guest, + self.user_no_roles, + self.anonymous, + ] + self.assert_response( + self.url_cat, + bad_users_cat, + 302, + redirect_user=reverse( + 'projectroles:detail', + kwargs={'project': self.category.sodar_uuid}, + ), + ) + self.assert_response( + self.url_cat, + bad_users_non_cat, + 302, # Non-category users get redirected to home + ) + self.project.set_public() + self.assert_response(self.url_cat, self.user_no_roles, 302) + + @override_settings(PROJECTROLES_ALLOW_ANONYMOUS=True) + def test_get_category_anon(self): + """Test GET with category and anonymous access""" + self.project.set_public() + self.assert_response(self.url_cat, self.user_no_roles, 302) + ''' + + class TestProjectRoleView(ProjectPermissionTestBase): """Tests for ProjectRoleView permissions""" diff --git a/projectroles/tests/test_views.py b/projectroles/tests/test_views.py index 6d7f4a42..345d48d2 100644 --- a/projectroles/tests/test_views.py +++ b/projectroles/tests/test_views.py @@ -1838,204 +1838,6 @@ def test_post_target_remote(self): self.assertEqual(Project.objects.all().count(), 2) -class TestProjectArchiveView( - ProjectMixin, RoleAssignmentMixin, RemoteTargetMixin, ViewTestBase -): - """Tests for ProjectArchiveView""" - - @classmethod - def _get_tl(cls): - return TimelineEvent.objects.filter(event_name='project_archive') - - @classmethod - def _get_tl_un(cls): - return TimelineEvent.objects.filter(event_name='project_unarchive') - - def _get_alerts(self): - return self.app_alert_model.objects.filter(alert_name='project_archive') - - def _get_alerts_un(self): - return self.app_alert_model.objects.filter( - alert_name='project_unarchive' - ) - - def setUp(self): - super().setUp() - self.category = self.make_project( - 'TestCategory', PROJECT_TYPE_CATEGORY, None - ) - self.owner_as_cat = self.make_assignment( - self.category, self.user, self.role_owner - ) - self.project = self.make_project( - 'TestProject', PROJECT_TYPE_PROJECT, self.category - ) - self.owner_as = self.make_assignment( - self.project, self.user, self.role_owner - ) - self.user_contributor = self.make_user('user_contributor') - self.contributor_as = self.make_assignment( - self.project, self.user_contributor, self.role_contributor - ) - self.app_alert_model = get_backend_api('appalerts_backend').get_model() - self.url = reverse( - 'projectroles:archive', - kwargs={'project': self.project.sodar_uuid}, - ) - self.url_cat = reverse( - 'projectroles:archive', - kwargs={'project': self.category.sodar_uuid}, - ) - - def test_get(self): - """Test ProjectArchiveView GET with project""" - with self.login(self.user): - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_get_category(self): - """Test GET with category (should fail)""" - with self.login(self.user): - response = self.client.get(self.url_cat) - self.assertRedirects( - response, - reverse( - 'projectroles:detail', - kwargs={'project': self.category.sodar_uuid}, - ), - ) - - def test_post(self): - """Test ProjectArchiveView POST""" - self.assertEqual(self.project.archive, False) - self.assertEqual(self._get_tl().count(), 0) - self.assertEqual(self._get_tl_un().count(), 0) - self.assertEqual(self._get_alerts().count(), 0) - self.assertEqual(self._get_alerts_un().count(), 0) - self.assertEqual(len(mail.outbox), 0) - - with self.login(self.user): - response = self.client.post(self.url, {'status': True}) - self.assertRedirects( - response, - reverse( - 'projectroles:detail', - kwargs={'project': self.project.sodar_uuid}, - ), - ) - - self.project.refresh_from_db() - self.assertEqual(self.project.archive, True) - self.assertEqual(self._get_tl().count(), 1) - self.assertEqual(self._get_tl_un().count(), 0) - # Only the contributor should receive an alert - self.assertEqual(self._get_alerts().count(), 1) - self.assertEqual(self._get_alerts_un().count(), 0) - self.assertEqual(self._get_alerts().first().user, self.user_contributor) - self.assertEqual(len(mail.outbox), 1) - self.assertIn( - SUBJECT_PROJECT_ARCHIVE.format( - project_label_title='Project', - project=self.project.title, - user=self.user.username, - ), - mail.outbox[0].subject, - ) - - def test_post_unarchive(self): - """Test POST to unarchiving project""" - self.project.set_archive() - self.assertEqual(self._get_tl().count(), 0) - self.assertEqual(self._get_tl_un().count(), 0) - self.assertEqual(self._get_alerts().count(), 0) - self.assertEqual(self._get_alerts_un().count(), 0) - self.assertEqual(len(mail.outbox), 0) - - with self.login(self.user): - self.client.post(self.url, {'status': False}) - - self.project.refresh_from_db() - self.assertEqual(self.project.archive, False) - self.assertEqual(self._get_tl().count(), 0) - self.assertEqual(self._get_tl_un().count(), 1) - self.assertEqual(self._get_alerts().count(), 0) - self.assertEqual(self._get_alerts_un().count(), 1) - self.assertEqual( - self._get_alerts_un().first().user, self.user_contributor - ) - self.assertEqual(len(mail.outbox), 1) - self.assertIn( - SUBJECT_PROJECT_UNARCHIVE.format( - project_label_title='Project', - project=self.project.title, - user=self.user.username, - ), - mail.outbox[0].subject, - ) - - def test_post_disable_email(self): - """Test POST with disabled email""" - app_settings.set( - APP_NAME, 'notify_email_project', False, user=self.user_contributor - ) - self.assertEqual(self.project.archive, False) - self.assertEqual(self._get_tl().count(), 0) - self.assertEqual(self._get_alerts().count(), 0) - self.assertEqual(len(mail.outbox), 0) - - with self.login(self.user): - response = self.client.post(self.url, {'status': True}) - self.assertRedirects( - response, - reverse( - 'projectroles:detail', - kwargs={'project': self.project.sodar_uuid}, - ), - ) - - self.project.refresh_from_db() - self.assertEqual(self.project.archive, True) - self.assertEqual(self._get_tl().count(), 1) - # Alert but no email for contributor - self.assertEqual(self._get_alerts().count(), 1) - self.assertEqual(self._get_alerts().first().user, self.user_contributor) - self.assertEqual(len(mail.outbox), 0) - - def test_post_project_archived(self): - """Test POST with already archived project""" - self.project.set_archive() - self.assertEqual(self._get_tl().count(), 0) - self.assertEqual(self._get_tl_un().count(), 0) - self.assertEqual(len(mail.outbox), 0) - with self.login(self.user): - self.client.post(self.url, {'status': True}) - self.project.refresh_from_db() - self.assertEqual(self.project.archive, True) - self.assertEqual(self._get_tl().count(), 0) - self.assertEqual(self._get_alerts().count(), 0) - self.assertEqual(len(mail.outbox), 0) - - def test_post_category(self): - """Test POST with category (should fail)""" - self.assertEqual(self.category.archive, False) - self.assertEqual(self._get_tl().count(), 0) - self.assertEqual(len(mail.outbox), 0) - with self.login(self.user): - response = self.client.post(self.url_cat, {'status': True}) - self.assertRedirects( - response, - reverse( - 'projectroles:detail', - kwargs={'project': self.category.sodar_uuid}, - ), - ) - self.category.refresh_from_db() - self.assertEqual(self.category.archive, False) - self.assertEqual(self._get_tl().count(), 0) - self.assertEqual(self._get_alerts().count(), 0) - self.assertEqual(len(mail.outbox), 0) - - class TestProjectForm( AppSettingMixin, ProjectMixin, RoleAssignmentMixin, ViewTestBase ): @@ -2600,6 +2402,306 @@ def test_post(self): ) +class TestProjectArchiveView( + ProjectMixin, RoleAssignmentMixin, RemoteTargetMixin, ViewTestBase +): + """Tests for ProjectArchiveView""" + + @classmethod + def _get_tl(cls): + return TimelineEvent.objects.filter(event_name='project_archive') + + @classmethod + def _get_tl_un(cls): + return TimelineEvent.objects.filter(event_name='project_unarchive') + + def _get_alerts(self): + return self.app_alert_model.objects.filter(alert_name='project_archive') + + def _get_alerts_un(self): + return self.app_alert_model.objects.filter( + alert_name='project_unarchive' + ) + + def setUp(self): + super().setUp() + self.category = self.make_project( + 'TestCategory', PROJECT_TYPE_CATEGORY, None + ) + self.owner_as_cat = self.make_assignment( + self.category, self.user, self.role_owner + ) + self.project = self.make_project( + 'TestProject', PROJECT_TYPE_PROJECT, self.category + ) + self.owner_as = self.make_assignment( + self.project, self.user, self.role_owner + ) + self.user_contributor = self.make_user('user_contributor') + self.contributor_as = self.make_assignment( + self.project, self.user_contributor, self.role_contributor + ) + self.app_alert_model = get_backend_api('appalerts_backend').get_model() + self.url = reverse( + 'projectroles:archive', + kwargs={'project': self.project.sodar_uuid}, + ) + self.url_cat = reverse( + 'projectroles:archive', + kwargs={'project': self.category.sodar_uuid}, + ) + + def test_get(self): + """Test ProjectArchiveView GET with project""" + with self.login(self.user): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + def test_get_category(self): + """Test GET with category (should fail)""" + with self.login(self.user): + response = self.client.get(self.url_cat) + self.assertRedirects( + response, + reverse( + 'projectroles:detail', + kwargs={'project': self.category.sodar_uuid}, + ), + ) + + def test_post(self): + """Test ProjectArchiveView POST""" + self.assertEqual(self.project.archive, False) + self.assertEqual(self._get_tl().count(), 0) + self.assertEqual(self._get_tl_un().count(), 0) + self.assertEqual(self._get_alerts().count(), 0) + self.assertEqual(self._get_alerts_un().count(), 0) + self.assertEqual(len(mail.outbox), 0) + + with self.login(self.user): + response = self.client.post(self.url, {'status': True}) + self.assertRedirects( + response, + reverse( + 'projectroles:detail', + kwargs={'project': self.project.sodar_uuid}, + ), + ) + + self.project.refresh_from_db() + self.assertEqual(self.project.archive, True) + self.assertEqual(self._get_tl().count(), 1) + self.assertEqual(self._get_tl_un().count(), 0) + # Only the contributor should receive an alert + self.assertEqual(self._get_alerts().count(), 1) + self.assertEqual(self._get_alerts_un().count(), 0) + self.assertEqual(self._get_alerts().first().user, self.user_contributor) + self.assertEqual(len(mail.outbox), 1) + self.assertIn( + SUBJECT_PROJECT_ARCHIVE.format( + project_label_title='Project', + project=self.project.title, + user=self.user.username, + ), + mail.outbox[0].subject, + ) + + def test_post_unarchive(self): + """Test POST to unarchiving project""" + self.project.set_archive() + self.assertEqual(self._get_tl().count(), 0) + self.assertEqual(self._get_tl_un().count(), 0) + self.assertEqual(self._get_alerts().count(), 0) + self.assertEqual(self._get_alerts_un().count(), 0) + self.assertEqual(len(mail.outbox), 0) + + with self.login(self.user): + self.client.post(self.url, {'status': False}) + + self.project.refresh_from_db() + self.assertEqual(self.project.archive, False) + self.assertEqual(self._get_tl().count(), 0) + self.assertEqual(self._get_tl_un().count(), 1) + self.assertEqual(self._get_alerts().count(), 0) + self.assertEqual(self._get_alerts_un().count(), 1) + self.assertEqual( + self._get_alerts_un().first().user, self.user_contributor + ) + self.assertEqual(len(mail.outbox), 1) + self.assertIn( + SUBJECT_PROJECT_UNARCHIVE.format( + project_label_title='Project', + project=self.project.title, + user=self.user.username, + ), + mail.outbox[0].subject, + ) + + def test_post_disable_email(self): + """Test POST with disabled email""" + app_settings.set( + APP_NAME, 'notify_email_project', False, user=self.user_contributor + ) + self.assertEqual(self.project.archive, False) + self.assertEqual(self._get_tl().count(), 0) + self.assertEqual(self._get_alerts().count(), 0) + self.assertEqual(len(mail.outbox), 0) + + with self.login(self.user): + response = self.client.post(self.url, {'status': True}) + self.assertRedirects( + response, + reverse( + 'projectroles:detail', + kwargs={'project': self.project.sodar_uuid}, + ), + ) + + self.project.refresh_from_db() + self.assertEqual(self.project.archive, True) + self.assertEqual(self._get_tl().count(), 1) + # Alert but no email for contributor + self.assertEqual(self._get_alerts().count(), 1) + self.assertEqual(self._get_alerts().first().user, self.user_contributor) + self.assertEqual(len(mail.outbox), 0) + + def test_post_project_archived(self): + """Test POST with already archived project""" + self.project.set_archive() + self.assertEqual(self._get_tl().count(), 0) + self.assertEqual(self._get_tl_un().count(), 0) + self.assertEqual(len(mail.outbox), 0) + with self.login(self.user): + self.client.post(self.url, {'status': True}) + self.project.refresh_from_db() + self.assertEqual(self.project.archive, True) + self.assertEqual(self._get_tl().count(), 0) + self.assertEqual(self._get_alerts().count(), 0) + self.assertEqual(len(mail.outbox), 0) + + def test_post_category(self): + """Test POST with category (should fail)""" + self.assertEqual(self.category.archive, False) + self.assertEqual(self._get_tl().count(), 0) + self.assertEqual(len(mail.outbox), 0) + with self.login(self.user): + response = self.client.post(self.url_cat, {'status': True}) + self.assertRedirects( + response, + reverse( + 'projectroles:detail', + kwargs={'project': self.category.sodar_uuid}, + ), + ) + self.category.refresh_from_db() + self.assertEqual(self.category.archive, False) + self.assertEqual(self._get_tl().count(), 0) + self.assertEqual(self._get_alerts().count(), 0) + self.assertEqual(len(mail.outbox), 0) + + +class TestProjectDeleteView( + ProjectMixin, RoleAssignmentMixin, RemoteTargetMixin, ViewTestBase +): + """Tests for ProjectDeleteView""" + + @classmethod + def _get_tl(cls): + return TimelineEvent.objects.filter(event_name='project_delete') + + def _get_alerts(self): + return self.app_alert_model.objects.filter(alert_name='project_delete') + + def setUp(self): + super().setUp() + self.category = self.make_project( + 'TestCategory', PROJECT_TYPE_CATEGORY, None + ) + self.owner_as_cat = self.make_assignment( + self.category, self.user, self.role_owner + ) + self.project = self.make_project( + 'TestProject', PROJECT_TYPE_PROJECT, self.category + ) + self.owner_as = self.make_assignment( + self.project, self.user, self.role_owner + ) + self.user_contributor = self.make_user('user_contributor') + self.contributor_as = self.make_assignment( + self.project, self.user_contributor, self.role_contributor + ) + self.app_alert_model = get_backend_api('appalerts_backend').get_model() + self.url = reverse( + 'projectroles:delete', + kwargs={'project': self.project.sodar_uuid}, + ) + self.url_cat = reverse( + 'projectroles:delete', + kwargs={'project': self.category.sodar_uuid}, + ) + + def test_get(self): + """Test ProjectDeleteView GET with project""" + with self.login(self.user): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + # TODO: Test category with and without children once checks are implemented + ''' + def test_get_category(self): + """Test GET with category (should fail)""" + with self.login(self.user): + response = self.client.get(self.url_cat) + self.assertRedirects( + response, + reverse( + 'projectroles:detail', + kwargs={'project': self.category.sodar_uuid}, + ), + ) + ''' + + def test_post(self): + """Test ProjectDeleteView POST""" + self.assertEqual(Project.objects.count(), 2) + ''' + self.assertEqual(self._get_tl().count(), 0) + self.assertEqual(self._get_alerts().count(), 0) + ''' + self.assertEqual(len(mail.outbox), 0) + + with self.login(self.user): + response = self.client.post(self.url, {'status': True}) + self.assertRedirects( + response, + reverse( + 'projectroles:detail', + kwargs={'project': self.category.sodar_uuid}, + ), + ) + + self.assertEqual(Project.objects.count(), 1) + self.assertIsNone( + Project.objects.filter(sodar_uuid=self.project.sodar_uuid).first(), + None, + ) + ''' + self.assertEqual(self._get_tl().count(), 1) + # Only the contributor should receive an alert + self.assertEqual(self._get_alerts().count(), 1) + self.assertEqual(self._get_alerts().first().user, self.user_contributor) + self.assertEqual(len(mail.outbox), 1) + self.assertIn( + SUBJECT_PROJECT_ARCHIVE.format( + project_label_title='Project', + project=self.project.title, + user=self.user.username, + ), + mail.outbox[0].subject, + ) + ''' + + class TestProjectRoleView(ProjectMixin, RoleAssignmentMixin, ViewTestBase): """Tests for ProjectRoleView""" diff --git a/projectroles/urls.py b/projectroles/urls.py index 4aafe02e..15fcb3a6 100644 --- a/projectroles/urls.py +++ b/projectroles/urls.py @@ -31,6 +31,11 @@ view=views.ProjectArchiveView.as_view(), name='archive', ), + path( + route='delete/', + view=views.ProjectDeleteView.as_view(), + name='delete', + ), # Search views path( route='search/results/', diff --git a/projectroles/views.py b/projectroles/views.py index f3bdacc1..840d9505 100644 --- a/projectroles/views.py +++ b/projectroles/views.py @@ -855,7 +855,7 @@ def post(self, request, *args, **kwargs): return ProjectSearchResultsView.as_view()(request) -# Project Editing Views -------------------------------------------------------- +# Project Modifying Views ------------------------------------------------------ class ProjectModifyPluginViewMixin: @@ -1659,6 +1659,64 @@ def post(self, request, **kwargs): return redirect(redirect_url) +class ProjectDeleteMixin(ProjectModifyPluginViewMixin): + """Mixin for Project deletion in UI and API views""" + + def handle_delete(self, project, request): + """ + Handle project deletion. + + :param project: Project object of project to be deleted + :param request: HttpRequest object + """ + # TODO: Add all necessary logic here + project.delete() + + +class ProjectDeleteView( + LoginRequiredMixin, + LoggedInPermissionMixin, + ProjectContextMixin, + ProjectDeleteMixin, + DeleteView, +): + """Project deletion view""" + + model = Project + permission_required = 'projectroles.delete_project' + slug_field = 'sodar_uuid' + slug_url_kwarg = 'project' + + # TODO: Add checks for category and children (should refuse access) + # TODO: Add checks for remote project and revoke status + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + return context + + def post(self, *args, **kwargs): + project = self.get_object() + try: + self.handle_delete(project, self.request) + p_type = get_display_name(project.type, title=True) + messages.success(self.request, f'{p_type} deleted.') + if project.parent: + redirect_url = reverse( + 'projectroles:detail', + kwargs={'project': project.parent.sodar_uuid}, + ) + else: + redirect_url = reverse('home') + except Exception as ex: + p_type = get_display_name(self.object.type, title=False) + messages.error(self.request, f'Failed to delete {p_type}: {ex}') + redirect_url = reverse( + 'projectroles:update', + kwargs={'project': self.object.sodar_uuid}, + ) + return redirect(redirect_url) + + # RoleAssignment Views ---------------------------------------------------------