From b07d4dbebc34e6b9abc589899a9290b02791e8aa Mon Sep 17 00:00:00 2001 From: Ryan Cross <rcross@amsl.com> Date: Fri, 15 Nov 2024 18:18:09 +0000 Subject: [PATCH] feat: add group leadership list (#8135) * feat: add Group Leadership list * fix: only offer export to staff * fix: fix export button conditional * fix: improve tests. black format --------- Co-authored-by: Robert Sparks <rjsparks@nostrum.com> --- ietf/group/tests.py | 47 ++++++++++++++++++++++ ietf/group/urls.py | 4 +- ietf/group/views.py | 43 ++++++++++++++++++++ ietf/templates/group/group_leadership.html | 34 ++++++++++++++++ 4 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 ietf/templates/group/group_leadership.html diff --git a/ietf/group/tests.py b/ietf/group/tests.py index 130c68b3fc..31f8cc45b5 100644 --- a/ietf/group/tests.py +++ b/ietf/group/tests.py @@ -65,6 +65,53 @@ def test_stream_edit(self): self.assertTrue(Role.objects.filter(name="delegate", group__acronym=stream_acronym, email__address="ad2@ietf.org")) +class GroupLeadershipTests(TestCase): + def test_leadership_wg(self): + # setup various group states + bof_role = RoleFactory( + group__type_id="wg", group__state_id="bof", name_id="chair" + ) + proposed_role = RoleFactory( + group__type_id="wg", group__state_id="proposed", name_id="chair" + ) + active_role = RoleFactory( + group__type_id="wg", group__state_id="active", name_id="chair" + ) + conclude_role = RoleFactory( + group__type_id="wg", group__state_id="conclude", name_id="chair" + ) + url = urlreverse( + "ietf.group.views.group_leadership", kwargs={"group_type": "wg"} + ) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertContains(r, "Group Leadership") + self.assertContains(r, bof_role.person.last_name()) + self.assertContains(r, proposed_role.person.last_name()) + self.assertContains(r, active_role.person.last_name()) + self.assertNotContains(r, conclude_role.person.last_name()) + + def test_leadership_wg_csv(self): + url = urlreverse( + "ietf.group.views.group_leadership_csv", kwargs={"group_type": "wg"} + ) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertEqual(r["Content-Type"], "text/csv") + self.assertContains(r, "Chairman, Sops") + + def test_leadership_rg(self): + role = RoleFactory(group__type_id="rg", name_id="chair") + url = urlreverse( + "ietf.group.views.group_leadership", kwargs={"group_type": "rg"} + ) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertContains(r, "Group Leadership") + self.assertContains(r, role.person.last_name()) + self.assertNotContains(r, "Chairman, Sops") + + class GroupStatsTests(TestCase): def setUp(self): super().setUp() diff --git a/ietf/group/urls.py b/ietf/group/urls.py index b2af8d9e2b..1824564c4d 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -57,7 +57,9 @@ group_urls = [ - url(r'^$', views.active_groups), + url(r'^$', views.active_groups), + url(r'^leadership/(?P<group_type>(wg|rg))/$', views.group_leadership), + url(r'^leadership/(?P<group_type>(wg|rg))/csv/$', views.group_leadership_csv), url(r'^groupstats.json', views.group_stats_data, None, 'ietf.group.views.group_stats_data'), url(r'^groupmenu.json', views.group_menu_data, None, 'ietf.group.views.group_menu_data'), url(r'^chartering/$', views.chartering_groups), diff --git a/ietf/group/views.py b/ietf/group/views.py index 71986384e0..f30569d230 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -35,6 +35,7 @@ import copy +import csv import datetime import itertools import math @@ -437,6 +438,48 @@ def prepare_group_documents(request, group, clist): return docs, meta, docs_related, meta_related + +def get_leadership(group_type): + people = Person.objects.filter( + role__name__slug="chair", + role__group__type=group_type, + role__group__state__slug__in=("active", "bof", "proposed"), + ).distinct() + leaders = [] + for person in people: + parts = person.name_parts() + groups = [ + r.group.acronym + for r in person.role_set.filter( + name__slug="chair", + group__type=group_type, + group__state__slug__in=("active", "bof", "proposed"), + ) + ] + entry = {"name": "%s, %s" % (parts[3], parts[1]), "groups": ", ".join(groups)} + leaders.append(entry) + return sorted(leaders, key=lambda a: a["name"]) + + +def group_leadership(request, group_type=None): + context = {} + context["leaders"] = get_leadership(group_type) + context["group_type"] = group_type + return render(request, "group/group_leadership.html", context) + + +def group_leadership_csv(request, group_type=None): + leaders = get_leadership(group_type) + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = ( + f'attachment; filename="group_leadership_{group_type}.csv"' + ) + writer = csv.writer(response, dialect=csv.excel, delimiter=str(",")) + writer.writerow(["Name", "Groups"]) + for leader in leaders: + writer.writerow([leader["name"], leader["groups"]]) + return response + def group_home(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) kwargs = dict(acronym=group.acronym) diff --git a/ietf/templates/group/group_leadership.html b/ietf/templates/group/group_leadership.html new file mode 100644 index 0000000000..644be3e150 --- /dev/null +++ b/ietf/templates/group/group_leadership.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2024, All Rights Reserved #} +{% load origin static person_filters ietf_filters %} +{% block pagehead %} + <link rel="stylesheet" href="{% static 'ietf/css/list.css' %}"> +{% endblock %} +{% block title %}Group Leadership{% endblock %} +{% block content %} + {% origin %} + <h1>Group Leadership ({{ group_type }})</h1> + {% if user|has_role:"Secretariat" %} + <div class="text-end"> + <a class="btn btn-primary" href="{% url 'ietf.group.views.group_leadership_csv' group_type=group_type %}"> + <i class="bi bi-file-ruled"></i> Export as CSV + </a> + </div> + {% endif %} + <table class="table table-sm table-striped"> + <thead> + <tr> + <th scope="col">Leader</th> + <th scope="col">Groups</th> + </tr> + </thead> + <tbody> + {% for leader in leaders %} + <tr> + <td>{{ leader.name }}</td> + <td>{{ leader.groups }}</td> + </tr> + {% endfor %} + </tbody> + </table> +{% endblock %}