diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index cf32789e85d8088676001060167d4c737447423d..1a514f965050d781272a94a7386e212e1ee621a8 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -820,6 +820,9 @@ msgstr "Speichern" msgid "ActivityLog_no_entries" msgstr "Keine Activity Log Einträge" +msgid "Cancel" +msgstr "Abbrechen" + #, python-format msgid "Invitation__description %(requester)s %(requested)s %(type)s" msgstr "Die Einladung von \"%(requester)s\" an \"%(requested)s\" (Art: %(type)s)" @@ -1478,6 +1481,16 @@ msgstr "Speichern" msgid "Details" msgstr "" +msgid "Team__delete__warning__header" +msgstr "Das Team löschen?" + +#, python-format +msgid "Team__delete__warning__text %(team)s" +msgstr "Das Team \"%(team)s\" wirklich löschen?" + +msgid "Team__delete__submit" +msgstr "Team löschen" + # Use translation from core msgid "Team__name" msgstr "" @@ -1520,6 +1533,18 @@ msgstr "Verwaltungsrechte entfernen" msgid "TeamMember__promote_button" msgstr "Verwaltungsrechte verleihen" +msgid "TeamMember__remove__button" +msgstr "Entfernen" + +msgid "Team__delete__title" +msgstr "Team löschen" + +msgid "Team__delete__introduction" +msgstr "Hiermit wird dieses Team gelöscht. Dies kann nicht rückgängig gemacht werden." + +msgid "Team__delete__button" +msgstr "Team löschen" + msgid "Team__member_count" msgstr "Mitgliederanzahl" @@ -2069,6 +2094,9 @@ msgstr "Das Team \"%(name)s\" wurde angelegt!" msgid "Team__update__success %(name)s" msgstr "Das Aktualisieren des Teams \"%(name)s\" war erfolgreich!" +msgid "Team__delete__success" +msgstr "Das Team wurde gelöscht." + msgid "Lock-gone" msgstr "Sperre bestand nicht mehr." diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index 1cd300fc6e2f272acd052f859337bbb47bf5d31b..71beda5147c752b69876ecc6469b9b2afcf33ae6 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -820,6 +820,9 @@ msgstr "save" msgid "ActivityLog_no_entries" msgstr "No activity log entries" +msgid "Cancel" +msgstr "" + #, python-format msgid "Invitation__description %(requester)s %(requested)s %(type)s" msgstr "Invitation from \"%(requester)s\" to \"%(requested)s\" (type: %(type)s)" @@ -1483,6 +1486,16 @@ msgstr "Save" msgid "Details" msgstr "" +msgid "Team__delete__warning__header" +msgstr "Delete the team?" + +#, python-format +msgid "Team__delete__warning__text %(team)s" +msgstr "Are you sure you want to delete the team \"%(team)s\"?" + +msgid "Team__delete__submit" +msgstr "Delete team" + # Use translation from core msgid "Team__name" msgstr "" @@ -1525,6 +1538,18 @@ msgstr "Remote management rights" msgid "TeamMember__promote_button" msgstr "Add management rights" +msgid "TeamMember__remove__button" +msgstr "Remove" + +msgid "Team__delete__title" +msgstr "Delete this team" + +msgid "Team__delete__introduction" +msgstr "This deletes this Team. This action cannot be reversed!" + +msgid "Team__delete__button" +msgstr "Delete team" + msgid "Team__member_count" msgstr "members count" @@ -2074,6 +2099,9 @@ msgstr "The team \"%(name)s\" was successfully created." msgid "Team__update__success %(name)s" msgstr "The team \"%(name)s\" was successfully updated." +msgid "Team__delete__success" +msgstr "Team was deleted." + msgid "Lock-gone" msgstr "Lock was gone already." diff --git a/src/backoffice/templates/backoffice/components/confirmation_modal.html b/src/backoffice/templates/backoffice/components/confirmation_modal.html new file mode 100644 index 0000000000000000000000000000000000000000..28a1098be1df309e6f8eb9fbe3275ef28c233761 --- /dev/null +++ b/src/backoffice/templates/backoffice/components/confirmation_modal.html @@ -0,0 +1,25 @@ +{% extends "backoffice/base.html" %} +{% load rules %} +{% load i18n %} +{% load static %} +{% load hub_absolute %} + +{% block title %} + {{ confirmation_title }} +{% endblock title %} +{% block content %} + <div class="card"> + <div class="card-header text-bg-{{ confirmation_class }}" + id="confirmationModalHeader"> + <h1 class="fs-5" id="confirmationModalLabel">{{ confirmation_title }}</h1> + </div> + <div class="card-body" id="confirmationModalBody">{{ confirmation_body }}</div> + <form action="?confirmation=true" method="post"> + <div class="card-footer text-end"> + <a href="{{ confirmation_cancel_url }}" class="btn btn-secondary">{% trans "Cancel" %}</a> + {% csrf_token %} + <button type="submit" class="btn btn-{{ confirmation_class }}">{{ confirmation_submit }}</button> + </div> + </form> + </div> +{% endblock content %} diff --git a/src/backoffice/templates/backoffice/teams/detail.html b/src/backoffice/templates/backoffice/teams/detail.html index 7071d50ebbe1edc3f69fbd6ec3fc7b8578574fb9..84e857fac4d7de209a67b36a7c1f38079376ae51 100644 --- a/src/backoffice/templates/backoffice/teams/detail.html +++ b/src/backoffice/templates/backoffice/teams/detail.html @@ -7,10 +7,35 @@ {% block title %} {% trans "Details" %} | {% trans "Team" %} | {{ team.name }} {% endblock title %} - +{% block scripts %} + <script src="{% static "backoffice/form-add.js" %}"></script> + <script src="{% static "backoffice/modal.js" %}"></script> + <script nonce="{{ request.csp_nonce }}"> + $(document).ready(() => { + showModal = registerModal() + deleteSubmit = document.getElementById('TeamDeleteSubmit') + if(deleteSubmit) { + deleteSubmit.addEventListener('click', (e) => { + e.preventDefault(); + showModal( + () => { + form = document.getElementById('TeamDeleteForm') + form.action = form.action + '?confirmation=true' + form.submit() + }, + 'danger', + '{% trans "Team__delete__warning__header" %}', + '{% blocktrans %}Team__delete__warning__text {{ team }}{% endblocktrans %}', + '{% trans "Team__delete__submit" %}') + }) + } + }); + </script> +{% endblock scripts %} {% block content %} {% has_perm 'core.change_team' request.user team as can_change %} {% has_perm 'core.view_details_team' request.user team as can_view_details %} + {% has_perm 'core.delete_team' request.user team as can_delete %} <div class="card border-default"> <div class="card-header bg-default">{% trans "Details" %} | {% trans "Team" %}</div> <div class="card-body"> @@ -79,6 +104,8 @@ <input type="hidden" name="can_manage" value="True"> <button type="submit" class="btn btn-sm btn-primary">{% trans "TeamMember__promote_button" %}</button> {% endif %} + <a href="{% url 'backoffice:team-member-delete' team=team.uuid pk=member.id %}" + class="btn btn-sm btn-danger">{% trans "TeamMember__remove__button" %}</a> </form> </td> {% endif %} @@ -90,4 +117,25 @@ </table> </div> </div> + + {% if not form.create and can_delete %} + <div class="card card-danger border-danger mt-3 mb-3"> + <div class="card-header bg-default text-danger">{% trans "Team__delete__title" %}</div> + <div class="card-body"> + <p>{% trans "Team__delete__introduction" %}</p> + </div> + <div class="card-footer text-end"> + + <form class="form" + action="{% url "backoffice:team-delete" uuid=team.uuid %}" + method="post" + id="TeamDeleteForm"> + {% csrf_token %} + <button type="submit" class="btn btn-sm btn-danger" id="TeamDeleteSubmit"> + {% trans "Team__delete__button" %}: {{ object.name }} + </button> + </form> + </div> + </div> + {% endif %} {% endblock content %} diff --git a/src/backoffice/tests/teams/__init__.py b/src/backoffice/tests/teams/__init__.py index 3345c3b8e7693c2020c814601cd27411a86ec39e..e9d9b0415299f6a0a4ad4e3aa6dfe6eda3784c54 100644 --- a/src/backoffice/tests/teams/__init__.py +++ b/src/backoffice/tests/teams/__init__.py @@ -3,6 +3,7 @@ from backoffice.tests.teams.members import ( ) from backoffice.tests.teams.teams import ( TeamCreateViewTestCase, + TeamDeleteViewTestCase, TeamDetailViewTestCase, TeamListViewTestCase, TeamUpdateViewTestCase, @@ -10,6 +11,7 @@ from backoffice.tests.teams.teams import ( __all__ = ( 'TeamCreateViewTestCase', + 'TeamDeleteViewTestCase', 'TeamDetailViewTestCase', 'TeamListViewTestCase', 'TeamMemberUpdateViewTestCase', diff --git a/src/backoffice/tests/teams/teams.py b/src/backoffice/tests/teams/teams.py index 5dde5c403360f234ada1a9d20a6c705a2a1ff8e2..6e0bc74cdd382a4bbb5b771572858f092283cd83 100644 --- a/src/backoffice/tests/teams/teams.py +++ b/src/backoffice/tests/teams/teams.py @@ -95,6 +95,7 @@ class TeamDetailViewTestCase(BackOfficeTestCase): self.assertTemplateUsed(response, 'backoffice/teams/detail.html') self.assertEqual(self.team, response.context['team']) self.assertContains(response, _('Team__edit__button')) + self.assertContains(response, _('Team__delete__button')) def test_team_detail_staff(self): self.client.force_login(self.staff) @@ -103,6 +104,7 @@ class TeamDetailViewTestCase(BackOfficeTestCase): self.assertTemplateUsed(response, 'backoffice/teams/detail.html') self.assertEqual(self.team, response.context['team']) self.assertContains(response, _('Team__edit__button')) + self.assertNotContains(response, _('Team__delete__button')) def test_team_detail_team_member(self): self.client.force_login(self.user) @@ -111,6 +113,7 @@ class TeamDetailViewTestCase(BackOfficeTestCase): self.assertTemplateUsed(response, 'backoffice/teams/detail.html') self.assertEqual(self.team, response.context['team']) self.assertNotContains(response, _('Team__edit__button')) + self.assertNotContains(response, _('Team__delete__button')) def test_team_detail_non_team_member(self): self.client.force_login(self.user_2) @@ -119,6 +122,7 @@ class TeamDetailViewTestCase(BackOfficeTestCase): self.assertTemplateUsed(response, 'backoffice/teams/detail.html') self.assertEqual(self.team, response.context['team']) self.assertNotContains(response, _('Team__edit__button')) + self.assertNotContains(response, _('Team__delete__button')) class TeamCreateViewTestCase(BackOfficeTestCase): @@ -208,3 +212,50 @@ class TeamUpdateViewTestCase(BackOfficeTestCase): self.client.force_login(self.user) response = self.client.get(reverse('backoffice:team-edit', kwargs={'uuid': self.team.uuid})) self.assertEqual(response.status_code, 403) + + +class TeamDeleteViewTestCase(BackOfficeTestCase): + def setUp(self): + super().setUp() + self.team = Team.objects.create( + name='team', + conference=self.conf, + ) + self.team_member = TeamMember.objects.create(team=self.team, user=self.admin) + self.team_member2 = TeamMember.objects.create(team=self.team, user=self.staff, can_manage=True) + self.team_member3 = TeamMember.objects.create(team=self.team, user=self.user) + + def test_team_delete_unauthenicated(self): + activate('en') + response = self.client.get(reverse('backoffice:team-delete', kwargs={'uuid': self.team.uuid})) + self.assertRedirects(response, reverse('backoffice:login') + '?next=' + reverse('backoffice:team-delete', kwargs={'uuid': self.team.uuid})) + + def test_team_delete_admin(self): + self.client.force_login(self.admin) + response = self.client.get(reverse('backoffice:team-delete', kwargs={'uuid': self.team.uuid})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/components/confirmation_modal.html') + self.assertContains(response, _('Team__delete__warning__header')) + self.assertContains(response, _('Team__delete__submit')) + + response = self.client.post(reverse('backoffice:team-delete', kwargs={'uuid': self.team.uuid})) + # No confirmation, should not delete + self.assertTrue(Team.objects.filter(name='team').exists()) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/components/confirmation_modal.html') + self.assertContains(response, _('Team__delete__warning__header')) + self.assertContains(response, _('Team__delete__submit')) + + response = self.client.post(reverse('backoffice:team-delete', kwargs={'uuid': self.team.uuid}), data={'confirmation': 'true'}) + self.assertFalse(Team.objects.filter(name='team').exists()) + self.assertRedirects(response, reverse('backoffice:teams')) + + def test_team_delete_staff(self): + self.client.force_login(self.staff) + response = self.client.get(reverse('backoffice:team-delete', kwargs={'uuid': self.team.uuid})) + self.assertEqual(response.status_code, 403) + + def test_team_delete_team_member(self): + self.client.force_login(self.user) + response = self.client.get(reverse('backoffice:team-delete', kwargs={'uuid': self.team.uuid})) + self.assertEqual(response.status_code, 403) diff --git a/src/backoffice/views/teams/__init__.py b/src/backoffice/views/teams/__init__.py index 3dca46fa17caef524893a014fb790b202936b1af..6d5a4533713701c7978c89d181b639ccdff36261 100644 --- a/src/backoffice/views/teams/__init__.py +++ b/src/backoffice/views/teams/__init__.py @@ -3,6 +3,7 @@ from backoffice.views.teams.members import ( ) from backoffice.views.teams.teams import ( TeamCreateView, + TeamDeleteView, TeamDetailView, TeamListView, TeamUpdateView, @@ -10,6 +11,7 @@ from backoffice.views.teams.teams import ( __all__ = [ 'TeamCreateView', + 'TeamDeleteView', 'TeamDetailView', 'TeamListView', 'TeamMemberUpdateView', diff --git a/src/backoffice/views/teams/teams.py b/src/backoffice/views/teams/teams.py index ac35c7cd69d9770f3c0d35dfabe8d18f28622753..6e185c487d0e538b620a8c9a335b1d644034796d 100644 --- a/src/backoffice/views/teams/teams.py +++ b/src/backoffice/views/teams/teams.py @@ -3,10 +3,11 @@ from typing import Any from django.contrib.postgres.aggregates.general import ArrayAgg from django.db.models import BooleanField, Case, Count, When from django.db.models.query import Q, QuerySet -from django.urls import reverse +from django.http import HttpRequest, HttpResponse +from django.urls import reverse, reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import DetailView, FormView, ListView -from django.views.generic.edit import CreateView, UpdateView +from django.views.generic.edit import CreateView, DeleteView, UpdateView from rules.contrib.views import AutoPermissionRequiredMixin from core.forms import TeamForm @@ -123,3 +124,25 @@ class TeamCreateView(TeamFormMixin, CreateView): class TeamUpdateView(SingleUUIDObjectMixin, TeamFormMixin, UpdateView): success_message = _('Team__update__success %(name)s') + + +class TeamDeleteView(SingleUUIDObjectMixin, FormMesssageMixin, TeamNavContextMixin, AutoPermissionRequiredMixin, DeleteView): + model = Team + template_name = 'backoffice/components/confirmation_modal.html' + success_url = reverse_lazy('backoffice:teams') + success_message = _('Team__delete__success') + + def post(self, request: HttpRequest, *args: str, **kwargs: Any) -> HttpResponse: + if request.POST.get('confirmation', request.GET.get('confirmation', 'false')) != 'true': + return self.get(request, *args, **kwargs) + return super().post(request, *args, **kwargs) + + def get_context_data(self, **kwargs) -> dict[str, Any]: + return { + **super().get_context_data(**kwargs), + 'confirmation_title': _('Team__delete__warning__header'), + 'confirmation_body': _('Team__delete__warning__text %(team)s') % {'team': self.object.name}, + 'confirmation_class': 'danger', + 'confirmation_submit': _('Team__delete__submit'), + 'confirmation_cancel_url': reverse('backoffice:team', kwargs={'uuid': self.object.uuid}), + }