diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index 63fd807168c207707ff3312c05422969d69f45e9..cf32789e85d8088676001060167d4c737447423d 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -1475,17 +1475,54 @@ msgstr "Du kannst die mit dem Team verknüpften Berechtigungen hier bearbeiten: msgid "Team__edit__submit" msgstr "Speichern" +msgid "Details" +msgstr "" + # Use translation from core msgid "Team__name" msgstr "" -msgid "Team__member_count" -msgstr "Mitgliederanzahl" +# Use translation from core +msgid "Team__description" +msgstr "" + +msgid "Team__no_description" +msgstr "Dieses Team hat aktuell keine Beschreibung" + +msgid "Team__edit__button" +msgstr "Team bearbeiten" + +# Use translation from core +msgid "TeamMembers" +msgstr "" + +msgid "name" +msgstr "Name" # Use translation from core msgid "TeamMember__can_manage" msgstr "" +# Use translation from core +msgid "TeamMember__created" +msgstr "" + +# Use translation from core +msgid "TeamMember__updated" +msgstr "" + +msgid "TeamMember__actions" +msgstr "Aktionen" + +msgid "TeamMember__demote_button" +msgstr "Verwaltungsrechte entfernen" + +msgid "TeamMember__promote_button" +msgstr "Verwaltungsrechte verleihen" + +msgid "Team__member_count" +msgstr "Mitgliederanzahl" + msgid "Team__list__not_a_member" msgstr "Kein Mitglied" @@ -1652,6 +1689,10 @@ msgstr "Account angelegt" msgid "registration_sign_up_mail_sent" msgstr "Eine E-Mail mit einem Aktivierungs-Link wurde an die angegebene E-Mail-Adresse gesendet. Bitte prüfe deine Inbox und öffne den in der Mail enthaltenen Link um deinen Account zu aktivieren." +# use translation from core +msgid "TeamMember__clean__cannot_remove_last_manager" +msgstr "" + msgid "assembly__created" msgstr "Assembly wurde angelegt. Bitte fülle die weiteren Felder aus um die Registrierung abzuschließen." diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index c9ad2cd568286e263d29f71c81e4ce67890a8d8d..1cd300fc6e2f272acd052f859337bbb47bf5d31b 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -1480,17 +1480,54 @@ msgstr "You are allowed to change the teams permissions at: " msgid "Team__edit__submit" msgstr "Save" +msgid "Details" +msgstr "" + # Use translation from core msgid "Team__name" msgstr "" -msgid "Team__member_count" -msgstr "members count" +# Use translation from core +msgid "Team__description" +msgstr "description" + +msgid "Team__no_description" +msgstr "This team has no description at the moment." + +msgid "Team__edit__button" +msgstr "Edit team" + +# Use translation from core +msgid "TeamMembers" +msgstr "" + +msgid "name" +msgstr "" # Use translation from core msgid "TeamMember__can_manage" msgstr "" +# Use translation from core +msgid "TeamMember__created" +msgstr "" + +# Use translation from core +msgid "TeamMember__updated" +msgstr "" + +msgid "TeamMember__actions" +msgstr "Actions" + +msgid "TeamMember__demote_button" +msgstr "Remote management rights" + +msgid "TeamMember__promote_button" +msgstr "Add management rights" + +msgid "Team__member_count" +msgstr "members count" + msgid "Team__list__not_a_member" msgstr "Not a member" @@ -1658,6 +1695,10 @@ msgstr "Account created." msgid "registration_sign_up_mail_sent" msgstr "An email with an activation link has been sent to the given e-mail address. Please check your inbox and open the link sent to you in order to activate your account." +# use translation from core +msgid "TeamMember__clean__cannot_remove_last_manager" +msgstr "" + msgid "assembly__created" msgstr "Assembly has been created. Please fill out the following extra information to complete registration." diff --git a/src/backoffice/templates/backoffice/teams/detail.html b/src/backoffice/templates/backoffice/teams/detail.html new file mode 100644 index 0000000000000000000000000000000000000000..7071d50ebbe1edc3f69fbd6ec3fc7b8578574fb9 --- /dev/null +++ b/src/backoffice/templates/backoffice/teams/detail.html @@ -0,0 +1,93 @@ +{% extends "backoffice/base.html" %} +{% load rules %} +{% load i18n %} +{% load static %} +{% load hub_absolute %} + +{% block title %} + {% trans "Details" %} | {% trans "Team" %} | {{ team.name }} +{% endblock title %} + +{% 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 %} + <div class="card border-default"> + <div class="card-header bg-default">{% trans "Details" %} | {% trans "Team" %}</div> + <div class="card-body"> + <div class="row"> + <label for="team-name" class="form-label col-sm-2">{% trans "Team__name" %}</label> + <span class="col-sm-4 " id="team-name">{{ team.name }}</span> + <label for="team-staff" class="form-label col-sm-2">{% trans "Team__require_staff" %}</label> + <span class="col-sm-4 " id="team-staff">{{ team.require_staff|yesno }}</span> + </div> + <div class="row"> + <label for="team-description" class="form-label">{% trans "Team__description" %}</label> + <div class="col-md-12"> + <p id="team-description"> + {% if team.description %} + {{ team.description_html|safe }} + {% else %} + {% trans "Team__no_description" %} + {% endif %} + </p> + </div> + </div> + </div> + {% if can_change %} + <div class="card-footer text-end"> + <a href="{% url "backoffice:team-edit" uuid=team.uuid %}" + class="btn btn-primary btn-sm">{% trans "Team__edit__button" %}</a> + </div> + {% endif %} + </div> + + <div class="card border-default mt-3"> + <div class="card-header bg-default">{% trans "TeamMembers" %}</div> + <div class="card-body"> + <table class="table table-sm m-0"> + <thead> + <tr> + <th>{% trans "name" %}/{% trans "username" %}</th> + <th>{% trans "TeamMember__can_manage" %}</th> + <th>{% trans "TeamMember__created" %}</th> + <th>{% trans "TeamMember__updated" %}</th> + {% if can_change %} + <th class="text-end">{% trans "TeamMember__actions" %}</th> + {% endif %} + </tr> + </thead> + <tbody> + {% for member in team.members.all %} + {% if not can_view_details and member.can_manage or can_view_details %} + <tr> + <td> + <a href="{% hub_absolute 'plainui:user' user_slug=member.user.slug %}">{{ member.user.username }}</a> + </td> + <td>{{ member.can_manage|yesno }}</td> + <td>{{ member.created }}</td> + <td>{{ member.updated }}</td> + {% if can_change %} + <td class="text-end"> + + <form action="{% url "backoffice:team-member-update" team=team.uuid pk=member.id %}" + method="post"> + {% csrf_token %} + {% if member.can_manage %} + <input type="hidden" name="can_manage" value="False"> + <button type="submit" class="btn btn-sm btn-warning">{% trans "TeamMember__demote_button" %}</button> + {% else %} + <input type="hidden" name="can_manage" value="True"> + <button type="submit" class="btn btn-sm btn-primary">{% trans "TeamMember__promote_button" %}</button> + {% endif %} + </form> + </td> + {% endif %} + </tr> + {% endif %} + {% endfor %} + </tbody> + + </table> + </div> + </div> +{% endblock content %} diff --git a/src/backoffice/tests/teams/__init__.py b/src/backoffice/tests/teams/__init__.py index c1553ae61c8f31f48546957f4513494793239976..3345c3b8e7693c2020c814601cd27411a86ec39e 100644 --- a/src/backoffice/tests/teams/__init__.py +++ b/src/backoffice/tests/teams/__init__.py @@ -1,11 +1,17 @@ +from backoffice.tests.teams.members import ( + TeamMemberUpdateViewTestCase, +) from backoffice.tests.teams.teams import ( TeamCreateViewTestCase, + TeamDetailViewTestCase, TeamListViewTestCase, TeamUpdateViewTestCase, ) __all__ = ( 'TeamCreateViewTestCase', + 'TeamDetailViewTestCase', 'TeamListViewTestCase', + 'TeamMemberUpdateViewTestCase', 'TeamUpdateViewTestCase', ) diff --git a/src/backoffice/tests/teams/members.py b/src/backoffice/tests/teams/members.py new file mode 100644 index 0000000000000000000000000000000000000000..3b801462c08c8ed7e57e6ca7feba9e46be49bc67 --- /dev/null +++ b/src/backoffice/tests/teams/members.py @@ -0,0 +1,85 @@ +from django.contrib.messages import ERROR +from django.contrib.messages.storage.base import Message +from django.contrib.messages.test import MessagesTestMixin +from django.urls import reverse +from django.utils.translation import activate +from django.utils.translation import gettext as _ + +from core.models import ActivityLogEntry, PlatformUser, Team, TeamMember + +from backoffice.tests.base import BackOfficeTestCase + + +class TeamMemberUpdateViewTestCase(MessagesTestMixin, BackOfficeTestCase): + def setUp(self): + super().setUp() + self.staff_2 = PlatformUser.objects.create(username='test_staff_2', is_staff=True) + self.team = Team.objects.create( + name='team', + conference=self.conf, + ) + self.team_member_staff = TeamMember.objects.create(team=self.team, user=self.staff, can_manage=True) + self.team_member_staff_2 = TeamMember.objects.create(team=self.team, user=self.staff_2, can_manage=True) + self.team_member_user = TeamMember.objects.create(team=self.team, user=self.user) + + def test_update_member_admin(self): + activate('en') + self.client.force_login(self.admin) + activity_log_count = ActivityLogEntry.objects.count() + response = self.client.post( + reverse('backoffice:team-member-update', kwargs={'pk': self.team_member_user.pk, 'team': self.team.uuid}), + data={'can_manage': 'true'}, + ) + self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertTrue(TeamMember.objects.filter(user=self.user, can_manage=True).exists()) + self.assertEqual(ActivityLogEntry.objects.count(), activity_log_count + 1) + + response = self.client.post( + reverse('backoffice:team-member-update', kwargs={'pk': self.team_member_staff.pk, 'team': self.team.uuid}), + data={'can_manage': 'false'}, + ) + self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertTrue(TeamMember.objects.filter(user=self.staff, can_manage=False).exists()) + self.assertEqual(ActivityLogEntry.objects.count(), activity_log_count + 2) + + def test_update_member_staff(self): + activate('en') + self.client.force_login(self.staff) + activity_log_count = ActivityLogEntry.objects.count() + response = self.client.post( + reverse('backoffice:team-member-update', kwargs={'pk': self.team_member_user.pk, 'team': self.team.uuid}), + data={'can_manage': 'true'}, + ) + self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertTrue(TeamMember.objects.filter(user=self.user, can_manage=True).exists()) + self.assertEqual(ActivityLogEntry.objects.count(), activity_log_count + 1) + + # Test to remove rights from yourself + response = self.client.post( + reverse('backoffice:team-member-update', kwargs={'pk': self.team_member_staff.pk, 'team': self.team.uuid}), + data={'can_manage': 'false'}, + ) + self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertTrue(TeamMember.objects.filter(user=self.staff, can_manage=False).exists()) + self.assertEqual(ActivityLogEntry.objects.count(), activity_log_count + 2) + + def test_cannot_remove_last_manager(self): + activate('en') + self.client.force_login(self.staff) + activity_log_count = ActivityLogEntry.objects.count() + response = self.client.post( + reverse('backoffice:team-member-update', kwargs={'pk': self.team_member_staff_2.pk, 'team': self.team.uuid}), + data={'can_manage': 'false'}, + ) + self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertTrue(TeamMember.objects.filter(user=self.staff_2, can_manage=False).exists()) + self.assertEqual(ActivityLogEntry.objects.count(), activity_log_count + 1) + + response = self.client.post( + reverse('backoffice:team-member-update', kwargs={'pk': self.team_member_staff.pk, 'team': self.team.uuid}), + data={'can_manage': 'false'}, + ) + self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertTrue(TeamMember.objects.filter(user=self.staff, can_manage=True).exists()) + self.assertEqual(ActivityLogEntry.objects.count(), activity_log_count + 1) + self.assertMessages(response, [Message(ERROR, _('TeamMember__clean__cannot_remove_last_manager'))]) diff --git a/src/backoffice/tests/teams/teams.py b/src/backoffice/tests/teams/teams.py index 844c12a40b92a5fd58346a5f82ac97b5e2dde9c6..5dde5c403360f234ada1a9d20a6c705a2a1ff8e2 100644 --- a/src/backoffice/tests/teams/teams.py +++ b/src/backoffice/tests/teams/teams.py @@ -5,6 +5,8 @@ from django.utils.translation import activate from django.utils.translation import gettext as _ from core.models import ( + ConferenceMember, + PlatformUser, Team, TeamMember, ) @@ -66,6 +68,59 @@ class TeamListViewTestCase(BackOfficeTestCase): self.assertNotContains(response, _('Team__create__button')) +class TeamDetailViewTestCase(BackOfficeTestCase): + def setUp(self): + super().setUp() + self.user_2 = PlatformUser.objects.create_user( + username='user2', + ) + self.user_2_cm = ConferenceMember.objects.create(conference=self.conf, user=self.user_2) + 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_detail_unauthenicated(self): + activate('en') + response = self.client.get(reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertRedirects(response, reverse('backoffice:login') + '?next=' + reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + + def test_team_detail_admin(self): + self.client.force_login(self.admin) + response = self.client.get(reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/detail.html') + self.assertEqual(self.team, response.context['team']) + self.assertContains(response, _('Team__edit__button')) + + def test_team_detail_staff(self): + self.client.force_login(self.staff) + response = self.client.get(reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/detail.html') + self.assertEqual(self.team, response.context['team']) + self.assertContains(response, _('Team__edit__button')) + + def test_team_detail_team_member(self): + self.client.force_login(self.user) + response = self.client.get(reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/detail.html') + self.assertEqual(self.team, response.context['team']) + self.assertNotContains(response, _('Team__edit__button')) + + def test_team_detail_non_team_member(self): + self.client.force_login(self.user_2) + response = self.client.get(reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/detail.html') + self.assertEqual(self.team, response.context['team']) + self.assertNotContains(response, _('Team__edit__button')) + + class TeamCreateViewTestCase(BackOfficeTestCase): def test_team_create_unauthenicated(self): activate('en') diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index dd93c12017f3e956b3f66a8e4d92919e930eeae9..08a7be98357123ba9fe5b4dc4f73c74cfd7b39f1 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -155,7 +155,10 @@ urlpatterns = [ path('sos/<uuid:pk>/', RedirectView.as_view(pattern_name='backoffice:sos-edit')), path('teams', teams.TeamListView.as_view(), name='teams'), path('team/create', teams.TeamCreateView.as_view(), name='team-create'), + path('team/<uuid:uuid>', teams.TeamDetailView.as_view(), name='team'), path('team/<uuid:uuid>/edit', teams.TeamUpdateView.as_view(), name='team-edit'), + path('team/<uuid:uuid>/delete', teams.TeamDeleteView.as_view(), name='team-delete'), + path('team/<uuid:team>/member/<uuid:pk>/update', teams.TeamMemberUpdateView.as_view(), name='team-member-update'), path('vouchers', vouchers.VouchersView.as_view(), name='vouchers'), path('_boom', misc.BoomView.as_view()), ] diff --git a/src/backoffice/views/teams/__init__.py b/src/backoffice/views/teams/__init__.py index 2f56a60e9a184a5b5b53cfa0a9a450a138cc2866..3dca46fa17caef524893a014fb790b202936b1af 100644 --- a/src/backoffice/views/teams/__init__.py +++ b/src/backoffice/views/teams/__init__.py @@ -1,11 +1,17 @@ +from backoffice.views.teams.members import ( + TeamMemberUpdateView, +) from backoffice.views.teams.teams import ( TeamCreateView, + TeamDetailView, TeamListView, TeamUpdateView, ) __all__ = [ 'TeamCreateView', + 'TeamDetailView', 'TeamListView', + 'TeamMemberUpdateView', 'TeamUpdateView', ] diff --git a/src/backoffice/views/teams/members.py b/src/backoffice/views/teams/members.py new file mode 100644 index 0000000000000000000000000000000000000000..665cf4c8f7705b0dad14f56dbc4ca1be53d8425e --- /dev/null +++ b/src/backoffice/views/teams/members.py @@ -0,0 +1,33 @@ +from django.contrib.messages import ERROR, add_message +from django.http import HttpResponseRedirect +from django.urls import reverse +from django.views.generic import UpdateView +from rules.contrib.views import AutoPermissionRequiredMixin + +from core.models import TeamMember + + +class TeamMemberUpdateView(AutoPermissionRequiredMixin, UpdateView): + model = TeamMember + http_method_names = ['post'] + fields = ['can_manage'] + + def form_valid(self, form): + if not TeamMember.type_is(team_member := self.get_object()): # pragma: no cover + raise ValueError('The object is not a TeamMember.') + team = team_member.team + old_manages = ', '.join(team.members.filter(can_manage=True).values_list('user__display_name', flat=True)) + response = super().form_valid(form) + team.log_activity( + self.request.user, + manages=ActivityLogChange(old=old_manages, new=', '.join(team.members.filter(can_manage=True).values_list('user__display_name', flat=True))), + ) + return response + + def form_invalid(self, form): + for error in form.errors['__all__']: + add_message(self.request, ERROR, str(error)) + return HttpResponseRedirect(self.get_success_url()) + + def get_success_url(self): + return reverse('backoffice:team', kwargs={'uuid': self.kwargs['team']}) diff --git a/src/backoffice/views/teams/teams.py b/src/backoffice/views/teams/teams.py index 4c2a0eee2895b07c2d96300ac4f76b2f1d3ee8bc..ac35c7cd69d9770f3c0d35dfabe8d18f28622753 100644 --- a/src/backoffice/views/teams/teams.py +++ b/src/backoffice/views/teams/teams.py @@ -5,7 +5,7 @@ from django.db.models import BooleanField, Case, Count, When from django.db.models.query import Q, QuerySet from django.urls import reverse from django.utils.translation import gettext as _ -from django.views.generic import FormView, ListView +from django.views.generic import DetailView, FormView, ListView from django.views.generic.edit import CreateView, UpdateView from rules.contrib.views import AutoPermissionRequiredMixin @@ -91,6 +91,15 @@ class TeamListView(TeamNavContextMixin, ListView): ) +class TeamDetailView(SingleUUIDObjectMixin, TeamNavContextMixin, AutoPermissionRequiredMixin, DetailView): + model = Team + template_name = 'backoffice/teams/detail.html' + context_object_name = 'team' + + def get_queryset(self) -> QuerySet[Team]: + return super().get_queryset().prefetch_related('members__user') + + class TeamFormMixin(AutoPermissionRequiredMixin, TeamNavContextMixin, FormMesssageMixin, FormView): model = Team object_name = 'team'