diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 9d7fc8f28..38a051b91 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -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') diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index b90f2506a..9415be9bc 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -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, diff --git a/ietf/templates/meeting/group_proceedings.html b/ietf/templates/meeting/group_proceedings.html index 99722fca4..1e0bcfe06 100644 --- a/ietf/templates/meeting/group_proceedings.html +++ b/ietf/templates/meeting/group_proceedings.html @@ -5,111 +5,85 @@ {% load proceedings_filters %} - {% if session.name %} -
{{ session.name }}
- {% else %} -
- {{ session.group.acronym }} + {% if entry.name %} +
{{ entry.name }}
+ {% elif entry.group.acronym %} +
+ {{ entry.group.acronym }}
- {% if session.group.state_id == "bof" %}BOF{% endif %} + {% if entry.group.state_id == "bof" %}BOF{% endif %} + {% else %} +

{{ entry.group }}

{% endif %} - {% if session.all_meeting_sessions_cancelled %} + {% if entry.canceled %} Session cancelled {% else %} + {# artifacts #} - {% if session.all_meeting_agendas %} - {% if session.all_meeting_agendas|length == 1 %} - Agenda -
- {% else %} - {% for agenda in session.all_meeting_agendas %} - - Agenda {{ agenda.sessionpresentation_set.first.session.official_timeslotassignment.timeslot.time|date:"D G:i" }} - -
- {% endfor %} - {% endif %} - {% else %} - {% if show_agenda == "True" and not meeting.proceedings_final %} + {% for agenda in entry.agendas %} + + Agenda + {% if agenda.time %}{{agenda.time|date:"D G:i"}}{% endif %} + +
+ {% empty %} + {% if show_agenda and not meeting.proceedings_final %} No agenda
{% endif %} - {% endif %} - {% if session.all_meeting_minutes %} - {% if session.all_meeting_minutes|length == 1 %} - Minutes -
- {% else %} - {% for minutes in session.all_meeting_minutes %} - - Minutes {{ minutes.sessionpresentation_set.first.session.official_timeslotassignment.timeslot.time|date:"D G:i" }} - -
- {% endfor %} - {% endif %} - {% else %} - {% if show_agenda == "True" and not meeting.proceedings_final %} + {% endfor %} + {% for minutes in entry.minutes %} + + Minutes + {% if minutes.time %}{{minutes.time|date:"D G:i"}}{% endif %} + +
+ {% empty %} + {% if show_agenda and not meeting.proceedings_final %} No minutes
{% endif %} - {% endif %} - {% if session.all_meeting_bluesheets %} - {% if session.all_meeting_bluesheets|length == 1 %} - Bluesheets -
- {% else %} - {% for bs in session.all_meeting_bluesheets %} - - Bluesheets {{ bs.sessionpresentation_set.first.session.official_timeslotassignment.timeslot.time|date:"D G:i" }} - -
- {% endfor %} - {% endif %} - {% endif %} - {% with session.group|status_for_meeting:meeting as status %} - {% if status %} - - Status - -
- {% endif %} - {% endwith %} + {% endfor %} + {% for bs in entry.bluesheets %} + + Bluesheets + {% if bs.time %}{{ bs.time|date:"D G:i" }}{% endif %} + +
+ {% endfor %} + {# recordings #} - {% if session.all_meeting_sessions_for_group|length == 1 %} - {% for rec in session.all_meeting_recordings %} - {{ rec|hack_recording_title:False }} -
- {% endfor %} - {% else %} - {% for rec in session.all_meeting_recordings %} - {{ rec|hack_recording_title:True }} -
- {% endfor %} - {% endif %} + {% for rec in entry.recordings %} + + {{ rec.material|hack_recording_title }} + {% if rec.time %}{{ rec.time|date:"D G:i"}}{% endif %} + +
+ {% endfor %} + {# slides #} - {% with session.all_meeting_slides as slides %} - {% for slide in slides %} - {{ slide.title|clean_whitespace }} -
- {% empty %} - {% if not meeting.proceedings_final %}No slides{% endif %} - {% endfor %} - {% endwith %} + {% for slide in entry.slides %} + {{ slide.material.title|clean_whitespace }} +
+ {% empty %} + {% if not meeting.proceedings_final %}No slides{% endif %} + {% endfor %} + {# drafts #} - {% with session.all_meeting_drafts as drafts %} - {% for draft in drafts %} - {{ draft.canonical_name }} -
- {% empty %} - {% if not meeting.proceedings_final %}No drafts{% endif %} - {% endfor %} - {% endwith %} + {% for draft in entry.drafts %} + + {{ draft.material.canonical_name }} + +
+ {% empty %} + {% if not meeting.proceedings_final %}No drafts{% endif %} + {% endfor %} {% endif %} - \ No newline at end of file + diff --git a/ietf/templates/meeting/proceedings.html b/ietf/templates/meeting/proceedings.html index 687c5dac1..6f3f2cda4 100644 --- a/ietf/templates/meeting/proceedings.html +++ b/ietf/templates/meeting/proceedings.html @@ -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 %} - - {% if plenaries %} -

Plenaries

+ + {% if plenaries %} +

Plenaries

+ + + + + + + + + + + + {% for entry in plenaries %} + {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %} + {% endfor %} + +
GroupArtifactsRecordingsSlidesDrafts
+ {% endif %} + + {% for area, meeting_groups, not_meeting_groups in ietf_areas %} +

+ {{ area.acronym|upper }} {{ area.name }} +

+ {% if meeting_groups %} @@ -38,161 +60,125 @@ - {% 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 %}
{% endif %} - - {% for area, meeting_sessions, not_meeting_sessions in ietf_areas %} -

- {{ area.acronym|upper }} {{ area.name }} -

- {% if meeting_sessions %} - - - - - - - - - - - - {% for session in meeting_sessions %} - {% ifchanged session.group.acronym %} - {% include "meeting/group_proceedings.html" %} - {% endifchanged %} - {% endfor %} - -
GroupArtifactsRecordingsSlidesDrafts
- {% endif %} - {% if not_meeting_sessions %} -

- {{ area.name }} groups not meeting: - {% for session in not_meeting_sessions %} - {% ifchanged session.group.acronym %} - {{ session.group.acronym }}{% if not forloop.last %},{% endif %} - {% endifchanged %} - {% endfor %} -

- - - - - - - - - - - - {% for session in not_meeting_sessions %} - {% ifchanged session.group.acronym %} - {% if session.sessionpresentation_set.exists %} - {% include "meeting/group_proceedings.html" %} - {% endif %} - {% endifchanged %} - {% endfor %} - -
- {% endif %} - {% endfor %} - - {% if training %} - {% with "False" as show_agenda %} -

Training

- - - - - - - - - - - - {% for session in training %} - {% ifchanged %} - {% include "meeting/group_proceedings.html" %} - {% endifchanged %} - {% endfor %} - -
GroupArtifactsRecordingsSlidesDrafts
- {% endwith %} - {% endif %} - - {% if iab %} -

- IAB Internet Architecture Board -

- + {% if not_meeting_groups %} +

+ {{ area.name }} groups not meeting: + {% for entry in not_meeting_groups %} + {% if entry.name == "" %}{# do not show named sessions in this list #} + + {{ entry.group.acronym }} + {% if not forloop.last %},{% endif %} + {% endif %} + {% endfor %} +

+
- - - - - + + + + + - {% 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 %}
- Group - - Artifacts - - Recordings - - Slides - - Drafts -
{% endif %} - - {% if irtf %} -

- IRTF Internet Research Task Force -

- - - - - - - - - - - - {% for session in irtf|dictsort:"group.acronym" %} - {% ifchanged %} - {% include "meeting/group_proceedings.html" %} - {% endifchanged %} - {% endfor %} - -
- Group - - Artifacts - - Recordings - - Slides - - Drafts -
- {% endif %} - {% endwith %} + {% endfor %} + + {% if training %} +

Training

+ + + + + + + + + + + + {% for entry in training %} + {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=False only %} + {% endfor %} + +
GroupArtifactsRecordingsSlidesDrafts
+ {% endif %} + + {% if iab %} +

+ IAB Internet Architecture Board +

+ + + + + + + + + + + + {% for entry in iab %} + {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %} + {% endfor %} + +
+ Group + + Artifacts + + Recordings + + Slides + + Drafts +
+ {% endif %} + + {% if irtf %} +

+ IRTF Internet Research Task Force +

+ + + + + + + + + + + + {% for entry in irtf %} + {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %} + {% endfor %} + +
+ Group + + Artifacts + + Recordings + + Slides + + Drafts +
+ {% endif %} {% endcache %} {% endblock %} {% block js %}