From 4a1ace361013f00598fe0f0258c4311c4654a1b3 Mon Sep 17 00:00:00 2001 From: Lucas Brandstaetter <lucas@brandstaetter.tech> Date: Sun, 8 Dec 2024 23:59:19 +0100 Subject: [PATCH] Add teams list view --- .../locale/de/LC_MESSAGES/django.po | 24 +++++++ .../locale/en/LC_MESSAGES/django.po | 24 +++++++ .../templates/backoffice/teams/list.html | 69 +++++++++++++++++++ src/backoffice/tests/__init__.py | 1 + src/backoffice/tests/teams/__init__.py | 3 + src/backoffice/tests/teams/teams.py | 66 ++++++++++++++++++ src/backoffice/urls.py | 2 + src/backoffice/views/teams/__init__.py | 7 ++ src/backoffice/views/teams/teams.py | 36 ++++++++++ 9 files changed, 232 insertions(+) create mode 100644 src/backoffice/templates/backoffice/teams/list.html create mode 100644 src/backoffice/tests/teams/__init__.py create mode 100644 src/backoffice/tests/teams/teams.py create mode 100644 src/backoffice/views/teams/__init__.py create mode 100644 src/backoffice/views/teams/teams.py diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index 68c1394bb..2663d40fe 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -1443,6 +1443,30 @@ msgstr "Self-organized Session löschen" msgid "SoS__delete__introduction" msgstr "Hiermit wird diese Self-organized Session gelöscht. Wenn du sie nur unsichtbar machen möchtest gibt es weiter oben die Möglichkeit die Self-organized Session zurückzunehmen!" +# use translation from core +msgid "Teams" +msgstr "" + +msgid "Team__name" +msgstr "Name" + +# Use translation from core +msgid "Team__require_staff" +msgstr "" + +msgid "Team__member_count" +msgstr "Mitgliederanzahl" + +# Use translation from core +msgid "TeamMember__can_manage" +msgstr "" + +msgid "Team__list__not_a_member" +msgstr "Kein Mitglied" + +msgid "Team__create__button" +msgstr "Team anlegen" + msgid "data_table__info__no_data" msgstr "Bisher keine Daten vorhanden" diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index 1d48e35e4..fda5d873c 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -1448,6 +1448,30 @@ msgstr "Delete self-organized session" msgid "SoS__delete__introduction" msgstr "This removes this self-organized session. If you only want to make it invisible, there is an option above to recall the self-organized session above!" +# use translation from core +msgid "Teams" +msgstr "" + +msgid "Team__name" +msgstr "name" + +# Use translation from core +msgid "Team__require_staff" +msgstr "" + +msgid "Team__member_count" +msgstr "members count" + +# Use translation from core +msgid "TeamMember__can_manage" +msgstr "" + +msgid "Team__list__not_a_member" +msgstr "Not a member" + +msgid "Team__create__button" +msgstr "Create team" + msgid "data_table__info__no_data" msgstr "No data available in table" diff --git a/src/backoffice/templates/backoffice/teams/list.html b/src/backoffice/templates/backoffice/teams/list.html new file mode 100644 index 000000000..a42244d76 --- /dev/null +++ b/src/backoffice/templates/backoffice/teams/list.html @@ -0,0 +1,69 @@ +{% extends "backoffice/base.html" %} +{% load rules %} +{% load i18n %} +{% load static %} + +{% block htmlhead %} + <link rel="stylesheet" + href="{% static 'vendor/datatables/datatables.min.css' %}"> +{% endblock htmlhead %} + +{% block scripts %} + <script src="{% static 'vendor/datatables/datatables.min.js' %}"></script> + <script nonce="{{ request.csp_nonce }}">{% include "backoffice/components/list_script.js" with table_id='teams' %}</script> +{% endblock scripts %} + +{% block content %} + {% has_perm 'books.add_book' request.user as can_add_team %} + <div class="card"> + <div class="card-header"> + <span class="text-muted">{% trans "Teams" %}:</span> {{ mode_display }} + </div> + <div class="card-body"> + + {% if filter_tag is not None %} + <div class="alert alert-info"> + {% trans "assemblies_filtered_tag" %}: <strong>{{ filter_tag }}</strong> + </div> + {% endif %} + + <table class="table table-sm" id="teams"> + <thead> + <tr> + <th>{% trans "Team__name" %}</th> + <th>{% trans "Team__require_staff" %}</th> + {% if user.is_staff %} + <th>{% trans "Team__member_count" %}</th> + {% endif %} + <th>{% trans "TeamMember__can_manage" %}</th> + </tr> + </thead> + <tbody> + {% for team in object_list %} + <tr> + <td> + <a href="{% url 'backoffice:team' uuid=team.uuid %}">{{ team.name }}</a> + </td> + <td>{{ team.require_staff|yesno }}</td> + {% if user.is_staff %}<td>{{ team.members_count }}</td>{% endif %} + <td> + {% if user.id in team.member_list %} + {{ team.can_manage|yesno }} + {% else %} + {% trans "Team__list__not_a_member" %} + {% endif %} + </td> + </tr> + {% endfor %} + </tbody> + </table> + + </div> + {% if can_add_team %} + <div class="card-footer text-end"> + <a href="{% url 'backoffice:team-create' %}" + class="btn btn-primary btn-sm">{% trans "Team__create__button" %}</a> + </div> + {% endif %} + </div> +{% endblock content %} diff --git a/src/backoffice/tests/__init__.py b/src/backoffice/tests/__init__.py index 5cb8812d2..92713085f 100644 --- a/src/backoffice/tests/__init__.py +++ b/src/backoffice/tests/__init__.py @@ -2,5 +2,6 @@ from .base import * # noqa: F401, F403, I001 from .assemblies import * # noqa: F401, F403 from .auth import * # noqa: F401, F403 from .invitations import * # noqa: F401, F403 +from .teams import * # noqa: F401, F403 __all__ = ('*',) # noqa: F405 diff --git a/src/backoffice/tests/teams/__init__.py b/src/backoffice/tests/teams/__init__.py new file mode 100644 index 000000000..db9ed6730 --- /dev/null +++ b/src/backoffice/tests/teams/__init__.py @@ -0,0 +1,3 @@ +from backoffice.tests.teams.teams import TeamListViewTestCase + +__all__ = ('TeamListViewTestCase',) diff --git a/src/backoffice/tests/teams/teams.py b/src/backoffice/tests/teams/teams.py new file mode 100644 index 000000000..ecdfd789e --- /dev/null +++ b/src/backoffice/tests/teams/teams.py @@ -0,0 +1,66 @@ +from unittest.mock import patch + +from django.urls import reverse +from django.utils.translation import activate +from django.utils.translation import gettext as _ + +from core.models import ( + Team, + TeamMember, +) +from core.tests.mock import mocktrans + +from backoffice.tests.base import BackOfficeTestCase + + +class TeamListViewTestCase(BackOfficeTestCase): + def setUp(self): + super().setUp() + self.teams = {} + for team in ['team1', 'team2', 'team3']: + self.teams[team] = Team.objects.create( + name=team, + conference=self.conf, + ) + self.teams['team1'].require_staff = True + TeamMember.objects.create(team=self.teams['team1'], user=self.staff) + TeamMember.objects.create(team=self.teams['team1'], user=self.admin, can_manage=True) + TeamMember.objects.create(team=self.teams['team2'], user=self.staff, can_manage=True) + TeamMember.objects.create(team=self.teams['team3'], user=self.admin) + + def test_team_list_unauthenticated(self): + activate('en') + response = self.client.get(reverse('backoffice:teams')) + self.assertRedirects(response, reverse('backoffice:login') + '?next=' + reverse('backoffice:teams')) + + def test_team_list_admin(self): + self.client.force_login(self.admin) + with patch('backoffice.views.teams.teams._', mocktrans): + response = self.client.get(reverse('backoffice:teams')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/list.html') + self.assertQuerySetEqual(Team.objects.all().order_by('name'), response.context['teams']) + for team in self.teams: + self.assertContains(response, team) + self.assertContains(response, _('Team__create__button')) + + def test_team_list_staff(self): + self.client.force_login(self.staff) + response = self.client.get(reverse('backoffice:teams')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/list.html') + self.assertQuerySetEqual(Team.objects.all().order_by('name'), response.context['teams']) + for team in self.teams: + self.assertContains(response, team) + self.assertNotContains(response, _('Team__create__button')) + + def test_team_list_user(self): + self.client.force_login(self.user) + with patch('backoffice.views.teams.teams._', mocktrans): + response = self.client.get(reverse('backoffice:teams')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/list.html') + self.assertQuerySetEqual(Team.objects.filter(require_staff=False).order_by('name'), response.context['teams']) + for team in self.teams: + self.assertContains(response, team) + self.assertNotContains(response, _('Team__create__button')) diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index accf9e23b..cf888bac6 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -13,6 +13,7 @@ from backoffice.views import ( profile, projects, schedules, + teams, vouchers, wiki, ) @@ -152,6 +153,7 @@ urlpatterns = [ path('self-organized/sessions/<uuid:pk>/delete', events.SoSDeleteView.as_view(), name='sos-delete'), path('sos/new', RedirectView.as_view(pattern_name='backoffice:sos-create')), path('sos/<uuid:pk>/', RedirectView.as_view(pattern_name='backoffice:sos-edit')), + path('teams', teams.TeamListView.as_view(), name='teams'), 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 new file mode 100644 index 000000000..a5dc575b1 --- /dev/null +++ b/src/backoffice/views/teams/__init__.py @@ -0,0 +1,7 @@ +from backoffice.views.teams.teams import ( + TeamListView, +) + +__all__ = [ + 'TeamListView', +] diff --git a/src/backoffice/views/teams/teams.py b/src/backoffice/views/teams/teams.py new file mode 100644 index 000000000..82af22e2d --- /dev/null +++ b/src/backoffice/views/teams/teams.py @@ -0,0 +1,36 @@ +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.views.generic import ListView + +from core.models import ConferenceMember, Team + +from backoffice.views.mixins import ConferenceRuleLoginRequiredMixin + + +class TeamListView(ConferenceRuleLoginRequiredMixin, ListView): + model = Team + template_name = 'backoffice/teams/list.html' + context_object_name = 'teams' + permission_required = 'core.view_team' + + def get_queryset(self) -> QuerySet[Team]: + member = ConferenceMember.get_member( + conference=self.conference, + user=self.request.user, + ) + qs = super().get_queryset().order_by('name') + if member.is_staff: + qs = qs.annotate( + members_count=Count('members'), + ) + else: + qs = qs.filter(require_staff=False) + return qs.annotate( + member_list=ArrayAgg('members__user'), + self_manage_count=Count('members', filter=Q(members__user=self.request.user, members__can_manage=True)), + ).annotate( + can_manage=Case(When(self_manage_count__gt=0, then=True), default=False, output_field=BooleanField()), + ) -- GitLab