Skip to content

Commit

Permalink
fix: Separately show proceedings sessions with different names (#5005)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jennifer-richards authored Jan 20, 2023
1 parent b931c8e commit c6663eb
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 257 deletions.
66 changes: 65 additions & 1 deletion ietf/meeting/tests_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7310,7 +7310,12 @@ def _assertProceedingsMaterialsDisplayed(self, response, meeting):
)

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')
Expand Down Expand Up @@ -7364,6 +7369,65 @@ def test_proceedings(self):
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')
Expand Down
126 changes: 103 additions & 23 deletions ietf/meeting/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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')
)

cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"]
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, 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))
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"]

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,
Expand Down
146 changes: 60 additions & 86 deletions ietf/templates/meeting/group_proceedings.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Loading

0 comments on commit c6663eb

Please sign in to comment.