feat: Add requests summary (#5439)

* feat: Add summary data to meeting/requests

* feat: Add Group Type and Purpose tables to requests summary

* fix: use self.assertXX instead of raw assert

---------

Co-authored-by: Robert Sparks <rjsparks@nostrum.com>
This commit is contained in:
Ryan Cross 2023-03-30 08:07:59 +09:00 committed by GitHub
parent 0f1a6c960f
commit 1bd5c5e2f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 158 additions and 2 deletions

View file

@ -49,6 +49,7 @@ from ietf.meeting.utils import finalize, condition_slide_order
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.meeting.utils import create_recording, get_next_sequence
from ietf.meeting.views import session_draft_list, parse_agenda_filter_params, sessions_post_save, agenda_extract_schedule
from ietf.meeting.views import get_summary_by_area, get_summary_by_type, get_summary_by_purpose
from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, ProceedingsMaterialTypeName
from ietf.utils.decorators import skip_coverage
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
@ -6589,6 +6590,28 @@ class ImportNotesTests(TestCase):
class SessionTests(TestCase):
def test_get_summary_by_area(self):
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
sessions = Session.objects.filter(meeting=meeting).with_current_status()
data = get_summary_by_area(sessions)
self.assertEqual(data[0][0], 'Duration')
self.assertGreater(len(data), 2)
self.assertEqual(data[-1][0], 'Total Hours')
def test_get_summary_by_type(self):
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
sessions = Session.objects.filter(meeting=meeting).with_current_status()
data = get_summary_by_type(sessions)
self.assertEqual(data[0][0], 'Group Type')
self.assertGreater(len(data), 2)
def test_get_summary_by_purpose(self):
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
sessions = Session.objects.filter(meeting=meeting).with_current_status()
data = get_summary_by_purpose(sessions)
self.assertEqual(data[0][0], 'Purpose')
self.assertGreater(len(data), 2)
def test_meeting_requests(self):
meeting = MeetingFactory(type_id='ietf')

View file

@ -101,7 +101,9 @@ from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSession
InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm,
UploadAgendaForm, UploadBlueSheetForm, UploadMinutesForm, UploadSlidesForm)
request_summary_exclude_group_types = ['team']
def get_interim_menu_entries(request):
'''Setup menu entries for interim meeting view tabs'''
entries = []
@ -2238,6 +2240,70 @@ def agenda_json(request, num=None):
response['Last-Modified'] = format_date_time(timegm(last_modified.timetuple()))
return response
def request_summary_filter(session):
if (session.group.area is None
or session.group.type.slug in request_summary_exclude_group_types
or session.current_status == 'notmeet'):
return False
return True
def get_area_column(area):
if area is None:
return ''
if area.type.slug in ['rfcedtyp']:
name = 'OTHER'
else:
name = area.acronym.upper()
return name
def get_summary_by_area(sessions):
"""Returns summary by area for list of session requests.
Summary is a two dimensional array[row=session duration][col=session area count]
It also includes row and column headers as well as a totals row.
"""
# first build a dictionary of counts, key=(duration,area)
durations = set()
areas = set()
duration_totals = defaultdict(int)
data = defaultdict(int)
for session in sessions:
area_column = get_area_column(session.group.area)
duration = session.requested_duration.seconds / 3600
key = (duration, area_column)
data[key] = data[key] + 1
durations.add(duration)
areas.add(area_column)
duration_totals[duration] = duration_totals[duration] + 1
# build two dimensional array for use in template
rows = []
sorted_areas = sorted(areas)
# move "other" to end
if 'OTHER' in sorted_areas:
sorted_areas.remove('OTHER')
sorted_areas.append('OTHER')
# add header row
rows.append(['Duration'] + sorted_areas + ['TOTAL SLOTS', 'TOTAL HOURS'])
for duration in sorted(durations):
rows.append([duration] + [data[(duration, a)] for a in sorted_areas] + [duration_totals[duration]] + [duration_totals[duration] * duration])
# add total row
rows.append(['Total Slots'] + [sum([rows[r][c] for r in range(1, len(rows))]) for c in range(1, len(rows[0]))])
rows.append(['Total Hours'] + [sum([d * data[(d, area)] for d in durations]) for area in sorted_areas])
return rows
def get_summary_by_type(sessions):
counter = Counter([s.group.type.name for s in sessions])
data = counter.most_common()
data.insert(0, ('Group Type', 'Count'))
return data
def get_summary_by_purpose(sessions):
counter = Counter([s.purpose.name for s in sessions])
data = counter.most_common()
data.insert(0, ('Purpose', 'Count'))
return data
def meeting_requests(request, num=None):
meeting = get_meeting(num)
groups_to_show = Group.objects.filter(
@ -2253,7 +2319,7 @@ def meeting_requests(request, num=None):
).with_current_status().with_requested_by().exclude(
requested_by=0
).prefetch_related(
"group","group__ad_role__person"
"group", "group__ad_role__person", "group__type"
)
)
@ -2276,12 +2342,14 @@ def meeting_requests(request, num=None):
)
groups_not_meeting = groups_to_show.exclude(
acronym__in = [session.group.acronym for session in sessions]
acronym__in=[session.group.acronym for session in sessions]
).order_by(
"parent__acronym",
"acronym",
).prefetch_related("parent")
summary_sessions = list(filter(request_summary_filter, sessions))
return render(
request,
"meeting/requests.html",
@ -2289,6 +2357,9 @@ def meeting_requests(request, num=None):
"meeting": meeting,
"sessions": sessions,
"groups_not_meeting": groups_not_meeting,
"summary_by_area": get_summary_by_area(summary_sessions),
"summary_by_group_type": get_summary_by_type(summary_sessions),
"summary_by_purpose": get_summary_by_purpose(summary_sessions),
},
)

View file

@ -15,6 +15,68 @@
{% if meeting.venue_name %} {{ meeting.venue_name }}{% endif %}
</small>
</h1>
<h2 class="mt-3">Requests Summary</h2>
<div class="mt-2">
<table class="table table-sm">
<tbody>
{% for row in summary_by_area %}
<tr>
{% if forloop.first %}
{% for col in row %}
<th scope="col" class="table-primary">{{ col }}</th>
{% endfor %}
{% else %}
{% for col in row %}
<td>{{ col }}</td>
{% endfor %}
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="row mt-2">
<div class="col">
<table class="table table-sm">
<tbody>
{% for row in summary_by_group_type %}
<tr>
{% if forloop.first %}
{% for col in row %}
<th scope="col" class="table-primary">{{ col }}</th>
{% endfor %}
{% else %}
{% for col in row %}
<td>{{ col }}</td>
{% endfor %}
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col">
<table class="table table-sm">
<tbody>
{% for row in summary_by_purpose %}
<tr>
{% if forloop.first %}
{% for col in row %}
<th scope="col" class="table-primary">{{ col }}</th>
{% endfor %}
{% else %}
{% for col in row %}
<td>{{ col }}</td>
{% endfor %}
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% regroup sessions by display_area as area_sessions %}
{% for area in area_sessions %}
<h2 class="mt-5" id="{% firstof area.grouper.acronym "other-groups" %}">