feat: keep proceedings cache up to date via celery (#8449)
* refactor: better control proceedings caching * refactor: move methods from views to utils * chore: revert accidental settings change * fix: eliminate circular import get_schedule() with name=None should perhaps be an anti-pattern * feat: task to recompute proceedings daily * chore: proceedings cache lifetime = 1 day * fix: ensure finalization is immediately reflected * chore: update beat comments in docker-compose * style: undo a couple whitespace changes * test: update / refactor tests * test: test task * refactor: disallow positional arg to task * refactor: add trivial test of old task
This commit is contained in:
parent
1fbedd7df1
commit
060320d766
|
@ -101,6 +101,7 @@ services:
|
|||
# stop_grace_period: 1m
|
||||
# volumes:
|
||||
# - .:/workspace
|
||||
# - app-assets:/assets
|
||||
|
||||
volumes:
|
||||
postgresdb-data:
|
||||
|
|
|
@ -3,10 +3,42 @@
|
|||
# Celery task definitions
|
||||
#
|
||||
from celery import shared_task
|
||||
from django.utils import timezone
|
||||
|
||||
from ietf.utils import log
|
||||
from .models import Meeting
|
||||
from .utils import generate_proceedings_content
|
||||
from .views import generate_agenda_data
|
||||
|
||||
|
||||
@shared_task
|
||||
def agenda_data_refresh():
|
||||
generate_agenda_data(force_refresh=True)
|
||||
|
||||
|
||||
@shared_task
|
||||
def proceedings_content_refresh_task(*, all=False):
|
||||
"""Refresh meeting proceedings cache
|
||||
|
||||
If `all` is `False`, then refreshes the cache for meetings whose numbers modulo
|
||||
24 equal the current hour number (0-23). Scheduling the task once per hour will
|
||||
then result in all proceedings being recomputed daily, with no more than two per
|
||||
hour (now) or a few per hour in the next decade. That keeps the computation time
|
||||
to under a couple minutes on our current production system.
|
||||
|
||||
If `all` is True, refreshes all meetings
|
||||
"""
|
||||
now = timezone.now()
|
||||
|
||||
for meeting in Meeting.objects.filter(type_id="ietf").order_by("number"):
|
||||
if meeting.proceedings_format_version == 1:
|
||||
continue # skip v1 proceedings, they're stored externally
|
||||
num = meeting.get_number() # convert str -> int
|
||||
if num is None:
|
||||
log.log(
|
||||
f"Not refreshing proceedings for meeting {meeting.number}: "
|
||||
f"type is 'ietf' but get_number() returned None"
|
||||
)
|
||||
elif all or (num % 24 == now.hour):
|
||||
log.log(f"Refreshing proceedings for meeting {meeting.number}...")
|
||||
generate_proceedings_content(meeting, force_refresh=True)
|
||||
|
|
51
ietf/meeting/tests_tasks.py
Normal file
51
ietf/meeting/tests_tasks.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
# Copyright The IETF Trust 2025, All Rights Reserved
|
||||
|
||||
import datetime
|
||||
from mock import patch, call
|
||||
from ietf.utils.test_utils import TestCase
|
||||
from .factories import MeetingFactory
|
||||
from .tasks import proceedings_content_refresh_task, agenda_data_refresh
|
||||
|
||||
|
||||
class TaskTests(TestCase):
|
||||
@patch("ietf.meeting.tasks.generate_agenda_data")
|
||||
def test_agenda_data_refresh(self, mock_generate):
|
||||
agenda_data_refresh()
|
||||
self.assertTrue(mock_generate.called)
|
||||
self.assertEqual(mock_generate.call_args, call(force_refresh=True))
|
||||
|
||||
@patch("ietf.meeting.tasks.generate_proceedings_content")
|
||||
def test_proceedings_content_refresh_task(self, mock_generate):
|
||||
# Generate a couple of meetings
|
||||
meeting120 = MeetingFactory(type_id="ietf", number="120") # 24 * 5
|
||||
meeting127 = MeetingFactory(type_id="ietf", number="127") # 24 * 5 + 7
|
||||
|
||||
# Times to be returned
|
||||
now_utc = datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
hour_00_utc = now_utc.replace(hour=0)
|
||||
hour_01_utc = now_utc.replace(hour=1)
|
||||
hour_07_utc = now_utc.replace(hour=7)
|
||||
|
||||
# hour 00 - should call meeting with number % 24 == 0
|
||||
with patch("ietf.meeting.tasks.timezone.now", return_value=hour_00_utc):
|
||||
proceedings_content_refresh_task()
|
||||
self.assertEqual(mock_generate.call_count, 1)
|
||||
self.assertEqual(mock_generate.call_args, call(meeting120, force_refresh=True))
|
||||
mock_generate.reset_mock()
|
||||
|
||||
# hour 01 - should call no meetings
|
||||
with patch("ietf.meeting.tasks.timezone.now", return_value=hour_01_utc):
|
||||
proceedings_content_refresh_task()
|
||||
self.assertEqual(mock_generate.call_count, 0)
|
||||
|
||||
# hour 07 - should call meeting with number % 24 == 0
|
||||
with patch("ietf.meeting.tasks.timezone.now", return_value=hour_07_utc):
|
||||
proceedings_content_refresh_task()
|
||||
self.assertEqual(mock_generate.call_count, 1)
|
||||
self.assertEqual(mock_generate.call_args, call(meeting127, force_refresh=True))
|
||||
mock_generate.reset_mock()
|
||||
|
||||
# With all=True, all should be called regardless of time. Reuse hour_01_utc which called none before
|
||||
with patch("ietf.meeting.tasks.timezone.now", return_value=hour_01_utc):
|
||||
proceedings_content_refresh_task(all=True)
|
||||
self.assertEqual(mock_generate.call_count, 2)
|
|
@ -32,6 +32,7 @@ from django.db.models import F, Max
|
|||
from django.http import QueryDict, FileResponse
|
||||
from django.template import Context, Template
|
||||
from django.utils import timezone
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import slugify
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
@ -46,7 +47,7 @@ from ietf.meeting.helpers import send_interim_meeting_cancellation_notice, send_
|
|||
from ietf.meeting.helpers import send_interim_minutes_reminder, populate_important_dates, update_important_dates
|
||||
from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission, SchedulingEvent, Room, Constraint, ConstraintName
|
||||
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting, make_interim_test_data
|
||||
from ietf.meeting.utils import condition_slide_order
|
||||
from ietf.meeting.utils import condition_slide_order, generate_proceedings_content
|
||||
from ietf.meeting.utils import add_event_info_to_session_qs, participants_for_meeting
|
||||
from ietf.meeting.utils import create_recording, delete_recording, get_next_sequence, bluesheet_data
|
||||
from ietf.meeting.views import session_draft_list, parse_agenda_filter_params, sessions_post_save, agenda_extract_schedule
|
||||
|
@ -8296,8 +8297,7 @@ class ProceedingsTests(BaseMeetingTestCase):
|
|||
path = Path(settings.BASE_DIR) / 'meeting/test_procmat.pdf'
|
||||
return path.open('rb')
|
||||
|
||||
def _assertMeetingHostsDisplayed(self, response, meeting):
|
||||
pq = PyQuery(response.content)
|
||||
def _assertMeetingHostsDisplayed(self, pq: PyQuery, meeting):
|
||||
host_divs = pq('div.host-logo')
|
||||
self.assertEqual(len(host_divs), meeting.meetinghosts.count(), 'Should have a logo for every meeting host')
|
||||
self.assertEqual(
|
||||
|
@ -8313,12 +8313,11 @@ class ProceedingsTests(BaseMeetingTestCase):
|
|||
'Correct image and name for each host should appear in the correct order'
|
||||
)
|
||||
|
||||
def _assertProceedingsMaterialsDisplayed(self, response, meeting):
|
||||
def _assertProceedingsMaterialsDisplayed(self, pq: PyQuery, meeting):
|
||||
"""Checks that all (and only) active materials are linked with correct href and title"""
|
||||
expected_materials = [
|
||||
m for m in meeting.proceedings_materials.order_by('type__order') if m.active()
|
||||
]
|
||||
pq = PyQuery(response.content)
|
||||
links = pq('div.proceedings-material a')
|
||||
self.assertEqual(len(links), len(expected_materials), 'Should have an entry for each active ProceedingsMaterial')
|
||||
self.assertEqual(
|
||||
|
@ -8327,9 +8326,8 @@ class ProceedingsTests(BaseMeetingTestCase):
|
|||
'Correct title and link for each ProceedingsMaterial should appear in the correct order'
|
||||
)
|
||||
|
||||
def _assertGroupSessions(self, response, meeting):
|
||||
def _assertGroupSessions(self, pq: PyQuery):
|
||||
"""Checks that group/sessions are present"""
|
||||
pq = PyQuery(response.content)
|
||||
sections = ["plenaries", "gen", "iab", "editorial", "irtf", "training"]
|
||||
for section in sections:
|
||||
self.assertEqual(len(pq(f"#{section}")), 1, f"{section} section should exists in proceedings")
|
||||
|
@ -8337,10 +8335,9 @@ class ProceedingsTests(BaseMeetingTestCase):
|
|||
def test_proceedings(self):
|
||||
"""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.
|
||||
Proceedings contents are tested in detail when testing generate_proceedings_content.
|
||||
"""
|
||||
# number must be >97 (settings.PROCEEDINGS_VERSION_CHANGES)
|
||||
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')
|
||||
|
@ -8365,16 +8362,72 @@ class ProceedingsTests(BaseMeetingTestCase):
|
|||
self._create_proceedings_materials(meeting)
|
||||
|
||||
url = urlreverse("ietf.meeting.views.proceedings", kwargs=dict(num=meeting.number))
|
||||
r = self.client.get(url)
|
||||
cached_content = mark_safe("<p>Fake proceedings content</p>")
|
||||
with patch("ietf.meeting.views.generate_proceedings_content") as mock_gpc:
|
||||
mock_gpc.return_value = cached_content
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(cached_content, r.content.decode())
|
||||
self.assertTemplateUsed(r, "meeting/proceedings_wrapper.html")
|
||||
self.assertTemplateNotUsed(r, "meeting/proceedings.html")
|
||||
|
||||
# These are rendered in proceedings_wrapper.html, so test them here
|
||||
if len(meeting.city) > 0:
|
||||
self.assertContains(r, meeting.city)
|
||||
if len(meeting.venue_name) > 0:
|
||||
self.assertContains(r, meeting.venue_name)
|
||||
self._assertMeetingHostsDisplayed(PyQuery(r.content), meeting)
|
||||
|
||||
@patch("ietf.meeting.utils.caches")
|
||||
def test_generate_proceedings_content(self, mock_caches):
|
||||
# number must be >97 (settings.PROCEEDINGS_VERSION_CHANGES)
|
||||
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
|
||||
|
||||
# First, check that by default a value in the cache is used without doing any other computation
|
||||
mock_default_cache = mock_caches["default"]
|
||||
mock_default_cache.get.return_value = "a cached value"
|
||||
result = generate_proceedings_content(meeting)
|
||||
self.assertEqual(result, "a cached value")
|
||||
self.assertFalse(mock_default_cache.set.called)
|
||||
self.assertTrue(mock_default_cache.get.called)
|
||||
cache_key = mock_default_cache.get.call_args.args[0]
|
||||
mock_default_cache.get.reset_mock()
|
||||
|
||||
# Now set up for actual computation of the proceedings content.
|
||||
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
|
||||
GroupEventFactory(group=session.group,type='status_update')
|
||||
SessionPresentationFactory(document__type_id='recording',session=session)
|
||||
SessionPresentationFactory(document__type_id='recording',session=session,document__title="Audio recording for tests")
|
||||
|
||||
# Add various group sessions
|
||||
groups = []
|
||||
parent_groups = [
|
||||
GroupFactory.create(type_id="area", acronym="gen"),
|
||||
GroupFactory.create(acronym="iab"),
|
||||
GroupFactory.create(acronym="irtf"),
|
||||
]
|
||||
for parent in parent_groups:
|
||||
groups.append(GroupFactory.create(parent=parent))
|
||||
for acronym in ["rsab", "edu"]:
|
||||
groups.append(GroupFactory.create(acronym=acronym))
|
||||
for group in groups:
|
||||
SessionFactory(meeting=meeting, group=group)
|
||||
|
||||
self.write_materials_files(meeting, session)
|
||||
self._create_proceedings_materials(meeting)
|
||||
|
||||
# Now "empty" the mock cache and see that we compute the expected proceedings content.
|
||||
mock_default_cache.get.return_value = None
|
||||
proceedings_content = generate_proceedings_content(meeting)
|
||||
self.assertTrue(mock_default_cache.get.called)
|
||||
self.assertEqual(mock_default_cache.get.call_args.args[0], cache_key, "same cache key each time")
|
||||
self.assertTrue(mock_default_cache.set.called)
|
||||
self.assertEqual(mock_default_cache.set.call_args, call(cache_key, proceedings_content, timeout=86400))
|
||||
mock_default_cache.get.reset_mock()
|
||||
mock_default_cache.set.reset_mock()
|
||||
|
||||
# standard items on every proceedings
|
||||
pq = PyQuery(r.content)
|
||||
pq = PyQuery(proceedings_content)
|
||||
self.assertNotEqual(
|
||||
pq('a[href="{}"]'.format(
|
||||
urlreverse('ietf.meeting.views.proceedings_overview', kwargs=dict(num=meeting.number)))
|
||||
|
@ -8405,9 +8458,17 @@ class ProceedingsTests(BaseMeetingTestCase):
|
|||
)
|
||||
|
||||
# configurable contents
|
||||
self._assertMeetingHostsDisplayed(r, meeting)
|
||||
self._assertProceedingsMaterialsDisplayed(r, meeting)
|
||||
self._assertGroupSessions(r, meeting)
|
||||
self._assertProceedingsMaterialsDisplayed(pq, meeting)
|
||||
self._assertGroupSessions(pq)
|
||||
|
||||
# Finally, repeat the first cache test, but now with force_refresh=True. The cached value
|
||||
# should be ignored and we should recompute the proceedings as before.
|
||||
mock_default_cache.get.return_value = "a cached value"
|
||||
result = generate_proceedings_content(meeting, force_refresh=True)
|
||||
self.assertEqual(result, proceedings_content) # should have recomputed the same thing
|
||||
self.assertFalse(mock_default_cache.get.called, "don't bother reading cache when force_refresh is True")
|
||||
self.assertTrue(mock_default_cache.set.called)
|
||||
self.assertEqual(mock_default_cache.set.call_args, call(cache_key, proceedings_content, timeout=86400))
|
||||
|
||||
def test_named_session(self):
|
||||
"""Session with a name should appear separately in the proceedings"""
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import datetime
|
||||
import itertools
|
||||
import os
|
||||
from hashlib import sha384
|
||||
|
||||
import pytz
|
||||
import subprocess
|
||||
|
||||
|
@ -11,8 +13,9 @@ from pathlib import Path
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.cache import caches
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.models import OuterRef, Subquery, TextField, Q, Value
|
||||
from django.db.models import OuterRef, Subquery, TextField, Q, Value, Max
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone
|
||||
|
@ -995,3 +998,169 @@ def participants_for_meeting(meeting):
|
|||
sessions = meeting.session_set.filter(Q(type='plenary') | Q(group__type__in=['wg', 'rg']))
|
||||
attended = Attended.objects.filter(session__in=sessions).values_list('person', flat=True).distinct()
|
||||
return (checked_in, attended)
|
||||
|
||||
|
||||
def generate_proceedings_content(meeting, force_refresh=False):
|
||||
"""Render proceedings content for a meeting and update cache
|
||||
|
||||
:meeting: meeting whose proceedings should be rendered
|
||||
:force_refresh: true to force regeneration and cache refresh
|
||||
"""
|
||||
cache = caches["default"]
|
||||
cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"]
|
||||
# Include proceedings_final in the bare_key so we'll always reflect that accurately, even at the cost of
|
||||
# a recomputation in the view
|
||||
bare_key = f"proceedings.{meeting.number}.{cache_version}.final={meeting.proceedings_final}"
|
||||
cache_key = sha384(bare_key.encode("utf8")).hexdigest()
|
||||
if not force_refresh:
|
||||
cached_content = cache.get(cache_key, None)
|
||||
if cached_content is not None:
|
||||
return cached_content
|
||||
|
||||
def area_and_group_acronyms_from_session(s):
|
||||
area = s.group_parent_at_the_time()
|
||||
if area == None:
|
||||
area = s.group.parent
|
||||
group = s.group_at_the_time()
|
||||
return (area.acronym, group.acronym)
|
||||
|
||||
schedule = meeting.schedule
|
||||
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_meeting, irtf_not_meeting = organize_proceedings_sessions(
|
||||
sessions.filter(group__parent__acronym = 'irtf').order_by('group__acronym')
|
||||
)
|
||||
# per Colin (datatracker #5010) - don't report not meeting rags
|
||||
irtf_not_meeting = [item for item in irtf_not_meeting if item["group"].type_id != "rag"]
|
||||
irtf = {"meeting_groups":irtf_meeting, "not_meeting_groups":irtf_not_meeting}
|
||||
|
||||
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')
|
||||
)
|
||||
editorial, _ = organize_proceedings_sessions(
|
||||
sessions.filter(group__acronym__in=['rsab','rswg'])
|
||||
.exclude(current_status='notmeet')
|
||||
)
|
||||
|
||||
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym__in=['edu','iepg','tools'])
|
||||
ietf = list(ietf)
|
||||
ietf.sort(key=lambda s: area_and_group_acronyms_from_session(s))
|
||||
ietf_areas = []
|
||||
for area, area_sessions in itertools.groupby(ietf, key=lambda s: s.group_parent_at_the_time()):
|
||||
meeting_groups, not_meeting_groups = organize_proceedings_sessions(area_sessions)
|
||||
ietf_areas.append((area, meeting_groups, not_meeting_groups))
|
||||
|
||||
with timezone.override(meeting.tz()):
|
||||
rendered_content = render_to_string(
|
||||
"meeting/proceedings.html",
|
||||
{
|
||||
'meeting': meeting,
|
||||
'plenaries': plenaries,
|
||||
'training': training,
|
||||
'irtf': irtf,
|
||||
'iab': iab,
|
||||
'editorial': editorial,
|
||||
'ietf_areas': ietf_areas,
|
||||
'meetinghost_logo': {
|
||||
'max_height': settings.MEETINGHOST_LOGO_MAX_DISPLAY_HEIGHT,
|
||||
'max_width': settings.MEETINGHOST_LOGO_MAX_DISPLAY_WIDTH,
|
||||
}
|
||||
},
|
||||
)
|
||||
cache.set(
|
||||
cache_key,
|
||||
rendered_content,
|
||||
timeout=86400, # one day, in seconds
|
||||
)
|
||||
return rendered_content
|
||||
|
||||
|
||||
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.presentations.exists():
|
||||
by_name[s.name].append(s) # for notmeet, only include sessions with materials
|
||||
for sess_name, ss in by_name.items():
|
||||
session = ss[0] if ss else None
|
||||
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:
|
||||
tsa = s.official_timeslotassignment()
|
||||
timestamp = tsa.timeslot.time if tsa else None
|
||||
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,
|
||||
'session': session,
|
||||
'canceled': all_canceled,
|
||||
'has_materials': s.presentations.exists(),
|
||||
'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),
|
||||
'meetecho_recordings': _format_materials((s, [s.session_recording_url()]) for s in ss),
|
||||
'chatlogs': _format_materials((s, s.chatlogs()) for s in ss),
|
||||
'slides': _format_materials((s, s.slides()) for s in ss),
|
||||
'drafts': _format_materials((s, s.drafts()) for s in ss),
|
||||
'last_update': session.last_update if hasattr(session, 'last_update') else None
|
||||
}
|
||||
if session and session.meeting.type_id == 'ietf' and not session.meeting.proceedings_final:
|
||||
entry['attendances'] = _format_materials((s, s) for s in ss if Attended.objects.filter(session=s).exists())
|
||||
if is_meeting:
|
||||
meeting_groups.append(entry)
|
||||
else:
|
||||
not_meeting_groups.append(entry)
|
||||
return meeting_groups, not_meeting_groups
|
||||
|
|
|
@ -75,7 +75,13 @@ from ietf.meeting.helpers import send_interim_meeting_cancellation_notice, send_
|
|||
from ietf.meeting.helpers import send_interim_approval
|
||||
from ietf.meeting.helpers import send_interim_approval_request
|
||||
from ietf.meeting.helpers import send_interim_announcement_request, sessions_post_cancel
|
||||
from ietf.meeting.utils import finalize, sort_accept_tuple, condition_slide_order
|
||||
from ietf.meeting.utils import (
|
||||
condition_slide_order,
|
||||
finalize,
|
||||
generate_proceedings_content,
|
||||
organize_proceedings_sessions,
|
||||
sort_accept_tuple,
|
||||
)
|
||||
from ietf.meeting.utils import add_event_info_to_session_qs
|
||||
from ietf.meeting.utils import session_time_for_sorting
|
||||
from ietf.meeting.utils import session_requested_by, SaveMaterialsError
|
||||
|
@ -4128,93 +4134,10 @@ 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.presentations.exists():
|
||||
by_name[s.name].append(s) # for notmeet, only include sessions with materials
|
||||
for sess_name, ss in by_name.items():
|
||||
session = ss[0] if ss else None
|
||||
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:
|
||||
tsa = s.official_timeslotassignment()
|
||||
timestamp = tsa.timeslot.time if tsa else None
|
||||
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,
|
||||
'session': session,
|
||||
'canceled': all_canceled,
|
||||
'has_materials': s.presentations.exists(),
|
||||
'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),
|
||||
'meetecho_recordings': _format_materials((s, [s.session_recording_url()]) for s in ss),
|
||||
'chatlogs': _format_materials((s, s.chatlogs()) for s in ss),
|
||||
'slides': _format_materials((s, s.slides()) for s in ss),
|
||||
'drafts': _format_materials((s, s.drafts()) for s in ss),
|
||||
'last_update': session.last_update if hasattr(session, 'last_update') else None
|
||||
}
|
||||
if session and session.meeting.type_id == 'ietf' and not session.meeting.proceedings_final:
|
||||
entry['attendances'] = _format_materials((s, s) for s in ss if Attended.objects.filter(session=s).exists())
|
||||
if is_meeting:
|
||||
meeting_groups.append(entry)
|
||||
else:
|
||||
not_meeting_groups.append(entry)
|
||||
return meeting_groups, not_meeting_groups
|
||||
|
||||
|
||||
def proceedings(request, num=None):
|
||||
|
||||
def area_and_group_acronyms_from_session(s):
|
||||
area = s.group_parent_at_the_time()
|
||||
if area == None:
|
||||
area = s.group.parent
|
||||
group = s.group_at_the_time()
|
||||
return (area.acronym, group.acronym)
|
||||
|
||||
meeting = get_meeting(num)
|
||||
|
||||
|
||||
# Early proceedings were hosted on www.ietf.org rather than the datatracker
|
||||
if meeting.proceedings_format_version == 1:
|
||||
return HttpResponseRedirect(settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting))
|
||||
|
@ -4225,72 +4148,12 @@ def proceedings(request, num=None):
|
|||
kwargs['num'] = num
|
||||
return redirect('ietf.meeting.views.materials', **kwargs)
|
||||
|
||||
begin_date = meeting.get_submission_start_date()
|
||||
cut_off_date = meeting.get_submission_cut_off_date()
|
||||
cor_cut_off_date = meeting.get_submission_correction_date()
|
||||
today_utc = date_today(datetime.timezone.utc)
|
||||
|
||||
schedule = get_schedule(meeting, None)
|
||||
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_meeting, irtf_not_meeting = organize_proceedings_sessions(
|
||||
sessions.filter(group__parent__acronym = 'irtf').order_by('group__acronym')
|
||||
)
|
||||
# per Colin (datatracker #5010) - don't report not meeting rags
|
||||
irtf_not_meeting = [item for item in irtf_not_meeting if item["group"].type_id != "rag"]
|
||||
irtf = {"meeting_groups":irtf_meeting, "not_meeting_groups":irtf_not_meeting}
|
||||
|
||||
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')
|
||||
)
|
||||
editorial, _ = organize_proceedings_sessions(
|
||||
sessions.filter(group__acronym__in=['rsab','rswg'])
|
||||
.exclude(current_status='notmeet')
|
||||
)
|
||||
|
||||
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym__in=['edu','iepg','tools'])
|
||||
ietf = list(ietf)
|
||||
ietf.sort(key=lambda s: area_and_group_acronyms_from_session(s))
|
||||
ietf_areas = []
|
||||
for area, area_sessions in itertools.groupby(ietf, key=lambda s: s.group_parent_at_the_time()):
|
||||
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", {
|
||||
return render(request, "meeting/proceedings_wrapper.html", {
|
||||
'meeting': meeting,
|
||||
'plenaries': plenaries,
|
||||
'training': training,
|
||||
'irtf': irtf,
|
||||
'iab': iab,
|
||||
'editorial': editorial,
|
||||
'ietf_areas': ietf_areas,
|
||||
'cut_off_date': cut_off_date,
|
||||
'cor_cut_off_date': cor_cut_off_date,
|
||||
'submission_started': today_utc > begin_date,
|
||||
'cache_version': cache_version,
|
||||
'attendance': meeting.get_attendance(),
|
||||
'meetinghost_logo': {
|
||||
'max_height': settings.MEETINGHOST_LOGO_MAX_DISPLAY_HEIGHT,
|
||||
'max_width': settings.MEETINGHOST_LOGO_MAX_DISPLAY_WIDTH,
|
||||
}
|
||||
'proceedings_content': generate_proceedings_content(meeting),
|
||||
})
|
||||
|
||||
@role_required('Secretariat')
|
||||
|
|
|
@ -1,184 +1,160 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load ietf_filters static %}
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
|
||||
{% endblock %}
|
||||
{% block title %}
|
||||
IETF {{ meeting.number }}
|
||||
{% if not meeting.proceedings_final %}Draft{% endif %}
|
||||
Proceedings
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
{% include 'meeting/proceedings/title.html' with meeting=meeting attendance=attendance only %}
|
||||
{% if user|has_role:"Secretariat" and not meeting.proceedings_final %}
|
||||
<a class="btn btn-warning finalize-button"
|
||||
href="{% url 'ietf.meeting.views.finalize_proceedings' num=meeting.number %}">
|
||||
Finalize proceedings
|
||||
</a>
|
||||
{% endif %}
|
||||
{# cache for 15 minutes, as long as there's no proceedings activity. takes 4-8 seconds to generate. #}
|
||||
{% load cache %}
|
||||
{% cache 900 ietf_meeting_proceedings meeting.number cache_version %}
|
||||
{% include 'meeting/proceedings/introduction.html' with meeting=meeting only %}
|
||||
<!-- Plenaries -->
|
||||
{% if plenaries %}
|
||||
<h2 class="mt-5" id="plenaries">Plenaries</h2>
|
||||
{% include 'meeting/proceedings/introduction.html' with meeting=meeting only %}
|
||||
<!-- 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">Internet-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-body-secondary">{{ area.name }}</small>
|
||||
</h2>
|
||||
{% if meeting_groups %}
|
||||
<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">Internet-Drafts</th>
|
||||
</tr>
|
||||
<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">Internet-Drafts</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in plenaries %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
|
||||
{% endfor %}
|
||||
{% 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_groups, not_meeting_groups in ietf_areas %}
|
||||
<h2 class="mt-5" id="{{ area.acronym }}">
|
||||
{{ area.acronym|upper }} <small class="text-body-secondary">{{ area.name }}</small>
|
||||
</h2>
|
||||
{% if meeting_groups %}
|
||||
<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">Internet-Drafts</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in meeting_groups %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% 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"></th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in not_meeting_groups %}{% if entry.has_materials %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
|
||||
{% endif %}{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- Training Sessions -->
|
||||
{% if training %}
|
||||
<h2 class="mt-5" id="training">Training</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">Internet-Drafts</th>
|
||||
</tr>
|
||||
<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 entry in training %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=False only %}
|
||||
{% endfor %}
|
||||
{% for entry in not_meeting_groups %}{% if entry.has_materials %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
|
||||
{% endif %}{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<!-- IAB Sessions -->
|
||||
{% if iab %}
|
||||
<h2 class="mt-5" id="iab">
|
||||
IAB <small class="text-body-secondary">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">
|
||||
Internet-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.meeting_groups %}
|
||||
<h2 class="mt-5" id="irtf">
|
||||
IRTF <small class="text-body-secondary">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">
|
||||
Internet-Drafts
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in irtf.meeting_groups %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if irtf.not_meeting_groups %}
|
||||
{% 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">Internet-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-body-secondary">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">
|
||||
Internet-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.meeting_groups %}
|
||||
<h2 class="mt-5" id="irtf">
|
||||
IRTF <small class="text-body-secondary">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">
|
||||
Internet-Drafts
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in irtf.meeting_groups %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if irtf.not_meeting_groups %}
|
||||
<p>
|
||||
IRTF groups not meeting:
|
||||
{% for entry in irtf.not_meeting_groups %}
|
||||
|
@ -191,18 +167,18 @@
|
|||
</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>
|
||||
<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 entry in irtf.not_meeting %}{% if entry.has_materials %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
|
||||
{% endif %}{% endfor %}
|
||||
{% for entry in irtf.not_meeting %}{% if entry.has_materials %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
|
||||
{% endif %}{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
@ -211,35 +187,29 @@
|
|||
<h2 class="mt-5" id="editorial">Editorial Stream</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">
|
||||
Internet-Drafts
|
||||
</th>
|
||||
</tr>
|
||||
<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">
|
||||
Internet-Drafts
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in editorial %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
|
||||
{% endfor %}
|
||||
{% for entry in editorial %}
|
||||
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endcache %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script src="{% static "ietf/js/list.js" %}">
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
|
27
ietf/templates/meeting/proceedings_wrapper.html
Normal file
27
ietf/templates/meeting/proceedings_wrapper.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load ietf_filters static %}
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
|
||||
{% endblock %}
|
||||
{% block title %}
|
||||
IETF {{ meeting.number }}
|
||||
{% if not meeting.proceedings_final %}Draft{% endif %}
|
||||
Proceedings
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
{% include 'meeting/proceedings/title.html' with meeting=meeting attendance=attendance only %}
|
||||
{% if user|has_role:"Secretariat" and not meeting.proceedings_final %}
|
||||
<a class="btn btn-warning finalize-button"
|
||||
href="{% url 'ietf.meeting.views.finalize_proceedings' num=meeting.number %}">
|
||||
Finalize proceedings
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ proceedings_content }}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script src="{% static "ietf/js/list.js" %}">
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue