diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index 72e7f543670454915b60ba3f488cd973bac2e029..c675dec3ed8f6acdc3d1af3d9bd19008ff6294cf 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -1481,11 +1481,11 @@ msgid "team__edit__metadata" msgstr "Team-Daten" # Use translation from core -msgid "Team__require_staff" +msgid "Team__visibility" msgstr "" # Use translation from core -msgid "Team__require_staff__help" +msgid "Team__visibility__help" msgstr "" msgid "Team__change_permission_admin" @@ -1507,9 +1507,8 @@ msgstr "Das Team \"%(team)s\" wirklich löschen?" msgid "Team__delete__submit" msgstr "Team löschen" -# Use translation from core msgid "Team__name" -msgstr "" +msgstr "Name" # Use translation from core msgid "Team__description" diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index 2f93691957216fd26aff9990719a2b3945ecafe4..70d0d1cef0a8bd616b715d0657b803630cc3f1a6 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -1486,11 +1486,11 @@ msgid "team__edit__metadata" msgstr "team data" # Use translation from core -msgid "Team__require_staff" +msgid "Team__visibility" msgstr "" # Use translation from core -msgid "Team__require_staff__help" +msgid "Team__visibility__help" msgstr "" msgid "Team__change_permission_admin" @@ -1512,13 +1512,12 @@ 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 "" +msgstr "name" # Use translation from core msgid "Team__description" -msgstr "description" +msgstr "" msgid "Team__no_description" msgstr "This team has no description at the moment." diff --git a/src/backoffice/templates/backoffice/teams/create_edit.html b/src/backoffice/templates/backoffice/teams/create_edit.html index 7d3696ca2bc1f28814a2878e630ebbf339270d60..b585a1327528f29db2d89657017c3c85604ebf7a 100644 --- a/src/backoffice/templates/backoffice/teams/create_edit.html +++ b/src/backoffice/templates/backoffice/teams/create_edit.html @@ -28,13 +28,13 @@ <p class="fw-bold border-bottom mb-3">{% trans "team__edit__metadata" %}</p> <div class="row mb-3"> <div class="col-md-9">{% bootstrap_field form.name %}</div> - {% if form.require_staff %} - <div class="col-md-3">{% bootstrap_field form.require_staff %}</div> + {% if form.visibility %} + <div class="col-md-3">{% bootstrap_field form.visibility %}</div> {% else %} <div class="col-md-3"> - <label for="require_staff">{% trans "Team__require_staff" %}:</label> - <span id="require_staff">{{ form.require_staff|yesno }}</span> - <div class="form-text">{% trans "Team__require_staff__help" %}</div> + <label for="visibility">{% trans "Team__visibility" %}:</label> + <span id="visibility">{{ form.instance.get_visibility_display }}</span> + <div class="form-text">{% trans "Team__visibility__help" %}</div> </div> {% endif %} </div> diff --git a/src/backoffice/templates/backoffice/teams/detail.html b/src/backoffice/templates/backoffice/teams/detail.html index 5309f643f502047ac62a2692c4edcd4b802fa845..21e84133f4b6192c404a0bcb2a9b564c4518900a 100644 --- a/src/backoffice/templates/backoffice/teams/detail.html +++ b/src/backoffice/templates/backoffice/teams/detail.html @@ -8,29 +8,32 @@ {% trans "Details" %} | {% trans "Team" %} | {{ team.name }} {% endblock title %} {% block scripts %} + {% has_perm 'core.delete_team' request.user team as can_delete %} <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> + {% if can_delete %} + <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> + {% endif %} {% endblock scripts %} {% block content %} {% has_perm 'core.change_team' request.user team as can_change %} @@ -42,8 +45,8 @@ <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> + <label for="team-staff" class="form-label col-sm-2">{% trans "Team__visibility" %}</label> + <span class="col-sm-4 " id="team-staff">{{ team.get_visibility_display }}</span> </div> <div class="row"> <label for="team-description" class="form-label">{% trans "Team__description" %}</label> @@ -117,8 +120,8 @@ </table> </div> <div class="card-footer text-end"> - {% if member_id %} - <a href="{% url "backoffice:team-member-delete" team=team.uuid pk=member_id %}" + {% if team.member_id %} + <a href="{% url "backoffice:team-member-delete" team=team.uuid pk=team.member_id %}" class="btn btn-danger btn-sm">{% trans "TeamMember__leave" %}</a> {% else %} <a href="{% url "backoffice:invitation-send" type="member_to_team" requester_id=user.uuid requested_id=team.uuid %}" diff --git a/src/backoffice/templates/backoffice/teams/list.html b/src/backoffice/templates/backoffice/teams/list.html index 56c9086dbd4f0f3ad829b60d8bd6c80c0e6e8350..d904f6c70d077e24d9ede674122109043c30231f 100644 --- a/src/backoffice/templates/backoffice/teams/list.html +++ b/src/backoffice/templates/backoffice/teams/list.html @@ -33,8 +33,8 @@ <thead> <tr> <th>{% trans "Team__name" %}</th> - <th>{% trans "Team__require_staff" %}</th> - {% if user.is_staff %} + <th>{% trans "Team__visibility" %}</th> + {% if conferencemember.is_staff %} <th>{% trans "Team__member_count" %}</th> {% endif %} <th>{% trans "TeamMember__can_manage" %}</th> @@ -46,11 +46,11 @@ <td> <a href="{% url 'backoffice:team' uuid=team.uuid %}">{{ team.name }}</a> </td> - <td>{{ team.require_staff|yesno }}</td> + <td>{{ team.get_visibility_display }}</td> {% if user.is_staff %}<td>{{ team.members_count }}</td>{% endif %} <td> - {% if user.id in team.member_list %} - {{ team.can_manage|yesno }} + {% if team.is_member %} + {{ team.is_manager|yesno }} {% else %} {% trans "Team__list__not_a_member" %} {% endif %} diff --git a/src/backoffice/tests/teams/members.py b/src/backoffice/tests/teams/members.py index ea2d3313b0c62c5d3b84794ccf5c3df331732944..56d9744885a9ffe75079da3e8b0ca5b873c0c769 100644 --- a/src/backoffice/tests/teams/members.py +++ b/src/backoffice/tests/teams/members.py @@ -17,6 +17,7 @@ class TeamMemberDeleteViewTestCase(BackOfficeTestCase): self.team = Team.objects.create( name='team', conference=self.conf, + visibility=Team.TeamVisibility.PUBLIC, ) 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) diff --git a/src/backoffice/tests/teams/teams.py b/src/backoffice/tests/teams/teams.py index 576965c9df517a461a7ff6ac2794f42ba883a644..1ea090c54b86a3dd025adcc8ccb69d985bad16dd 100644 --- a/src/backoffice/tests/teams/teams.py +++ b/src/backoffice/tests/teams/teams.py @@ -1,265 +1,305 @@ -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.forms.teams import TeamForm from core.models import ( - ConferenceMember, PlatformUser, Team, TeamMember, ) -from core.tests.mock import mocktrans from backoffice.tests.base import BackOfficeTestCase class TeamListViewTestCase(BackOfficeTestCase): + users = ['admin', 'staff', 'user', 'team_manager', 'team_user'] + detail_matrix = { + Team.TeamVisibility.PUBLIC: ['admin', 'staff', 'user', 'team_manager', 'team_user'], + Team.TeamVisibility.STAFF: ['admin', 'staff', 'team_manager', 'team_user'], + Team.TeamVisibility.HIDDEN: ['admin', 'team_manager', 'team_user'], + } + create_button = ['admin'] + def setUp(self): super().setUp() + self.team_user = PlatformUser.objects.create_user(username='team_user') + self.team_manager = PlatformUser.objects.create_user(username='team_manager') 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) + self.team = Team.objects.create( + name='team', + conference=self.conf, + ) + TeamMember.objects.create(team=self.team, user=self.team_manager, can_manage=True) + TeamMember.objects.create(team=self.team, user=self.team_user) - def test_team_list_unauthenticated(self): + def test_team_list_unauthenicated(self): activate('en') - response = self.client.get(reverse('backoffice:teams')) - self.assertRedirects(response, reverse('backoffice:login') + '?next=' + reverse('backoffice:teams')) + for visibility in Team.TeamVisibility: + with self.subTest(visibility=visibility): + self.team.visibility = visibility + self.team.save() + 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_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')) + activate('en') + for user in self.users: + for visibility, users in self.detail_matrix.items(): + with self.subTest(user=user, visibility=visibility): + self.team.visibility = visibility + self.team.save() + self.client.force_login(getattr(self, user)) + response = self.client.get(reverse('backoffice:teams')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/list.html') + if user in self.create_button: + self.assertContains(response, _('Team__create__button')) + else: + self.assertNotContains(response, _('Team__create__button')) + if user in users: + self.assertIn(self.team, response.context['teams']) + else: + self.assertNotIn(self.team, response.context['teams']) class TeamDetailViewTestCase(BackOfficeTestCase): + detail_matrix = { + Team.TeamVisibility.PUBLIC: { + 'admin': ['edit', 'delete', 'join'], + 'staff': ['join'], + 'team_manager': ['edit', 'leave'], + 'user': ['leave'], + 'user_2': ['join'], + }, + Team.TeamVisibility.STAFF: { + 'admin': ['edit', 'delete', 'join'], + 'staff': ['join'], + 'team_manager': ['edit', 'leave'], + 'user': ['leave'], + 'user_2': False, + }, + Team.TeamVisibility.HIDDEN: { + 'admin': ['edit', 'delete', 'join'], + 'staff': False, + 'team_manager': ['edit', 'leave'], + 'user': ['leave'], + 'user_2': False, + }, + } + button_texts = { + 'edit': _('Team__edit__button'), + 'delete': _('Team__delete__button'), + 'join': _('TeamMember__join'), + 'leave': _('TeamMember__leave'), + } + def setUp(self): super().setUp() + self.team_manager = PlatformUser.objects.create_user( + username='team_mamanger', + ) 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) + self.team_member_manager = TeamMember.objects.create(team=self.team, user=self.team_manager, can_manage=True) + self.team_member_user = 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')) - self.assertContains(response, _('Team__delete__button')) - self.assertNotContains(response, _('TeamMember__join')) - - 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')) - self.assertNotContains(response, _('Team__delete__button')) - self.assertNotContains(response, _('TeamMember__join')) - - 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')) - self.assertNotContains(response, _('Team__delete__button')) - self.assertNotContains(response, _('TeamMember__join')) - - 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')) - self.assertNotContains(response, _('Team__delete__button')) - self.assertContains(response, _('TeamMember__join')) + for visibility in Team.TeamVisibility: + with self.subTest(visibility=visibility): + self.team.visibility = visibility + self.team.save() + 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(self): + activate('en') + for visibility, matrix in self.detail_matrix.items(): + for user, buttons in matrix.items(): + with self.subTest(visibility=visibility, user=user): + self.team.visibility = visibility + self.team.save() + self.client.force_login(getattr(self, user)) + response = self.client.get(reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + if buttons is False: + self.assertEqual(response.status_code, 403) + else: + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/detail.html') + self.assertEqual(self.team, response.context['team']) + for button, text in self.button_texts.items(): + if button not in buttons: + self.assertNotContains(response, text) + else: + self.assertContains(response, text) class TeamCreateViewTestCase(BackOfficeTestCase): + expected_results = { + 'admin': True, + 'staff': False, + 'user': False, + } + def test_team_create_unauthenicated(self): activate('en') response = self.client.get(reverse('backoffice:team-create')) self.assertRedirects(response, reverse('backoffice:login') + '?next=' + reverse('backoffice:team-create')) - def test_team_create_admin(self): - self.client.force_login(self.admin) - response = self.client.get(reverse('backoffice:team-create')) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'backoffice/teams/create_edit.html') - self.assertContains(response, _('create')) - self.assertContains(response, _('Team__create__title')) - - response = self.client.post(reverse('backoffice:team-create'), data={'name': 'team'}) - self.assertTrue(Team.objects.filter(name='team').exists()) - team = Team.objects.get(name='team') - self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': team.uuid})) - - def test_team_create_staff(self): - self.client.force_login(self.staff) - response = self.client.get(reverse('backoffice:team-create')) - self.assertEqual(response.status_code, 403) - - def test_team_create_team_member(self): - self.client.force_login(self.user) - response = self.client.get(reverse('backoffice:team-create')) - self.assertEqual(response.status_code, 403) + def test_team_create(self): + for user, result in self.expected_results.items(): + with self.subTest(user=user): + self.client.force_login(getattr(self, user)) + response = self.client.get(reverse('backoffice:team-create')) + if result is True: + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/create_edit.html') + self.assertContains(response, _('create')) + self.assertContains(response, _('Team__create__title')) + + response = self.client.post(reverse('backoffice:team-create'), data={'name': 'team', 'visibility': Team.TeamVisibility.PUBLIC}) + team = Team.objects.filter(name='team', visibility=Team.TeamVisibility.PUBLIC).first() + self.assertIsNotNone(team, 'Team not created') + self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': team.uuid})) + else: + self.assertEqual(response.status_code, 403) class TeamUpdateViewTestCase(BackOfficeTestCase): + fields = { + 'name': 'test-name', + 'description_en': 'test-en', + 'description_de': 'test-de', + 'visibility': Team.TeamVisibility.PUBLIC.value, + } + expected_results = { + 'admin': ['name', 'description_en', 'description_de', 'visibility'], + 'staff': False, + 'team_manager': ['name', 'description_en', 'description_de'], + 'user': False, + } + 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_manager = PlatformUser.objects.create_user(username='team_mamanger') + self.user_2 = PlatformUser.objects.create_user(username='user2') + + def repeatable_setUp(self): + for team in Team.objects.all(): + team.delete() 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) + self.team_member_manager = TeamMember.objects.create(team=self.team, user=self.team_manager, can_manage=True) + self.team_member_user = TeamMember.objects.create(team=self.team, user=self.user) - def test_team_update_unauthenicated(self): - activate('en') - response = self.client.get(reverse('backoffice:team-edit', kwargs={'uuid': self.team.uuid})) - self.assertRedirects(response, reverse('backoffice:login') + '?next=' + reverse('backoffice:team-edit', kwargs={'uuid': self.team.uuid})) - - def test_team_edit_admin(self): - self.client.force_login(self.admin) - response = self.client.get(reverse('backoffice:team-edit', kwargs={'uuid': self.team.uuid})) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'backoffice/teams/create_edit.html') - self.assertNotContains(response, _('create')) - # TODO: Add test for title - self.assertContains(response, _('Team__edit__submit')) - - response = self.client.post(reverse('backoffice:team-edit', kwargs={'uuid': self.team.uuid}), data={'name': 'team'}) - self.assertTrue(Team.objects.filter(name='team').exists()) - team = Team.objects.get(name='team') - self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': team.uuid})) + def test_all_fields(self): + self.assertListEqual(list(self.fields.keys()), TeamForm.Meta.fields) - def test_team_edit_staff(self): - self.client.force_login(self.staff) - response = self.client.get(reverse('backoffice:team-edit', kwargs={'uuid': self.team.uuid})) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'backoffice/teams/create_edit.html') - self.assertNotContains(response, _('create')) - # TODO: Add test for title - self.assertContains(response, _('Team__edit__submit')) - - response = self.client.post( - reverse('backoffice:team-edit', kwargs={'uuid': self.team.uuid}), - data={'name': 'team', 'description_de': 'description_de', 'description_en': 'description_en'}, - ) - self.assertTrue(Team.objects.filter(name='team').exists()) - self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) - self.team.refresh_from_db() - self.assertEqual(self.team.description_de, 'description_de') - self.assertEqual(self.team.description_en, 'description_en') - - def test_team_edit_team_member(self): - 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) + def test_team_edit(self): + activate('en') + for visibility in Team.TeamVisibility: + for user, user_fields in self.expected_results.items(): + with self.subTest(visibility=visibility, user=user): + self.repeatable_setUp() + self.team.visibility = visibility + self.team.save() + self.client.force_login(getattr(self, user)) + response = self.client.get(reverse('backoffice:team-edit', kwargs={'uuid': self.team.uuid})) + if user_fields: + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'backoffice/teams/create_edit.html') + self.assertNotContains(response, _('create')) + self.assertContains(response, _('Team__edit__submit')) + for field in self.fields: + if field in user_fields: + self.assertIn(field, response.context['form'].fields) + else: + self.assertNotIn(field, response.context['form'].fields) + else: + self.assertEqual(response.status_code, 403) + response = self.client.post( + reverse('backoffice:team-edit', kwargs={'uuid': self.team.uuid}), + data={field: self.fields[field] for field in user_fields} if user_fields else {'name': self.fields['name']}, + ) + if user_fields: + self.team.refresh_from_db() + self.assertRedirects(response, reverse('backoffice:team', kwargs={'uuid': self.team.uuid})) + for field in [x for x in self.fields if x in user_fields]: + self.assertEqual(getattr(self.team, field), self.fields[field]) + else: + self.assertEqual(response.status_code, 403) class TeamDeleteViewTestCase(BackOfficeTestCase): + expected_results = { + 'admin': True, + 'staff': False, + 'team_manager': False, + 'user': False, + } + def setUp(self): super().setUp() + self.team_manager = PlatformUser.objects.create_user(username='team_mamanger') + + def repeatable_setUp(self, visibility=Team.TeamVisibility.STAFF): + if team := Team.objects.filter(name='team').first(): + team.delete() self.team = Team.objects.create( name='team', conference=self.conf, + visibility=visibility, ) - 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) + self.team_member_manager = TeamMember.objects.create(team=self.team, user=self.staff, can_manage=True) + self.team_member_user = 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})) + self.repeatable_setUp() + for visibility in Team.TeamVisibility: + with self.subTest(visibility=visibility): + self.team.visibility = visibility + self.team.save() + 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) + activate('en') + for user, result in self.expected_results.items(): + for visibility in Team.TeamVisibility: + with self.subTest(user=user, visibility=visibility): + self.repeatable_setUp(visibility) + self.client.force_login(getattr(self, user)) + response = self.client.get(reverse('backoffice:team-delete', kwargs={'uuid': self.team.uuid})) + if result is True: + 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')) + else: + self.assertEqual(response.status_code, 403) + + response = self.client.post(reverse('backoffice:team-delete', kwargs={'uuid': self.team.uuid})) + if result is True: + # 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')) + else: + self.assertEqual(response.status_code, 403) + + response = self.client.post(reverse('backoffice:team-delete', kwargs={'uuid': self.team.uuid}), data={'confirmation': 'true'}) + if result is True: + self.assertFalse(Team.objects.filter(name='team').exists()) + self.assertRedirects(response, reverse('backoffice:teams')) + else: + self.assertEqual(response.status_code, 403) diff --git a/src/backoffice/views/teams/teams.py b/src/backoffice/views/teams/teams.py index 0fb6c7a49887ad50e8f0c2f6116c0c36bcbcd3e5..1a473645fe4b1b94c0f668a7140e2c284b32a6e7 100644 --- a/src/backoffice/views/teams/teams.py +++ b/src/backoffice/views/teams/teams.py @@ -1,7 +1,6 @@ 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 import Count, Exists, OuterRef, Subquery from django.db.models.query import Q, QuerySet from django.http import HttpRequest, HttpResponse from django.urls import reverse, reverse_lazy @@ -11,7 +10,7 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView from rules.contrib.views import AutoPermissionRequiredMixin from core.forms import TeamForm -from core.models import ConferenceMember, Invitation, Team +from core.models import ConferenceMember, Invitation, Team, TeamMember from core.views.mixins import FormMesssageMixin from backoffice.views.mixins import ConferenceRuleLoginRequiredMixin, SingleUUIDObjectMixin, guess_active_sidebar_item @@ -19,27 +18,19 @@ from backoffice.views.mixins import ConferenceRuleLoginRequiredMixin, SingleUUID class TeamNavContextMixin(ConferenceRuleLoginRequiredMixin): def get_context_data(self, **kwargs) -> dict[str, Any]: - member = ConferenceMember.get_member( - conference=self.conference, - user=self.request.user, - ) teams = [ { - 'caption': team['name'], + 'caption': team.name, 'link': reverse( 'backoffice:team', kwargs={ - 'uuid': team['uuid'], + 'uuid': team.uuid, }, ), 'classes': [], } - for team in Team.objects.all().values( - 'uuid', - 'name', - 'require_staff', - ) - if member.is_staff or not team['require_staff'] + for team in Team.objects.all() + if self.request.user.has_perm('core.view_team', team) ] team_entry = { 'caption': _('Teams'), @@ -77,37 +68,42 @@ class TeamListView(TeamNavContextMixin, ListView): conference=self.conference, user=self.request.user, ) - qs = super().get_queryset().order_by('name') - if member.is_staff: - qs = qs.annotate( + qs = ( + super() + .get_queryset() + .annotate( members_count=Count('members'), + is_member=Exists(TeamMember.objects.filter(team=OuterRef('pk'), user=self.request.user)), + is_manager=Exists(TeamMember.objects.filter(team=OuterRef('pk'), user=self.request.user, can_manage=True)), ) - 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()), ) + if self.request.user.is_superuser: + return qs + if member.is_staff: + return qs.exclude(visibility=Team.TeamVisibility.HIDDEN, is_member=False) + return qs.filter(Q(visibility=Team.TeamVisibility.PUBLIC) | Q(is_member=True)) + 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') + def get_queryset(self): + return ( + super() + .get_queryset() + .prefetch_related('members', 'members__user') + .annotate(member_id=Subquery(TeamMember.objects.filter(team=OuterRef('pk'), user=self.request.user).values('id')[:1])) + ) def get_context_data(self, **kwargs) -> dict[str, Any]: if not Team.type_is(team := self.object): # pragma: no cover raise ValueError('Invalid object type') - member_id = self.get_queryset().filter(uuid=team.uuid, members__user=self.request.user).values_list('members__id', flat=True).first() log_entries = team.logentries.order_by('-timestamp') return { **super().get_context_data(**kwargs), - 'member_id': member_id, 'received_invitations': team.received_invitations.filter(state=Invitation.RequestsState.REQUESTED), 'sent_invitations': team.sent_invitations.filter(state=Invitation.RequestsState.REQUESTED), 'all_log_entries': log_entries.all(), diff --git a/src/core/admin.py b/src/core/admin.py index 729f8bbaecd9b6301f3d29ecd1f40b7acd73728d..216cefcfa5c5cd90120f02881de53950b16d0b83 100644 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -322,9 +322,9 @@ class TeamMemberInline(admin.TabularInline): class TeamAdmin(GroupAdmin): model = Team inlines = [TeamMemberInline] - list_display = ['name', 'description', 'require_staff', 'member_count'] + list_display = ['name', 'description', 'visibility', 'member_count'] list_display_links = ['name'] - list_filter = ['require_staff'] + list_filter = ['visibility'] readonly_fields = ['description_html'] search_fields = ['name', 'description'] diff --git a/src/core/forms/teams.py b/src/core/forms/teams.py index e1f91be9ee1bb9352402e47035ff3b2fb6127059..57d18711818e0a5a2e910079801c3503ae036e60 100644 --- a/src/core/forms/teams.py +++ b/src/core/forms/teams.py @@ -11,7 +11,7 @@ class TeamForm(ModelForm): 'name', 'description_en', 'description_de', - 'require_staff', + 'visibility', ] help_texts = { 'name': _('Team__name__help'), @@ -22,7 +22,7 @@ class TeamForm(ModelForm): self.conference = conference super().__init__(*args, instance=instance, **kwargs) if not super_user: - del self.fields['require_staff'] + del self.fields['visibility'] def save(self, commit: bool = True) -> Team: team = super().save(commit=False) diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po index 0ea55dfcb048f77cba45eb3165692da184c03e35..1175719fd8d344f3adf5fb0bc55f6f8c6ee52a96 100644 --- a/src/core/locale/de/LC_MESSAGES/django.po +++ b/src/core/locale/de/LC_MESSAGES/django.po @@ -2008,6 +2008,15 @@ msgstr "Dieses Mitglied ist das letzte mit Verwaltungsrechten, sie können nicht msgid "TeamMember__delete__cannot_delete_last_manager" msgstr "Du kannst dieses Mitglied nicht entfernen, da es das letzte Mitglied mit Verwaltungsrechten ist." +msgid "Team__visibility__Public" +msgstr "öffentlich" + +msgid "Team__visibility__Staff" +msgstr "Konferenz-Team" + +msgid "Team__visibility__Hidden" +msgstr "Admins" + msgid "Teams" msgstr "" @@ -2023,11 +2032,11 @@ msgstr "öffentliche Information (Markdown unterstützt)." msgid "Team__description_html" msgstr "Das gerenderte HTML der Beschreibung" -msgid "Team__require_staff" -msgstr "Nur für Konferenz-Team Mitglieder" +msgid "Team__visibility" +msgstr "Sichtbarkeit" -msgid "Team__require_staff__help" -msgstr "Dieses Team ist nur für Konferenz-Team Mitglieder zugreifbar." +msgid "Team__visibility__help" +msgstr "Für wen ist dieses Team sichtbar?" msgid "ConferenceMemberTicket__token_wrong_conference" msgstr "Das Ticket ist nicht für diese Konferenz." diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index 13724a859864f24fe1229afe95f922f91cf711e0..9492069223bcb22df38ddabf0ae025211a646e19 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -2006,6 +2006,15 @@ msgstr "You cannot remove this members rights, as they are the last manager of t msgid "TeamMember__delete__cannot_delete_last_manager" msgstr "You cannot remove this member, as it is the last manager of the team" +msgid "Team__visibility__Public" +msgstr "public" + +msgid "Team__visibility__Staff" +msgstr "conference team" + +msgid "Team__visibility__Hidden" +msgstr "admins" + msgid "Teams" msgstr "" @@ -2021,11 +2030,11 @@ msgstr "public information (markdown supported)." msgid "Team__description_html" msgstr "the html rendered from the description" -msgid "Team__require_staff" -msgstr "require staff status" +msgid "Team__visibility" +msgstr "visibility" -msgid "Team__require_staff__help" -msgstr "This team is only accessible to persons with the staff status." +msgid "Team__visibility__help" +msgstr "Who may see this team?" msgid "ConferenceMemberTicket__token_wrong_conference" msgstr "This ticket is for another conference." diff --git a/src/core/migrations/0171_remove_team_require_staff_team_visibility.py b/src/core/migrations/0171_remove_team_require_staff_team_visibility.py new file mode 100644 index 0000000000000000000000000000000000000000..cc5b51f7edf9f943a1f153a9e2c0f68489795cc9 --- /dev/null +++ b/src/core/migrations/0171_remove_team_require_staff_team_visibility.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.3 on 2024-12-24 01:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0170_alter_assembly_slug"), + ] + + operations = [ + migrations.RemoveField( + model_name="team", + name="require_staff", + ), + migrations.AddField( + model_name="team", + name="visibility", + field=models.CharField( + choices=[ + ("public", "Team__visibility__Public"), + ("staff", "Team__visibility__Staff"), + ("hidden", "Team__visibility__Hidden"), + ], + default="staff", + help_text="Team__visibility__help", + verbose_name="Team__visibility", + ), + ), + ] diff --git a/src/core/models/teams/teams.py b/src/core/models/teams/teams.py index ccd8b07cfc970a2a44999cf243ddec78cd914f97..5a445ecc4326faa73865ea34fe5ccee6870971fa 100644 --- a/src/core/models/teams/teams.py +++ b/src/core/models/teams/teams.py @@ -32,24 +32,36 @@ def is_team_manager(user: 'PlatformUser', team: 'Team | None' = None) -> bool: return user.is_authenticated and team.members.filter(user=user, can_manage=True).exists() +@predicate +def is_staff_team(user: 'PlatformUser', team: 'Team | None' = None) -> bool: + if team is None: # pragma: no cover + return False + return team.visibility == Team.TeamVisibility.STAFF + + @predicate def is_public_team(user: 'PlatformUser', team: 'Team | None' = None) -> bool: # If team is None, we are checking for the permission to view the list of teams, everybody can see it. if team is None: return True - return not team.require_staff + return team.visibility == Team.TeamVisibility.PUBLIC class Team(RulesModelMixin, ActivityLogMixin, Group, metaclass=RulesModelBase): + class TeamVisibility(models.TextChoices): + PUBLIC = 'public', _('Team__visibility__Public') + STAFF = 'staff', _('Team__visibility__Staff') + HIDDEN = 'hidden', _('Team__visibility__Hidden') + class Meta: rules_permissions = { - 'view': is_conference_staff | is_public_team, + 'view': is_team_member | is_public_team | is_superuser | (is_conference_staff & is_staff_team), 'view_details': is_team_member | is_superuser, 'add': is_superuser, 'change': is_team_manager | is_superuser, 'change_permissions': is_superuser, 'delete': is_superuser, - 'can_join': is_conference_staff | is_public_team, + 'can_join': (is_conference_staff & is_staff_team) | is_public_team | is_superuser, } indexes = [ models.Index(fields=['uuid']), @@ -72,11 +84,11 @@ class Team(RulesModelMixin, ActivityLogMixin, Group, metaclass=RulesModelBase): null=True, verbose_name=_('Team__description_html'), ) - # Is this ream only for staff members (e.g. assembly team)? - require_staff = models.BooleanField( - default=False, - verbose_name=_('Team__require_staff'), - help_text=_('Team__require_staff__help'), + visibility = models.CharField( + default=TeamVisibility.STAFF, + verbose_name=_('Team__visibility'), + help_text=_('Team__visibility__help'), + choices=TeamVisibility.choices, ) sent_invitations = GenericRelation(