fix: Separately show proceedings sessions with different names (#5005)

* refactor: Update call to deprecated add_event_info_to_session_qs

* feat: Gather proceedings sessions in the view, grouped by name

* feat: Begin using new session data format in templates (WIP)

* feat: Show non-meeting groups (WIP)

Non-meeting groups (all sessions are notmeet) now show up on the proceedings.
Session materials associated with these groups are not shown, need to restore
that functionality.

* refactor: Rework template data, show materials for notmeet groups (WIP)

* fix: Restore "No agenda", etc, when meeting materials are not present

* chore: Remove commented out old code

* fix: Restore contents in non-area sections of proceedings

* chore: Remove commented-out stale code

* fix: Suppress duplicate agendas for a group on proceedings

* refactor: Generalize agenda deduplication and apply to minutes

* refactor: Format multiple items per session; apply to bluesheets

* refactor: Apply _format_materials to recordings, slides, and drafts

* chore: Add comment about limitations of test_proceedings() test

* test: Test separation of named sessions in the proceedings
This commit is contained in:
Jennifer Richards 2023-01-20 17:16:48 -04:00 committed by GitHub
parent b931c8e38f
commit c6663eb593
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 362 additions and 258 deletions

View file

@ -7310,7 +7310,12 @@ class ProceedingsTests(BaseMeetingTestCase):
)
def test_proceedings(self):
"""Proceedings should be displayed correctly"""
"""Proceedings should be displayed correctly
Currently only tests that the view responds with a 200 response code and checks the ProceedingsMaterials
at the top of the proceedings. Ought to actually test the display of the individual group/session
materials as well.
"""
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
GroupEventFactory(group=session.group,type='status_update')
@ -7364,6 +7369,65 @@ class ProceedingsTests(BaseMeetingTestCase):
self._assertMeetingHostsDisplayed(r, meeting)
self._assertProceedingsMaterialsDisplayed(r, meeting)
def test_named_session(self):
"""Session with a name should appear separately in the proceedings"""
meeting = MeetingFactory(type_id='ietf', number='100')
group = GroupFactory()
plain_session = SessionFactory(meeting=meeting, group=group)
named_session = SessionFactory(meeting=meeting, group=group, name='I Got a Name')
for doc_type_id in ('agenda', 'minutes', 'bluesheets', 'recording', 'slides', 'draft'):
# Set up sessions materials that will have distinct URLs for each session.
# This depends on settings.MEETING_DOC_HREFS and may need updating if that changes.
SessionPresentationFactory(
session=plain_session,
document__type_id=doc_type_id,
document__uploaded_filename=f'upload-{doc_type_id}-plain',
document__external_url=f'external_url-{doc_type_id}-plain',
)
SessionPresentationFactory(
session=named_session,
document__type_id=doc_type_id,
document__uploaded_filename=f'upload-{doc_type_id}-named',
document__external_url=f'external_url-{doc_type_id}-named',
)
url = urlreverse('ietf.meeting.views.proceedings', kwargs={'num': meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
plain_label = q(f'div#{group.acronym}')
self.assertEqual(plain_label.text(), group.acronym)
plain_row = plain_label.closest('tr')
self.assertTrue(plain_row)
named_label = q(f'div#{slugify(named_session.name)}')
self.assertEqual(named_label.text(), named_session.name)
named_row = named_label.closest('tr')
self.assertTrue(named_row)
for material in (sp.document for sp in plain_session.sessionpresentation_set.all()):
if material.type_id == 'draft':
expected_url = urlreverse(
'ietf.doc.views_doc.document_main',
kwargs={'name': material.canonical_name()},
)
else:
expected_url = material.get_href(meeting)
self.assertTrue(plain_row.find(f'a[href="{expected_url}"]'))
self.assertFalse(named_row.find(f'a[href="{expected_url}"]'))
for material in (sp.document for sp in named_session.sessionpresentation_set.all()):
if material.type_id == 'draft':
expected_url = urlreverse(
'ietf.doc.views_doc.document_main',
kwargs={'name': material.canonical_name()},
)
else:
expected_url = material.get_href(meeting)
self.assertFalse(plain_row.find(f'a[href="{expected_url}"]'))
self.assertTrue(named_row.find(f'a[href="{expected_url}"]'))
def test_proceedings_no_agenda(self):
# Meeting number must be larger than the last special-cased proceedings (currently 96)
meeting = MeetingFactory(type_id='ietf',populate_schedule=False,date=date_today(), number='100')

View file

@ -3586,6 +3586,74 @@ def upcoming_json(request):
response = HttpResponse(json.dumps(data, indent=2, sort_keys=False), content_type='application/json;charset=%s'%settings.DEFAULT_CHARSET)
return response
def organize_proceedings_sessions(sessions):
# Collect sessions by Group, then bin by session name (including sessions with blank names).
# If all of a group's sessions are 'notmeet', the processed data goes in not_meeting_sessions.
# Otherwise, the data goes in meeting_sessions.
meeting_groups = []
not_meeting_groups = []
for group_acronym, group_sessions in itertools.groupby(sessions, key=lambda s: s.group.acronym):
by_name = {}
is_meeting = False
all_canceled = True
group = None
for s in sorted(
group_sessions,
key=lambda gs: (
gs.official_timeslotassignment().timeslot.time
if gs.official_timeslotassignment() else datetime.datetime(datetime.MAXYEAR, 1, 1)
),
):
group = s.group
if s.current_status != 'notmeet':
is_meeting = True
if s.current_status != 'canceled':
all_canceled = False
by_name.setdefault(s.name, [])
if s.current_status != 'notmeet' or s.sessionpresentation_set.exists():
by_name[s.name].append(s) # for notmeet, only include sessions with materials
for sess_name, ss in by_name.items():
def _format_materials(items):
"""Format session/material for template
Input is a list of (session, materials) pairs. The materials value can be a single value or a list.
"""
material_times = {} # key is material, value is first timestamp it appeared
for s, mats in items:
timestamp = s.official_timeslotassignment().timeslot.time
if not isinstance(mats, list):
mats = [mats]
for mat in mats:
if mat and mat not in material_times:
material_times[mat] = timestamp
n_mats = len(material_times)
result = []
if n_mats == 1:
result.append({'material': list(material_times)[0]}) # no 'time' when only a single material
elif n_mats > 1:
for mat, timestamp in material_times.items():
result.append({'material': mat, 'time': timestamp})
return result
entry = {
'group': group,
'name': sess_name,
'canceled': all_canceled,
# pass sessions instead of the materials here so session data (like time) is easily available
'agendas': _format_materials((s, s.agenda()) for s in ss),
'minutes': _format_materials((s, s.minutes()) for s in ss),
'bluesheets': _format_materials((s, s.bluesheets()) for s in ss),
'recordings': _format_materials((s, s.recordings()) for s in ss),
'slides': _format_materials((s, s.slides()) for s in ss),
'drafts': _format_materials((s, s.drafts()) for s in ss),
}
if is_meeting:
meeting_groups.append(entry)
else:
not_meeting_groups.append(entry)
return meeting_groups, not_meeting_groups
def proceedings(request, num=None):
meeting = get_meeting(num)
@ -3606,36 +3674,48 @@ def proceedings(request, num=None):
today_utc = date_today(datetime.timezone.utc)
schedule = get_schedule(meeting, None)
sessions = add_event_info_to_session_qs(
Session.objects.filter(meeting__number=meeting.number)
).filter(
Q(timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None]) | Q(current_status='notmeet')
).select_related().order_by('-current_status')
plenaries = sessions.filter(name__icontains='plenary').exclude(current_status='notmeet')
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu')
irtf = sessions.filter(group__parent__acronym = 'irtf')
training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['regular', 'other',]).exclude(current_status='notmeet')
iab = sessions.filter(group__parent__acronym = 'iab').exclude(current_status='notmeet')
sessions = (
meeting.session_set.with_current_status()
.filter(Q(timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None])
| Q(current_status='notmeet'))
.select_related()
.order_by('-current_status')
)
plenaries, _ = organize_proceedings_sessions(
sessions.filter(name__icontains='plenary')
.exclude(current_status='notmeet')
)
irtf, _ = organize_proceedings_sessions(
sessions.filter(group__parent__acronym = 'irtf').order_by('group__acronym')
)
training, _ = organize_proceedings_sessions(
sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['regular', 'other',])
.exclude(current_status='notmeet')
)
iab, _ = organize_proceedings_sessions(
sessions.filter(group__parent__acronym = 'iab')
.exclude(current_status='notmeet')
)
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu').order_by('group__parent__acronym', 'group__acronym')
ietf_areas = []
for area, area_sessions in itertools.groupby(
ietf,
key=lambda s: s.group.parent
):
meeting_groups, not_meeting_groups = organize_proceedings_sessions(area_sessions)
ietf_areas.append((area, meeting_groups, not_meeting_groups))
cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"]
ietf_areas = []
for area, sessions in itertools.groupby(sorted(ietf, key=lambda s: (s.group.parent.acronym, s.group.acronym)), key=lambda s: s.group.parent):
sessions = list(sessions)
meeting_groups = set(s.group_id for s in sessions if s.current_status != 'notmeet')
meeting_sessions = []
not_meeting_sessions = []
for s in sessions:
if s.current_status == 'notmeet' and s.group_id not in meeting_groups:
not_meeting_sessions.append(s)
else:
meeting_sessions.append(s)
ietf_areas.append((area, meeting_sessions, not_meeting_sessions))
with timezone.override(meeting.tz()):
return render(request, "meeting/proceedings.html", {
'meeting': meeting,
'plenaries': plenaries, 'ietf': ietf, 'training': training, 'irtf': irtf, 'iab': iab,
'plenaries': plenaries,
'training': training,
'irtf': irtf,
'iab': iab,
'ietf_areas': ietf_areas,
'cut_off_date': cut_off_date,
'cor_cut_off_date': cor_cut_off_date,

View file

@ -5,111 +5,85 @@
{% load proceedings_filters %}
<tr>
<td>
{% if session.name %}
<div id="{{ session.name|slugify }}">{{ session.name }}</div>
{% else %}
<div id="{{ session.group.acronym }}">
<a href="{% url 'ietf.group.views.group_home' acronym=session.group.acronym %}">{{ session.group.acronym }}</a>
{% if entry.name %}
<div id="{{ entry.name|slugify }}">{{ entry.name }}</div>
{% elif entry.group.acronym %}
<div id="{{ entry.group.acronym }}">
<a href="{% url 'ietf.group.views.group_home' acronym=entry.group.acronym %}">{{ entry.group.acronym }}</a>
</div>
{% if session.group.state_id == "bof" %}<span class="badge rounded-pill bg-success">BOF</span>{% endif %}
{% if entry.group.state_id == "bof" %}<span class="badge rounded-pill bg-success">BOF</span>{% endif %}
{% else %}
<h1>{{ entry.group }}</h1>
{% endif %}
</td>
{% if session.all_meeting_sessions_cancelled %}
{% if entry.canceled %}
<td colspan="4">
<span class="badge rounded-pill bg-danger">Session cancelled</span>
</td>
{% else %}
{# artifacts #}
<td>
{% if session.all_meeting_agendas %}
{% if session.all_meeting_agendas|length == 1 %}
<a href="{{ session.all_meeting_agendas.0|meeting_href:meeting }}">Agenda</a>
<br>
{% else %}
{% for agenda in session.all_meeting_agendas %}
<a href="{{ agenda|meeting_href:meeting }}">
Agenda {{ agenda.sessionpresentation_set.first.session.official_timeslotassignment.timeslot.time|date:"D G:i" }}
</a>
<br>
{% endfor %}
{% endif %}
{% else %}
{% if show_agenda == "True" and not meeting.proceedings_final %}
{% for agenda in entry.agendas %}
<a href="{{ agenda.material|meeting_href:meeting }}">
Agenda
{% if agenda.time %}{{agenda.time|date:"D G:i"}}{% endif %}
</a>
<br>
{% empty %}
{% if show_agenda and not meeting.proceedings_final %}
<span class="badge rounded-pill bg-warning">No agenda</span>
<br>
{% endif %}
{% endif %}
{% if session.all_meeting_minutes %}
{% if session.all_meeting_minutes|length == 1 %}
<a href="{{ session.all_meeting_minutes.0|meeting_href:meeting }}">Minutes</a>
<br>
{% else %}
{% for minutes in session.all_meeting_minutes %}
<a href="{{ minutes|meeting_href:meeting }}">
Minutes {{ minutes.sessionpresentation_set.first.session.official_timeslotassignment.timeslot.time|date:"D G:i" }}
</a>
<br>
{% endfor %}
{% endif %}
{% else %}
{% if show_agenda == "True" and not meeting.proceedings_final %}
{% endfor %}
{% for minutes in entry.minutes %}
<a href="{{ minutes.material|meeting_href:meeting }}">
Minutes
{% if minutes.time %}{{minutes.time|date:"D G:i"}}{% endif %}
</a>
<br>
{% empty %}
{% if show_agenda and not meeting.proceedings_final %}
<span class="badge rounded-pill bg-warning">No minutes</span>
<br>
{% endif %}
{% endif %}
{% if session.all_meeting_bluesheets %}
{% if session.all_meeting_bluesheets|length == 1 %}
<a href="{{ session.all_meeting_bluesheets.0|meeting_href:meeting }}">Bluesheets</a>
<br>
{% else %}
{% for bs in session.all_meeting_bluesheets %}
<a href="{{ bs|meeting_href:meeting }}">
Bluesheets {{ bs.sessionpresentation_set.first.session.official_timeslotassignment.timeslot.time|date:"D G:i" }}
</a>
<br>
{% endfor %}
{% endif %}
{% endif %}
{% with session.group|status_for_meeting:meeting as status %}
{% if status %}
<a href="{% url 'ietf.group.views.group_about_status_meeting' acronym=session.group.acronym num=meeting.number %}">
Status
</a>
<br>
{% endif %}
{% endwith %}
{% endfor %}
{% for bs in entry.bluesheets %}
<a href="{{ bs.material|meeting_href:meeting }}">
Bluesheets
{% if bs.time %}{{ bs.time|date:"D G:i" }}{% endif %}
</a>
<br>
{% endfor %}
</td>
{# recordings #}
<td>
{% if session.all_meeting_sessions_for_group|length == 1 %}
{% for rec in session.all_meeting_recordings %}
<a href="{{ rec|meeting_href:meeting|default:"#" }}">{{ rec|hack_recording_title:False }}</a>
<br>
{% endfor %}
{% else %}
{% for rec in session.all_meeting_recordings %}
<a href="{{ rec|meeting_href:meeting|default:"#" }}">{{ rec|hack_recording_title:True }}</a>
<br>
{% endfor %}
{% endif %}
{% for rec in entry.recordings %}
<a href="{{ rec.material|meeting_href:meeting|default:"#" }}">
{{ rec.material|hack_recording_title }}
{% if rec.time %}{{ rec.time|date:"D G:i"}}{% endif %}
</a>
<br>
{% endfor %}
</td>
{# slides #}
<td>
{% with session.all_meeting_slides as slides %}
{% for slide in slides %}
<a href="{{ slide|meeting_href:meeting }}">{{ slide.title|clean_whitespace }}</a>
<br>
{% empty %}
{% if not meeting.proceedings_final %}<span class="badge rounded-pill bg-warning">No slides</span>{% endif %}
{% endfor %}
{% endwith %}
{% for slide in entry.slides %}
<a href="{{ slide.material|meeting_href:meeting }}">{{ slide.material.title|clean_whitespace }}</a>
<br>
{% empty %}
{% if not meeting.proceedings_final %}<span class="badge rounded-pill bg-warning">No slides</span>{% endif %}
{% endfor %}
</td>
{# drafts #}
<td>
{% with session.all_meeting_drafts as drafts %}
{% for draft in drafts %}
<a href="{% url "ietf.doc.views_doc.document_main" name=draft.canonical_name %}">{{ draft.canonical_name }}</a>
<br>
{% empty %}
{% if not meeting.proceedings_final %}<span class="badge rounded-pill bg-warning">No drafts</span>{% endif %}
{% endfor %}
{% endwith %}
{% for draft in entry.drafts %}
<a href="{% url "ietf.doc.views_doc.document_main" name=draft.material.canonical_name %}">
{{ draft.material.canonical_name }}
</a>
<br>
{% empty %}
{% if not meeting.proceedings_final %}<span class="badge rounded-pill bg-warning">No drafts</span>{% endif %}
{% endfor %}
</td>
{% endif %}
</tr>
</tr>

View file

@ -23,10 +23,32 @@
{% load cache %}
{% cache 900 ietf_meeting_proceedings meeting.number cache_version %}
{% include 'meeting/proceedings/introduction.html' with meeting=meeting only %}
{% with "True" as show_agenda %}
<!-- Plenaries -->
{% if plenaries %}
<h2 class="mt-5" id="plenaries">Plenaries</h2>
<!-- Plenaries -->
{% if plenaries %}
<h2 class="mt-5" id="plenaries">Plenaries</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">Group</th>
<th scope="col" data-sort="artifacts">Artifacts</th>
<th scope="col" data-sort="recordings">Recordings</th>
<th scope="col" data-sort="slides">Slides</th>
<th scope="col" data-sort="drafts">Drafts</th>
</tr>
</thead>
<tbody>
{% for entry in plenaries %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% endif %}
<!-- Working groups -->
{% for area, meeting_groups, not_meeting_groups in ietf_areas %}
<h2 class="mt-5" id="{{ area.acronym }}">
{{ area.acronym|upper }} <small class="text-muted">{{ area.name }}</small>
</h2>
{% if meeting_groups %}
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
@ -38,161 +60,125 @@
</tr>
</thead>
<tbody>
{% for session in plenaries %}
{% include "meeting/group_proceedings.html" %}
{% for entry in meeting_groups %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% endif %}
<!-- Working groups -->
{% for area, meeting_sessions, not_meeting_sessions in ietf_areas %}
<h2 class="mt-5" id="{{ area.acronym }}">
{{ area.acronym|upper }} <small class="text-muted">{{ area.name }}</small>
</h2>
{% if meeting_sessions %}
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">Group</th>
<th scope="col" data-sort="artifacts">Artifacts</th>
<th scope="col" data-sort="recordings">Recordings</th>
<th scope="col" data-sort="slides">Slides</th>
<th scope="col" data-sort="drafts">Drafts</th>
</tr>
</thead>
<tbody>
{% for session in meeting_sessions %}
{% ifchanged session.group.acronym %}
{% include "meeting/group_proceedings.html" %}
{% endifchanged %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% if not_meeting_sessions %}
<p>
{{ area.name }} groups not meeting:
{% for session in not_meeting_sessions %}
{% ifchanged session.group.acronym %}
<a href="{% url 'ietf.group.views.group_home' acronym=session.group.acronym %}">{{ session.group.acronym }}</a>{% if not forloop.last %},{% endif %}
{% endifchanged %}
{% endfor %}
</p>
<table class="table table-sm table-striped">
<thead>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for session in not_meeting_sessions %}
{% ifchanged session.group.acronym %}
{% if session.sessionpresentation_set.exists %}
{% include "meeting/group_proceedings.html" %}
{% endif %}
{% endifchanged %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
<!-- Training Sessions -->
{% if training %}
{% with "False" as show_agenda %}
<h2 class="mt-5" id="training">Training</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">Group</th>
<th scope="col" data-sort="artifacts">Artifacts</th>
<th scope="col" data-sort="recordings">Recordings</th>
<th scope="col" data-sort="slides">Slides</th>
<th scope="col" data-sort="drafts">Drafts</th>
</tr>
</thead>
<tbody>
{% for session in training %}
{% ifchanged %}
{% include "meeting/group_proceedings.html" %}
{% endifchanged %}
{% endfor %}
</tbody>
</table>
{% endwith %}
{% endif %}
<!-- IAB Sessions -->
{% if iab %}
<h2 class="mt-5" id="iab">
IAB <small class="text-muted">Internet Architecture Board</small>
</h2>
<table class="table table-sm table-striped tablesorter">
{% if not_meeting_groups %}
<p>
{{ area.name }} groups not meeting:
{% for entry in not_meeting_groups %}
{% if entry.name == "" %}{# do not show named sessions in this list #}
<a href="{% url 'ietf.group.views.group_home' acronym=entry.group.acronym %}">
{{ entry.group.acronym }}
</a>{% if not forloop.last %},{% endif %}
{% endif %}
{% endfor %}
</p>
<table class="table table-sm table-striped">
<thead>
<tr>
<th scope="col" data-sort="group">
Group
</th>
<th scope="col" data-sort="artifacts">
Artifacts
</th>
<th scope="col" data-sort="recordings">
Recordings
</th>
<th scope="col" data-sort="slides">
Slides
</th>
<th scope="col" data-sort="drafts">
Drafts
</th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for session in iab %}
{% ifchanged %}
{% include "meeting/group_proceedings.html" %}
{% endifchanged %}
{% endfor %}
{% for entry in not_meeting_groups %}{% if entry.sessions_with_materials %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endif %}{% endfor %}
</tbody>
</table>
{% endif %}
<!-- IRTF Sessions -->
{% if irtf %}
<h2 class="mt-5" id="irtf">
IRTF <small class="text-muted">Internet Research Task Force</small>
</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">
Group
</th>
<th scope="col" data-sort="artifacts">
Artifacts
</th>
<th scope="col" data-sort="recordings">
Recordings
</th>
<th scope="col" data-sort="slides">
Slides
</th>
<th scope="col" data-sort="drafts">
Drafts
</th>
</tr>
</thead>
<tbody>
{% for session in irtf|dictsort:"group.acronym" %}
{% ifchanged %}
{% include "meeting/group_proceedings.html" %}
{% endifchanged %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% endwith %}
{% endfor %}
<!-- Training Sessions -->
{% if training %}
<h2 class="mt-5" id="training">Training</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">Group</th>
<th scope="col" data-sort="artifacts">Artifacts</th>
<th scope="col" data-sort="recordings">Recordings</th>
<th scope="col" data-sort="slides">Slides</th>
<th scope="col" data-sort="drafts">Drafts</th>
</tr>
</thead>
<tbody>
{% for entry in training %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=False only %}
{% endfor %}
</tbody>
</table>
{% endif %}
<!-- IAB Sessions -->
{% if iab %}
<h2 class="mt-5" id="iab">
IAB <small class="text-muted">Internet Architecture Board</small>
</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">
Group
</th>
<th scope="col" data-sort="artifacts">
Artifacts
</th>
<th scope="col" data-sort="recordings">
Recordings
</th>
<th scope="col" data-sort="slides">
Slides
</th>
<th scope="col" data-sort="drafts">
Drafts
</th>
</tr>
</thead>
<tbody>
{% for entry in iab %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% endif %}
<!-- IRTF Sessions -->
{% if irtf %}
<h2 class="mt-5" id="irtf">
IRTF <small class="text-muted">Internet Research Task Force</small>
</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">
Group
</th>
<th scope="col" data-sort="artifacts">
Artifacts
</th>
<th scope="col" data-sort="recordings">
Recordings
</th>
<th scope="col" data-sort="slides">
Slides
</th>
<th scope="col" data-sort="drafts">
Drafts
</th>
</tr>
</thead>
<tbody>
{% for entry in irtf %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% endcache %}
{% endblock %}
{% block js %}