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 %}