Skip to content

Commit dcce2df

Browse files
authored
feat: add attendance summary and pie chart to meeting attendees page (#10481)
* feat: add attendance summary and pie chart to meeting attendees page For IETF meetings ≥ 118, the attendees proceedings page now shows an Onsite / Remote / Total summary row matching the counts displayed on registration.ietf.org, together with a "View chart" button that opens a Bootstrap modal containing a Highcharts pie chart. * Split out attendees-chart.js
1 parent 0b1c87e commit dcce2df

5 files changed

Lines changed: 151 additions & 7 deletions

File tree

ietf/meeting/tests_views.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9007,6 +9007,8 @@ def test_proceedings_attendees(self):
90079007
- assert onsite checkedin=True appears, not onsite checkedin=False
90089008
- assert remote attended appears, not remote not attended
90099009
- prefer onsite checkedin=True to remote attended when same person has both
9010+
- summary stats row shows correct counts
9011+
- chart data JSON is embedded with correct values
90109012
"""
90119013

90129014
m = MeetingFactory(type_id='ietf', date=datetime.date(2023, 11, 4), number="118")
@@ -9028,6 +9030,17 @@ def test_proceedings_attendees(self):
90289030
text = q('#id_attendees tbody tr').text().replace('\n', ' ')
90299031
self.assertEqual(text, f"A Person {areg.affiliation} {areg.country_code} onsite C Person {creg.affiliation} {creg.country_code} remote")
90309032

9033+
# Summary stats row: Onsite / Remote / Total (matches registration.ietf.org)
9034+
self.assertContains(response, 'Onsite:')
9035+
self.assertContains(response, 'Remote:')
9036+
self.assertContains(response, 'Total:')
9037+
self.assertContains(response, '<strong>1</strong>') # onsite and remote
9038+
self.assertContains(response, '<strong>2</strong>') # total
9039+
9040+
# Chart data embedded in page
9041+
chart_json = json.loads(q('#attendees-chart-data').text())
9042+
self.assertEqual(chart_json['type'], [['Onsite', 1], ['Remote', 1]])
9043+
90319044
def test_proceedings_overview(self):
90329045
'''Test proceedings IETF Overview page.
90339046
Note: old meetings aren't supported so need to add a new meeting then test.

ietf/meeting/views.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
from ietf.meeting.utils import get_activity_stats, post_process, create_recording, delete_recording
110110
from ietf.meeting.utils import participants_for_meeting, generate_bluesheet, bluesheet_data, save_bluesheet
111111
from ietf.message.utils import infer_message
112-
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName
112+
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName, CountryName
113113
from ietf.utils import markdown
114114
from ietf.utils.decorators import require_api_key
115115
from ietf.utils.hedgedoc import Note, NoteError
@@ -4812,15 +4812,36 @@ def proceedings_attendees(request, num=None):
48124812
template = None
48134813
registrations = None
48144814

4815+
stats = None
4816+
chart_data = None
4817+
48154818
if int(meeting.number) >= 118:
48164819
checked_in, attended = participants_for_meeting(meeting)
48174820
regs = list(Registration.objects.onsite().filter(meeting__number=num, checkedin=True))
4818-
4819-
for reg in Registration.objects.remote().filter(meeting__number=num).select_related('person'):
4820-
if reg.person.pk in attended and reg.person.pk not in checked_in:
4821-
regs.append(reg)
4821+
onsite_count = len(regs)
4822+
regs += [
4823+
reg
4824+
for reg in Registration.objects.remote().filter(meeting__number=num).select_related('person')
4825+
if reg.person.pk in attended and reg.person.pk not in checked_in
4826+
]
4827+
remote_count = len(regs) - onsite_count
48224828

48234829
registrations = sorted(regs, key=lambda x: (x.last_name, x.first_name))
4830+
4831+
country_codes = [r.country_code for r in registrations if r.country_code]
4832+
stats = {
4833+
'total': onsite_count + remote_count,
4834+
'onsite': onsite_count,
4835+
'remote': remote_count,
4836+
}
4837+
4838+
code_to_name = dict(CountryName.objects.values_list('slug', 'name'))
4839+
country_counts = Counter(code_to_name.get(c, c) for c in country_codes).most_common()
4840+
4841+
chart_data = {
4842+
'type': [['Onsite', onsite_count], ['Remote', remote_count]],
4843+
'countries': country_counts,
4844+
}
48244845
else:
48254846
overview_template = "/meeting/proceedings/%s/attendees.html" % meeting.number
48264847
try:
@@ -4832,6 +4853,8 @@ def proceedings_attendees(request, num=None):
48324853
'meeting': meeting,
48334854
'registrations': registrations,
48344855
'template': template,
4856+
'stats': stats,
4857+
'chart_data': chart_data,
48354858
})
48364859

48374860
def proceedings_overview(request, num=None):

ietf/static/js/attendees-chart.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
(function () {
2+
var raw = document.getElementById('attendees-chart-data');
3+
if (!raw) return;
4+
var chartData = JSON.parse(raw.textContent);
5+
var chart = null;
6+
var currentBreakdown = 'type';
7+
8+
// Override the global transparent background set by highcharts.js so the
9+
// export menu and fullscreen view use the page background color.
10+
var container = document.getElementById('attendees-pie-chart');
11+
var bodyBg = getComputedStyle(document.body).backgroundColor;
12+
container.style.setProperty('--highcharts-background-color', bodyBg);
13+
14+
function renderChart(breakdown) {
15+
var seriesData = chartData[breakdown].map(function (item) {
16+
return { name: item[0], y: item[1] };
17+
});
18+
if (chart) chart.destroy();
19+
chart = Highcharts.chart(container, {
20+
chart: { type: 'pie', height: 400 },
21+
title: { text: null },
22+
tooltip: { pointFormat: '{point.name}: <b>{point.y}</b> ({point.percentage:.1f}%)' },
23+
plotOptions: {
24+
pie: {
25+
dataLabels: {
26+
enabled: true,
27+
format: '<b>{point.name}</b><br>{point.y} ({point.percentage:.1f}%)',
28+
},
29+
showInLegend: false,
30+
}
31+
},
32+
series: [{ name: 'Attendees', data: seriesData }],
33+
});
34+
}
35+
36+
var modal = document.getElementById('attendees-chart-modal');
37+
38+
// Render (or re-render) the chart each time the modal becomes fully visible,
39+
// so Highcharts can measure the container dimensions correctly.
40+
modal.addEventListener('shown.bs.modal', function () {
41+
renderChart(currentBreakdown);
42+
});
43+
44+
// Release the chart when the modal closes to avoid stale renders.
45+
modal.addEventListener('hidden.bs.modal', function () {
46+
if (chart) {
47+
chart.destroy();
48+
chart = null;
49+
}
50+
});
51+
52+
document.querySelectorAll('[name="attendees-breakdown"]').forEach(function (radio) {
53+
radio.addEventListener('change', function () {
54+
currentBreakdown = this.value;
55+
renderChart(currentBreakdown);
56+
});
57+
});
58+
})();

ietf/templates/meeting/proceedings_attendees.html

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
{% load origin markup_tags static %}
44
{% block pagehead %}
55
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
6+
{% if chart_data %}<link rel="stylesheet" href="{% static "ietf/css/highcharts.css" %}">{% endif %}
67
{% endblock %}
78
{% block title %}IETF {{ meeting.number }} proceedings{% endblock %}
89
{% block content %}
@@ -14,8 +15,52 @@ <h1>
1415
</a>
1516
</h1>
1617
<h2>Attendee list of IETF {{ meeting.number }} meeting</h2>
17-
18+
19+
{% if chart_data %}
20+
<div class="d-flex align-items-center gap-4 mb-3 flex-wrap">
21+
<div class="d-flex gap-4">
22+
<div>Onsite: <strong>{{ stats.onsite }}</strong></div>
23+
<div>Remote: <strong>{{ stats.remote }}</strong></div>
24+
<div>Total: <strong>{{ stats.total }}</strong></div>
25+
</div>
26+
<button type="button" class="btn btn-primary btn-sm"
27+
data-bs-toggle="modal" data-bs-target="#attendees-chart-modal">
28+
<i class="bi bi-pie-chart-fill"></i> View chart
29+
</button>
30+
</div>
31+
32+
<div class="modal fade" id="attendees-chart-modal" tabindex="-1"
33+
aria-labelledby="attendees-chart-modal-label" aria-hidden="true">
34+
<div class="modal-dialog modal-lg">
35+
<div class="modal-content">
36+
<div class="modal-header">
37+
<h2 class="modal-title fs-5" id="attendees-chart-modal-label">
38+
IETF {{ meeting.number }} attendees
39+
</h2>
40+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
41+
</div>
42+
<div class="modal-body">
43+
<div class="btn-group mb-3" role="group" aria-label="Chart breakdown">
44+
<input type="radio" class="btn-check" name="attendees-breakdown"
45+
id="breakdown-type" value="type" checked>
46+
<label class="btn btn-outline-secondary btn-sm" for="breakdown-type">Onsite/Remote</label>
47+
<input type="radio" class="btn-check" name="attendees-breakdown"
48+
id="breakdown-countries" value="countries">
49+
<label class="btn btn-outline-secondary btn-sm" for="breakdown-countries">Countries</label>
50+
</div>
51+
<div id="attendees-pie-chart" class="bg-body"></div>
52+
</div>
53+
</div>
54+
</div>
55+
</div>
56+
57+
{{ chart_data|json_script:"attendees-chart-data" }}
58+
{% endif %}{# chart_data #}
59+
1860
{% if template %}
61+
<div class="alert alert-info" role="alert">
62+
Attendee statistics are not available for this meeting.
63+
</div>
1964
{{template|safe}}
2065
{% else %}
2166
<table id="id_attendees" class="table table-sm table-striped tablesorter">
@@ -44,4 +89,8 @@ <h2>Attendee list of IETF {{ meeting.number }} meeting</h2>
4489
{% endblock %}
4590
{% block js %}
4691
<script src="{% static "ietf/js/list.js" %}"></script>
47-
{% endblock %}
92+
{% if chart_data %}
93+
<script src="{% static "ietf/js/highcharts.js" %}"></script>
94+
<script src="{% static "ietf/js/attendees-chart.js" %}"></script>
95+
{% endif %}
96+
{% endblock %}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"ietf/static/images/irtf-logo-white.svg",
113113
"ietf/static/images/irtf-logo.svg",
114114
"ietf/static/js/add_session_recordings.js",
115+
"ietf/static/js/attendees-chart.js",
115116
"ietf/static/js/agenda_filter.js",
116117
"ietf/static/js/agenda_materials.js",
117118
"ietf/static/js/announcement.js",

0 commit comments

Comments
 (0)