From 45ed2c5a2ccf14d95a5c05caefa17cf73cdd1a0b Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Tue, 30 Jun 2020 16:55:24 +0000
Subject: [PATCH 01/17] Add support in the new meeting schedule editor for
making a tombstone session when rescheduling a session after the schedule is
made the official meeting schedule.
Show both cancelled and rescheduled sessions as tombstones in the new
meeting schedule editor.
Add support for showing rescheduled tombstones in the meeting agenda
views.
Adjust the Secretariat session tool so that it's not possible to
(re)cancel cancelled or rescheduled tombstones.
- Legacy-Id: 18108
---
ietf/meeting/helpers.py | 5 +
.../migrations/0029_session_tombstone_for.py | 19 ++
ietf/meeting/models.py | 2 +
ietf/meeting/tests_js.py | 10 +-
ietf/meeting/tests_views.py | 51 +++-
ietf/meeting/views.py | 256 +++++++++++-------
ietf/name/fixtures/names.json | 10 +
.../0013_add_rescheduled_session_name.py | 24 ++
ietf/secr/meetings/views.py | 16 +-
ietf/secr/templates/meetings/sessions.html | 12 +-
ietf/static/ietf/css/ietf.css | 5 +
ietf/static/ietf/js/edit-meeting-schedule.js | 40 ++-
ietf/templates/meeting/agenda.html | 18 +-
ietf/templates/meeting/agenda.txt | 2 +-
14 files changed, 332 insertions(+), 138 deletions(-)
create mode 100644 ietf/meeting/migrations/0029_session_tombstone_for.py
create mode 100644 ietf/name/migrations/0013_add_rescheduled_session_name.py
diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py
index c4f3e72c4..18644f46e 100644
--- a/ietf/meeting/helpers.py
+++ b/ietf/meeting/helpers.py
@@ -212,10 +212,15 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
parents = Group.objects.filter(pk__in=parent_id_set)
parent_replacements = find_history_replacements_active_at(parents, meeting_time)
+ timeslot_by_session_pk = {a.session_id: a.timeslot for a in assignments}
+
for a in assignments:
if a.session and a.session.historic_group and a.session.historic_group.parent_id:
a.session.historic_group.historic_parent = parent_replacements.get(a.session.historic_group.parent_id)
+ if a.session.current_status == 'resched':
+ a.session.rescheduled_to = timeslot_by_session_pk.get(a.session.tombstone_for_id)
+
for d in a.session.prefetched_active_materials:
# make sure these are precomputed with the meeting instead
# of having to look it up
diff --git a/ietf/meeting/migrations/0029_session_tombstone_for.py b/ietf/meeting/migrations/0029_session_tombstone_for.py
new file mode 100644
index 000000000..0b765c533
--- /dev/null
+++ b/ietf/meeting/migrations/0029_session_tombstone_for.py
@@ -0,0 +1,19 @@
+# Copyright The IETF Trust 2020, All Rights Reserved
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('meeting', '0028_auto_20200501_0139'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='session',
+ name='tombstone_for',
+ field=models.ForeignKey(blank=True, help_text='This session is the tombstone for a session that was rescheduled', null=True, on_delete=django.db.models.deletion.CASCADE, to='meeting.Session'),
+ ),
+ ]
diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py
index e4483e144..0038ca1ac 100644
--- a/ietf/meeting/models.py
+++ b/ietf/meeting/models.py
@@ -923,6 +923,8 @@ class Session(models.Model):
modified = models.DateTimeField(auto_now=True)
remote_instructions = models.CharField(blank=True,max_length=1024)
+ tombstone_for = models.ForeignKey('Session', blank=True, null=True, help_text="This session is the tombstone for a session that was rescheduled", on_delete=models.CASCADE)
+
materials = models.ManyToManyField(Document, through=SessionPresentation, blank=True)
resources = models.ManyToManyField(ResourceAssociation, blank=True)
diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py
index 4c6d04f34..7b5b5b924 100644
--- a/ietf/meeting/tests_js.py
+++ b/ietf/meeting/tests_js.py
@@ -16,9 +16,11 @@ import debug # pyflakes:ignore
from ietf.doc.factories import DocumentFactory
from ietf.group import colors
+from ietf.person.models import Person
from ietf.meeting.factories import SessionFactory
from ietf.meeting.test_data import make_meeting_test_data
from ietf.meeting.models import Schedule, SchedTimeSessAssignment, Session, Room, TimeSlot, Constraint, ConstraintName
+from ietf.meeting.models import SchedulingEvent, SessionStatusName
from ietf.utils.test_runner import IetfLiveServerTestCase
from ietf.utils.pipe import pipe
from ietf import settings
@@ -107,6 +109,12 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
s2b = Session.objects.create(meeting=meeting, group=s2.group, attendees=10, requested_duration=datetime.timedelta(minutes=60), type_id='regular')
+ SchedulingEvent.objects.create(
+ session=s2b,
+ status=SessionStatusName.objects.get(slug='appr'),
+ by=Person.objects.get(name='(System)'),
+ )
+
Constraint.objects.create(
meeting=meeting,
source=s1.group,
@@ -226,7 +234,7 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
self.assertTrue(s1_element.is_displayed())
# hide timeslots
- self.driver.find_element_by_css_selector(".timeslot-group-toggles button".format(s1.group.parent.acronym)).click()
+ self.driver.find_element_by_css_selector(".timeslot-group-toggles button").click()
self.assertTrue(self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed())
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click()
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click()
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index 00909d18e..914a50eae 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -1049,10 +1049,14 @@ class EditTests(TestCase):
self.assertEqual(r.status_code, 403)
# turn us into owner
- meeting.schedule.owner = Person.objects.get(user__username="secretary")
- meeting.schedule.save()
+ schedule = meeting.schedule
+ schedule.owner = Person.objects.get(user__username="secretary")
+ schedule.save()
- url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
+ meeting.schedule = None
+ meeting.save()
+
+ url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=schedule.owner_email(), name=schedule.name))
r = self.client.get(url)
q = PyQuery(r.content)
self.assertTrue(not q("em:contains(\"You can't edit this schedule\")"))
@@ -1065,25 +1069,52 @@ class EditTests(TestCase):
'timeslot': timeslots[0].pk,
'session': s1.pk,
})
- self.assertEqual(r.content, b"OK")
- self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=meeting.schedule, session=s1).timeslot, timeslots[0])
+ self.assertEqual(json.loads(r.content)['success'], True)
+ self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0])
- # move assignment
+ # move assignment on unofficial schedule
r = self.client.post(url, {
'action': 'assign',
'timeslot': timeslots[1].pk,
'session': s1.pk,
})
- self.assertEqual(r.content, b"OK")
- self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=meeting.schedule, session=s1).timeslot, timeslots[1])
+ self.assertEqual(json.loads(r.content)['success'], True)
+ self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[1])
+
+ # move assignment on official schedule, leaving tombstone
+ meeting.schedule = schedule
+ meeting.save()
+ SchedulingEvent.objects.create(
+ session=s1,
+ status=SessionStatusName.objects.get(slug='sched'),
+ by=Person.objects.get(name='(System)')
+ )
+ r = self.client.post(url, {
+ 'action': 'assign',
+ 'timeslot': timeslots[0].pk,
+ 'session': s1.pk,
+ })
+ json_content = json.loads(r.content)
+ self.assertEqual(json_content['success'], True)
+ self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0])
+
+ sessions_for_group = Session.objects.filter(group=s1.group, meeting=meeting)
+ self.assertEqual(len(sessions_for_group), 2)
+ s_tombstone = [s for s in sessions_for_group if s != s1][0]
+ self.assertEqual(s_tombstone.tombstone_for, s1)
+ tombstone_event = SchedulingEvent.objects.get(session=s_tombstone)
+ self.assertEqual(tombstone_event.status_id, 'resched')
+
+ self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s_tombstone).timeslot, timeslots[1])
+ self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.tombstone".format(s_tombstone.pk)).html())
# unassign
r = self.client.post(url, {
'action': 'unassign',
'session': s1.pk,
})
- self.assertEqual(r.content, b"OK")
- self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule, session=s1)), [])
+ self.assertEqual(json.loads(r.content)['success'], True)
+ self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), [])
def test_copy_meeting_schedule(self):
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index d927afcb1..9e023ada5 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -26,14 +26,14 @@ import debug # pyflakes:ignore
from django import forms
from django.shortcuts import render, redirect, get_object_or_404
-from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404
+from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404, JsonResponse
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.urls import reverse,reverse_lazy
-from django.db.models import F, Min, Max, Prefetch, Q
+from django.db.models import F, Min, Max, Q
from django.forms.models import modelform_factory, inlineformset_factory
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
@@ -51,7 +51,6 @@ from ietf.doc.models import Document, State, DocEvent, NewRevisionDocEvent, DocA
from ietf.group.models import Group
from ietf.group.utils import can_manage_session_materials, can_manage_some_groups, can_manage_group
from ietf.person.models import Person
-from ietf.person.name import plain_name
from ietf.ietfauth.utils import role_required, has_role
from ietf.mailtrigger.utils import gather_address_lists
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
@@ -459,7 +458,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
assignments = get_all_assignments_from_schedule(schedule)
rooms = meeting.room_set.filter(session_types__slug='regular').distinct().order_by("capacity")
- timeslots_qs = meeting.timeslot_set.filter(type='regular').prefetch_related('type', 'sessions').order_by('location', 'time', 'name')
+
+ tombstone_states = ['canceled', 'canceledpa', 'resched']
sessions = add_event_info_to_session_qs(
Session.objects.filter(
@@ -469,45 +469,14 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
).order_by('pk'),
requested_time=True,
requested_by=True,
- ).exclude(current_status__in=['notmeet', 'disappr', 'deleted', 'apprw']).prefetch_related(
+ ).filter(
+ Q(current_status__in=['appr', 'schedw', 'scheda', 'sched'])
+ | Q(current_status__in=tombstone_states, pk__in={a.session_id for a in assignments})
+ ).prefetch_related(
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups',
)
- if request.method == 'POST':
- if not can_edit:
- return HttpResponseForbidden("Can't edit this schedule")
-
- action = request.POST.get('action')
-
- if action == 'assign' and request.POST.get('session', '').isdigit() and request.POST.get('timeslot', '').isdigit():
- session = get_object_or_404(sessions, pk=request.POST['session'])
- timeslot = get_object_or_404(timeslots_qs, pk=request.POST['timeslot'])
-
- existing_assignments = SchedTimeSessAssignment.objects.filter(session=session, schedule=schedule)
- if existing_assignments:
- existing_assignments.update(timeslot=timeslot, modified=datetime.datetime.now())
- else:
- SchedTimeSessAssignment.objects.create(
- session=session,
- schedule=schedule,
- timeslot=timeslot,
- )
-
- return HttpResponse("OK")
-
- elif action == 'unassign' and request.POST.get('session', '').isdigit():
- session = get_object_or_404(sessions, pk=request.POST['session'])
- SchedTimeSessAssignment.objects.filter(session=session, schedule=schedule).delete()
-
- return HttpResponse("OK")
-
- return HttpResponse("Invalid parameters", status_code=400)
-
- assignments_by_session = defaultdict(list)
- for a in assignments:
- assignments_by_session[a.session_id].append(a)
-
- # prepare timeslot layout
+ timeslots_qs = meeting.timeslot_set.filter(type='regular').prefetch_related('type', 'sessions').order_by('location', 'time', 'name')
min_duration = min(t.duration for t in timeslots_qs)
max_duration = max(t.duration for t in timeslots_qs)
@@ -525,6 +494,119 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
scale = (capped_timedelta - capped_min_d) / (capped_max_d - capped_min_d) if capped_min_d != capped_max_d else 1
return min_d_css_rems + (max_d_css_rems - min_d_css_rems) * scale
+ def prepare_sessions_for_display(sessions):
+ # requesters
+ requested_by_lookup = {p.pk: p for p in Person.objects.filter(pk__in=set(s.requested_by for s in sessions if s.requested_by))}
+
+ # constraints
+ constraints_for_sessions, formatted_constraints_for_sessions, constraint_names = preprocess_constraints_for_meeting_schedule_editor(meeting, sessions)
+
+ sessions_for_group = defaultdict(list)
+ for s in sessions:
+ sessions_for_group[s.group_id].append(s)
+
+ for s in sessions:
+ s.requested_by_person = requested_by_lookup.get(s.requested_by)
+
+ s.scheduling_label = "???"
+ if s.group:
+ s.scheduling_label = s.group.acronym
+ elif s.name:
+ s.scheduling_label = s.name
+
+ s.requested_duration_in_hours = round(s.requested_duration.seconds / 60.0 / 60.0, 1)
+
+ session_layout_margin = 0.2
+ s.layout_width = timedelta_to_css_ems(s.requested_duration) - 2 * session_layout_margin
+ s.parent_acronym = s.group.parent.acronym if s.group and s.group.parent else ""
+
+ # compress the constraints, so similar constraint labels are
+ # shared between the conflicting sessions they cover - the JS
+ # then simply has to detect violations and show the
+ # preprocessed labels
+ constrained_sessions_grouped_by_label = defaultdict(set)
+ for name_id, ts in itertools.groupby(sorted(constraints_for_sessions.get(s.pk, [])), key=lambda t: t[0]):
+ ts = list(ts)
+ session_pks = (t[1] for t in ts)
+ constraint_name = constraint_names[name_id]
+ if "{count}" in constraint_name.formatted_editor_label:
+ for session_pk, grouped_session_pks in itertools.groupby(session_pks):
+ count = sum(1 for i in grouped_session_pks)
+ constrained_sessions_grouped_by_label[format_html(constraint_name.formatted_editor_label, count=count)].add(session_pk)
+
+ else:
+ constrained_sessions_grouped_by_label[constraint_name.formatted_editor_label].update(session_pks)
+
+ s.constrained_sessions = list(constrained_sessions_grouped_by_label.items())
+ s.formatted_constraints = formatted_constraints_for_sessions.get(s.pk, {})
+
+ s.other_sessions = [s_other for s_other in sessions_for_group.get(s.group_id) if s != s_other]
+
+ s.is_tombstone = s.current_status in tombstone_states
+
+
+ if request.method == 'POST': # handle ajax requests
+ if not can_edit:
+ return HttpResponseForbidden("Can't edit this schedule")
+
+ action = request.POST.get('action')
+
+ if action == 'assign' and request.POST.get('session', '').isdigit() and request.POST.get('timeslot', '').isdigit():
+ session = get_object_or_404(sessions, pk=request.POST['session'])
+ timeslot = get_object_or_404(timeslots_qs, pk=request.POST['timeslot'])
+
+ tombstone_session = None
+
+ existing_assignments = SchedTimeSessAssignment.objects.filter(session=session, schedule=schedule)
+ if existing_assignments:
+ if schedule.pk == meeting.schedule_id and session.current_status == 'sched':
+ old_timeslot = existing_assignments[0].timeslot
+ # clone session and leave it as a tombstone
+ tombstone_session = session
+ tombstone_session.tombstone_for_id = session.pk
+ tombstone_session.pk = None
+ tombstone_session.save()
+
+ session = None
+
+ SchedulingEvent.objects.create(
+ session=tombstone_session,
+ status=SessionStatusName.objects.get(slug='resched'),
+ by=request.user.person,
+ )
+
+ tombstone_session.current_status = 'resched' # rematerialize status for the rendering
+
+ SchedTimeSessAssignment.objects.create(
+ session=tombstone_session,
+ schedule=schedule,
+ timeslot=old_timeslot,
+ )
+
+ existing_assignments.update(timeslot=timeslot, modified=datetime.datetime.now())
+ else:
+ SchedTimeSessAssignment.objects.create(
+ session=session,
+ schedule=schedule,
+ timeslot=timeslot,
+ )
+
+ r = {'success': True}
+ if tombstone_session:
+ prepare_sessions_for_display([tombstone_session])
+ r['tombstone'] = render_to_string("meeting/edit_meeting_schedule_session.html", {'session': tombstone_session})
+ return JsonResponse(r)
+
+ elif action == 'unassign' and request.POST.get('session', '').isdigit():
+ session = get_object_or_404(sessions, pk=request.POST['session'])
+ SchedTimeSessAssignment.objects.filter(session=session, schedule=schedule).delete()
+
+ return JsonResponse({'success': True})
+
+ return HttpResponse("Invalid parameters", status_code=400)
+
+ # prepare timeslot layout
+
timeslots_by_room_and_day = defaultdict(list)
room_has_timeslots = set()
for t in timeslots_qs:
@@ -559,10 +641,28 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
timeslot_groups[ts.time.date()].add((ts.time, ts.end_time(), ts.start_end_group))
# prepare sessions
+ prepare_sessions_for_display(sessions)
+
for ts in timeslots_qs:
ts.session_assignments = []
timeslots_by_pk = {ts.pk: ts for ts in timeslots_qs}
+ assignments_by_session = defaultdict(list)
+ for a in assignments:
+ assignments_by_session[a.session_id].append(a)
+
+ unassigned_sessions = []
+ for s in sessions:
+ assigned = False
+ for a in assignments_by_session.get(s.pk, []):
+ timeslot = timeslots_by_pk.get(a.timeslot_id)
+ if timeslot:
+ timeslot.session_assignments.append((a, s))
+ assigned = True
+
+ if not assigned:
+ unassigned_sessions.append(s)
+
# group parent colors
def cubehelix(i, total, hue=1.2, start_angle=0.5):
# theory in https://arxiv.org/pdf/1108.5083.pdf
@@ -586,64 +686,6 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
p.scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round(x * 255)) for x in rgb_color))
p.light_scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round((0.9 + 0.1 * x) * 255)) for x in rgb_color))
- # requesters
- requested_by_lookup = {p.pk: p for p in Person.objects.filter(pk__in=set(s.requested_by for s in sessions if s.requested_by))}
-
- # constraints
- constraints_for_sessions, formatted_constraints_for_sessions, constraint_names = preprocess_constraints_for_meeting_schedule_editor(meeting, sessions)
-
- sessions_for_group = defaultdict(list)
- for s in sessions:
- sessions_for_group[s.group_id].append(s)
-
- unassigned_sessions = []
- for s in sessions:
- s.requested_by_person = requested_by_lookup.get(s.requested_by)
-
- s.scheduling_label = "???"
- if s.group:
- s.scheduling_label = s.group.acronym
- elif s.name:
- s.scheduling_label = s.name
-
- s.requested_duration_in_hours = round(s.requested_duration.seconds / 60.0 / 60.0, 1)
-
- session_layout_margin = 0.2
- s.layout_width = timedelta_to_css_ems(s.requested_duration) - 2 * session_layout_margin
- s.parent_acronym = s.group.parent.acronym if s.group and s.group.parent else ""
-
- # compress the constraints, so similar constraint labels are
- # shared between the conflicting sessions they cover - the JS
- # then simply has to detect violations and show the
- # preprocessed labels
- constrained_sessions_grouped_by_label = defaultdict(set)
- for name_id, ts in itertools.groupby(sorted(constraints_for_sessions.get(s.pk, [])), key=lambda t: t[0]):
- ts = list(ts)
- session_pks = (t[1] for t in ts)
- constraint_name = constraint_names[name_id]
- if "{count}" in constraint_name.formatted_editor_label:
- for session_pk, grouped_session_pks in itertools.groupby(session_pks):
- count = sum(1 for i in grouped_session_pks)
- constrained_sessions_grouped_by_label[format_html(constraint_name.formatted_editor_label, count=count)].add(session_pk)
-
- else:
- constrained_sessions_grouped_by_label[constraint_name.formatted_editor_label].update(session_pks)
-
- s.constrained_sessions = list(constrained_sessions_grouped_by_label.items())
- s.formatted_constraints = formatted_constraints_for_sessions.get(s.pk, {})
-
- s.other_sessions = [s_other for s_other in sessions_for_group.get(s.group_id) if s != s_other]
-
- assigned = False
- for a in assignments_by_session.get(s.pk, []):
- timeslot = timeslots_by_pk.get(a.timeslot_id)
- if timeslot:
- timeslot.session_assignments.append((a, s))
- assigned = True
-
- if not assigned:
- unassigned_sessions.append(s)
-
js_data = {
'can_edit': can_edit,
'urls': {
@@ -1214,9 +1256,19 @@ def room_view(request, num=None, name=None, owner=None):
template = "meeting/room-view.html"
return render(request, template,{"meeting":meeting,"schedule":schedule,"unavailable":unavailable,"assignments":assignments,"rooms":rooms,"days":days})
-def ical_session_status(session_with_current_status):
- if session_with_current_status == 'canceled':
+def ical_session_status(assignment):
+ if assignment.session.current_status == 'canceled':
return "CANCELLED"
+ elif assignment.session.current_status == 'resched':
+ t = "RESCHEDULED"
+ if assignment.session.tombstone_for_id is not None:
+ other_assignment = SchedTimeSessAssignment.objects.filter(schedule=assignment.schedule_id, session=assignment.session.tombstone_for_id).first()
+ if other_assignment:
+ t = "RESCHEDULED TO {}-{}".format(
+ other_assignment.timeslot.time.strftime("%A %H:%M").upper(),
+ other_assignment.timeslot.end_time().strftime("%H:%M")
+ )
+ return t
else:
return "CONFIRMED"
@@ -1268,7 +1320,7 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
for a in assignments:
if a.session:
- a.session.ical_status = ical_session_status(a.session.current_status)
+ a.session.ical_status = ical_session_status(a)
return render(request, "meeting/agenda.ics", {
"schedule": schedule,
@@ -2655,7 +2707,7 @@ def upcoming_ical(request):
for a in assignments:
if a.session_id is not None:
a.session = sessions.get(a.session_id) or a.session
- a.session.ical_status = ical_session_status(a.session.current_status)
+ a.session.ical_status = ical_session_status(a)
# gather vtimezones
vtimezones = set()
diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json
index c678b951d..4c87b0bb0 100644
--- a/ietf/name/fixtures/names.json
+++ b/ietf/name/fixtures/names.json
@@ -11653,6 +11653,16 @@
"model": "name.sessionstatusname",
"pk": "schedw"
},
+ {
+ "fields": {
+ "desc": "",
+ "name": "Rescheduled",
+ "order": 0,
+ "used": true
+ },
+ "model": "name.sessionstatusname",
+ "pk": "resched"
+ },
{
"fields": {
"desc": "",
diff --git a/ietf/name/migrations/0013_add_rescheduled_session_name.py b/ietf/name/migrations/0013_add_rescheduled_session_name.py
new file mode 100644
index 000000000..c3ffe692e
--- /dev/null
+++ b/ietf/name/migrations/0013_add_rescheduled_session_name.py
@@ -0,0 +1,24 @@
+# Copyright The IETF Trust 2020, All Rights Reserved
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('name', '0012_update_constraintname_order_and_label'),
+ ]
+
+ def add_rescheduled_session_status_name(apps, schema_editor):
+ SessionStatusName = apps.get_model('name', 'SessionStatusName')
+ SessionStatusName.objects.get_or_create(
+ slug='resched',
+ name="Rescheduled",
+ )
+
+ def noop(apps, schema_editor):
+ pass
+
+ operations = [
+ migrations.RunPython(add_rescheduled_session_status_name, noop, elidable=True),
+ ]
diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py
index 815b16121..8e79efdb6 100644
--- a/ietf/secr/meetings/views.py
+++ b/ietf/secr/meetings/views.py
@@ -659,17 +659,21 @@ def regular_sessions(request, meeting_id, schedule_name):
if 'cancel' in request.POST:
pk = request.POST.get('pk')
session = get_object_or_404(sessions, pk=pk)
- SchedulingEvent.objects.create(
- session=session,
- status=SessionStatusName.objects.get(slug='canceled'),
- by=request.user.person,
- )
- messages.success(request, 'Session cancelled')
+ if session.current_status not in ['canceled', 'resched']:
+ SchedulingEvent.objects.create(
+ session=session,
+ status=SessionStatusName.objects.get(slug='canceled'),
+ by=request.user.person,
+ )
+ messages.success(request, 'Session cancelled')
+
+ return redirect('ietf.secr.meetings.views.regular_sessions', meeting_id=meeting_id, schedule_name=schedule_name)
status_names = {n.slug: n.name for n in SessionStatusName.objects.all()}
for s in sessions:
s.current_status_name = status_names.get(s.current_status, s.current_status)
+ s.can_cancel = s.current_status not in ['canceled', 'resched']
return render(request, 'meetings/sessions.html', {
'meeting': meeting,
diff --git a/ietf/secr/templates/meetings/sessions.html b/ietf/secr/templates/meetings/sessions.html
index 2e6638fa0..558cd43e1 100644
--- a/ietf/secr/templates/meetings/sessions.html
+++ b/ietf/secr/templates/meetings/sessions.html
@@ -36,11 +36,13 @@
{{ session.current_status_name }} |
Edit |
-
+ {% if session.can_cancel %}
+
+ {% endif %}
|
{% endfor %}
diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css
index 8d5f65829..0d1457e6a 100644
--- a/ietf/static/ietf/css/ietf.css
+++ b/ietf/static/ietf/css/ietf.css
@@ -1118,6 +1118,11 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
cursor: default;
}
+.edit-meeting-schedule .session.tombstone {
+ cursor: default;
+ background-color: #ddd;
+}
+
.edit-meeting-schedule .session.selected .session-label {
font-weight: bold;
}
diff --git a/ietf/static/ietf/js/edit-meeting-schedule.js b/ietf/static/ietf/js/edit-meeting-schedule.js
index 711502ee0..a7fe03ca6 100644
--- a/ietf/static/ietf/js/edit-meeting-schedule.js
+++ b/ietf/static/ietf/js/edit-meeting-schedule.js
@@ -1,14 +1,14 @@
jQuery(document).ready(function () {
let content = jQuery(".edit-meeting-schedule");
- function failHandler(xhr, textStatus, error) {
- let errorText = error;
+ function reportServerError(xhr, textStatus, error) {
+ let errorText = error || textStatus;
if (xhr && xhr.responseText)
errorText += "\n\n" + xhr.responseText;
alert("Error: " + errorText);
}
- let sessions = content.find(".session");
+ let sessions = content.find(".session").not(".tombstone");
let timeslots = content.find(".timeslot");
let days = content.find(".day-flow .day");
@@ -130,7 +130,6 @@ jQuery(document).ready(function () {
});
sessions.on("dragend", function () {
jQuery(this).removeClass("dragging");
-
});
sessions.prop('draggable', true);
@@ -161,31 +160,50 @@ jQuery(document).ready(function () {
});
dropElements.on('drop', function (event) {
- jQuery(this).parent().removeClass("dropping");
+ let dropElement = jQuery(this);
let sessionId = event.originalEvent.dataTransfer.getData("text/plain");
- if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
+ if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session") {
+ dropElement.parent().removeClass("dropping");
return;
+ }
let sessionElement = sessions.filter("#" + sessionId);
- if (sessionElement.length == 0)
+ if (sessionElement.length == 0) {
+ dropElement.parent().removeClass("dropping");
return;
+ }
event.preventDefault(); // prevent opening as link
- if (sessionElement.parent().is(this))
+ let dragParent = sessionElement.parent();
+ if (dragParent.is(this)) {
+ dropElement.parent().removeClass("dropping");
return;
+ }
- let dropElement = jQuery(this);
let dropParent = dropElement.parent();
+ function failHandler(xhr, textStatus, error) {
+ dropElement.parent().removeClass("dropping");
+ console.log("xhr", xhr)
+ console.log("textstatus", textStatus)
+ console.log("error", error)
+ reportServerError(xhr, textStatus, error);
+ }
+
function done(response) {
- if (response != "OK") {
- failHandler(null, null, response);
+ dropElement.parent().removeClass("dropping");
+
+ if (!response.success) {
+ reportServerError(null, null, response);
return;
}
dropElement.append(sessionElement); // move element
+ if (response.tombstone)
+ dragParent.append(response.tombstone);
+
updateCurrentSchedulingHints();
if (dropParent.hasClass("unassigned-sessions"))
sortUnassigned();
diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html
index 8897ae8e0..6a8d9ab84 100644
--- a/ietf/templates/meeting/agenda.html
+++ b/ietf/templates/meeting/agenda.html
@@ -234,7 +234,7 @@
{% if item.timeslot.type.slug == 'other' %}
{% if item.session.agenda %}
- {% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
+ {% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
{% else %}
{% for slide in item.session.slides %}
{{ slide.title|clean_whitespace }}
@@ -323,6 +323,20 @@
CANCELLED
{% endif %}
+ {% if item.session.current_status == 'resched' %}
+
+ RESCHEDULED
+ {% if item.session.rescheduled_to %}
+ TO
+ {% if "-utc" in request.path %}
+ {{ item.session.rescheduled_to.utc_start_time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.utc_end_time|date:"G:i" }}
+ {% else %}
+ {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}
+ {% endif %}
+ {% endif %}
+
+ {% endif %}
+
{% if item.session.agenda_note|first_url %}
{{item.session.agenda_note|slice:":23"}}
{% elif item.session.agenda_note %}
@@ -332,7 +346,7 @@
- {% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
+ {% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
|
diff --git a/ietf/templates/meeting/agenda.txt b/ietf/templates/meeting/agenda.txt
index 1bc6d5715..ce804b224 100644
--- a/ietf/templates/meeting/agenda.txt
+++ b/ietf/templates/meeting/agenda.txt
@@ -22,7 +22,7 @@
{% endif %}{% if item.timeslot.type_id == 'regular' %}{% if item.session.historic_group %}{% ifchanged %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}
-{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% endif %}
+{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% elif item.session.current_status == 'resched' %} *** RESCHEDULED{% if item.session.rescheduled_to %} TO {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}{% endif %} ***{% endif %}
{% endif %}{% endif %}{% if item.timeslot.type.slug == "break" %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.break_area and item.timeslot.show_location %} - {{ schedule.meeting.break_area }}{% endif %}{% endif %}{% if item.timeslot.type.slug == "other" %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }} - {{ item.timeslot.location.name }}{% endif %}{% endfor %}
From ae515e6a27b743b569bb6658e99a95ce3627fda1 Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Wed, 1 Jul 2020 10:14:14 +0000
Subject: [PATCH 02/17] Add notes field to Schedule.
Rearrange schedules in schedule list and be more consistent in the
naming. Add edit properties link to the meeting schedule editor.
Reword the Schedule.visible and .public help texts to try to better
explain what setting the fields results in.
- Legacy-Id: 18111
---
.../meeting/migrations/0030_schedule_notes.py | 28 +++++
ietf/meeting/models.py | 19 +---
ietf/meeting/views.py | 104 +++++++++++-------
ietf/static/ietf/css/ietf.css | 8 ++
.../meeting/copy_meeting_schedule.html | 6 +-
.../meeting/edit_meeting_schedule.html | 16 ++-
ietf/templates/meeting/landscape_edit.html | 3 +
ietf/templates/meeting/schedule_list.html | 51 +++++----
8 files changed, 152 insertions(+), 83 deletions(-)
create mode 100644 ietf/meeting/migrations/0030_schedule_notes.py
diff --git a/ietf/meeting/migrations/0030_schedule_notes.py b/ietf/meeting/migrations/0030_schedule_notes.py
new file mode 100644
index 000000000..83c7ced80
--- /dev/null
+++ b/ietf/meeting/migrations/0030_schedule_notes.py
@@ -0,0 +1,28 @@
+# Generated by Django 2.0.13 on 2020-07-01 02:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('meeting', '0029_session_tombstone_for'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='schedule',
+ name='notes',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AlterField(
+ model_name='schedule',
+ name='public',
+ field=models.BooleanField(default=True, help_text='Allow others to see this agenda.'),
+ ),
+ migrations.AlterField(
+ model_name='schedule',
+ name='visible',
+ field=models.BooleanField(default=True, help_text='Show in the list of possible agendas for the meeting.', verbose_name='Show in agenda list'),
+ ),
+ ]
diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py
index 0038ca1ac..8bcc8b221 100644
--- a/ietf/meeting/models.py
+++ b/ietf/meeting/models.py
@@ -622,9 +622,10 @@ class Schedule(models.Model):
meeting = ForeignKey(Meeting, null=True, related_name='schedule_set')
name = models.CharField(max_length=16, blank=False, help_text="Letters, numbers and -:_ allowed.", validators=[RegexValidator(r'^[A-Za-z0-9-:_]*$')])
owner = ForeignKey(Person)
- visible = models.BooleanField(default=True, help_text="Make this agenda available to those who know about it.")
- public = models.BooleanField(default=True, help_text="Make this agenda publically available.")
+ visible = models.BooleanField("Show in agenda list", default=True, help_text="Show in the list of possible agendas for the meeting.")
+ public = models.BooleanField(default=True, help_text="Allow others to see this agenda.")
badness = models.IntegerField(null=True, blank=True)
+ notes = models.TextField(blank=True)
# considering copiedFrom = ForeignKey('Schedule', blank=True, null=True)
def __str__(self):
@@ -652,20 +653,6 @@ class Schedule(models.Model):
else:
return "noemail"
- @property
- def visible_token(self):
- if self.visible:
- return "visible"
- else:
- return "hidden"
-
- @property
- def public_token(self):
- if self.public:
- return "public"
- else:
- return "private"
-
@property
def is_official(self):
return (self.meeting.schedule == self)
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 9e023ada5..e88f67645 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -51,7 +51,7 @@ from ietf.doc.models import Document, State, DocEvent, NewRevisionDocEvent, DocA
from ietf.group.models import Group
from ietf.group.utils import can_manage_session_materials, can_manage_some_groups, can_manage_group
from ietf.person.models import Person
-from ietf.ietfauth.utils import role_required, has_role
+from ietf.ietfauth.utils import role_required, has_role, user_is_person
from ietf.mailtrigger.utils import gather_address_lists
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment
@@ -374,7 +374,7 @@ class CopyScheduleForm(forms.ModelForm):
counter += 1
self.fields['name'].initial = name_suggestion
- self.fields['name'].label = "Name of new schedule"
+ self.fields['name'].label = "Name of new agenda"
def clean_name(self):
name = self.cleaned_data.get('name')
@@ -697,6 +697,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
'meeting': meeting,
'schedule': schedule,
'can_edit': can_edit,
+ 'can_edit_properties': can_edit or secretariat,
+ 'secretariat': secretariat,
'js_data': json.dumps(js_data, indent=2),
'days': days,
'room_labels': room_labels,
@@ -777,64 +779,86 @@ def edit_schedule(request, num=None, owner=None, name=None):
"assignments": assignments,
"show_inline": set(["txt","htm","html"]),
"hide_menu": True,
+ "can_edit_properties": can_edit or secretariat,
})
-##############################################################################
-# show the properties associated with a schedule (visible, public)
-#
-SchedulePropertiesForm = modelform_factory(Schedule, fields=('name','visible', 'public'))
-
-# The meeing urls.py won't allow empy num, owmer, or name values
+SchedulePropertiesForm = modelform_factory(Schedule, fields=['name', 'notes', 'visible', 'public'])
@role_required('Area Director','Secretariat')
-def edit_schedule_properties(request, num=None, owner=None, name=None):
+def edit_schedule_properties(request, num, owner, name):
meeting = get_meeting(num)
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
if schedule is None:
- raise Http404("No meeting information for meeting %s owner %s schedule %s available" % (num, owner, name))
+ raise Http404("No agenda information for meeting %s owner %s schedule %s available" % (num, owner, name))
- cansee, canedit, secretariat = schedule_permissions(meeting, schedule, request.user)
+ can_see, can_edit, secretariat = schedule_permissions(meeting, schedule, request.user)
- if not (canedit or has_role(request.user,'Secretariat')):
+ can_edit_properties = can_edit or secretariat
+
+ if not can_edit_properties:
return HttpResponseForbidden("You may not edit this schedule")
- else:
- if request.method == 'POST':
- form = SchedulePropertiesForm(instance=schedule,data=request.POST)
- if form.is_valid():
- form.save()
- return HttpResponseRedirect(reverse('ietf.meeting.views.list_schedules',kwargs={'num': num}))
- else:
- form = SchedulePropertiesForm(instance=schedule)
- return render(request, "meeting/properties_edit.html",
- {"schedule":schedule,
- "form":form,
- "meeting":meeting,
- })
-##############################################################################
-# show list of schedules.
-#
+ if request.method == 'POST':
+ form = SchedulePropertiesForm(instance=schedule, data=request.POST)
+ if form.is_valid():
+ form.save()
+ return redirect('ietf.meeting.views.edit_schedule', num=num, owner=owner, name=name)
+ else:
+ form = SchedulePropertiesForm(instance=schedule)
+
+ return render(request, "meeting/properties_edit.html", {
+ "schedule": schedule,
+ "form": form,
+ "meeting": meeting,
+ })
+
+
+nat_sort_re = re.compile('([0-9]+)')
+def natural_sort_key(s): # from https://stackoverflow.com/questions/4836710/is-there-a-built-in-function-for-string-natural-sort
+ return [int(text) if text.isdecimal() else text.lower() for text in nat_sort_re.split(s)]
@role_required('Area Director','Secretariat')
-def list_schedules(request, num=None ):
-
+def list_schedules(request, num):
meeting = get_meeting(num)
- user = request.user
- schedules = meeting.schedule_set
- if not has_role(user, 'Secretariat'):
- schedules = schedules.filter(visible = True) | schedules.filter(owner = user.person)
+ schedules = Schedule.objects.filter(meeting=meeting).prefetch_related('owner').order_by('owner', '-name', '-public').distinct()
+ if not has_role(request.user, 'Secretariat'):
+ schedules = schedules.filter(Q(visible=True) | Q(owner=request.user.person))
- schedules = schedules.order_by('owner', 'name')
+ official_schedules = []
+ own_schedules = []
+ other_public_schedules = []
+ other_private_schedules = []
- schedules = sorted(list(schedules),key=lambda x:not x.is_official)
+ is_secretariat = has_role(request.user, 'Secretariat')
- return render(request, "meeting/schedule_list.html",
- {"meeting": meeting,
- "schedules": schedules,
- })
+ for s in schedules:
+ s.can_edit_properties = is_secretariat or user_is_person(request.user, s.owner)
+
+ if s.pk == meeting.schedule_id:
+ official_schedules.append(s)
+ elif user_is_person(request.user, s.owner):
+ own_schedules.append(s)
+ elif s.public:
+ other_public_schedules.append(s)
+ else:
+ other_private_schedules.append(s)
+
+ schedule_groups = [
+ ("Official Agenda", official_schedules),
+ ("Own Draft Agendas", own_schedules),
+ ("Other Draft Agendas", other_public_schedules),
+ ("Other Private Draft Agendas", other_private_schedules),
+ ]
+
+ schedule_groups = [(label, sorted(l, reverse=True, key=lambda s: natural_sort_key(s.name))) for label, l in schedule_groups if l]
+
+ return render(request, "meeting/schedule_list.html", {
+ 'meeting': meeting,
+ 'schedule_groups': schedule_groups,
+ })
@ensure_csrf_cookie
def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css
index 0d1457e6a..a5f046459 100644
--- a/ietf/static/ietf/css/ietf.css
+++ b/ietf/static/ietf/css/ietf.css
@@ -1003,6 +1003,14 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
border-top: 1px solid #ddd;
}
+/* === List Meeting Schedules ====================================== */
+
+.table a.edit-schedule-properties {
+ display: inline-block;
+ margin-left: 0.2em;
+}
+
+
/* === Edit Meeting Schedule ====================================== */
.edit-meeting-schedule .edit-grid {
diff --git a/ietf/templates/meeting/copy_meeting_schedule.html b/ietf/templates/meeting/copy_meeting_schedule.html
index 14c1365b5..3b28fcc66 100644
--- a/ietf/templates/meeting/copy_meeting_schedule.html
+++ b/ietf/templates/meeting/copy_meeting_schedule.html
@@ -7,14 +7,14 @@
{% block content %}
{% origin %}
- {% block title %}Copy schedule {{ schedule.name }}{% endblock %}
+ {% block title %}Copy agenda {{ schedule.name }}{% endblock %}
-
{% endblock %}
diff --git a/ietf/templates/meeting/edit_meeting_schedule.html b/ietf/templates/meeting/edit_meeting_schedule.html
index 616103f59..bf8f898e9 100644
--- a/ietf/templates/meeting/edit_meeting_schedule.html
+++ b/ietf/templates/meeting/edit_meeting_schedule.html
@@ -12,7 +12,7 @@
{% endfor %}
{% endblock morecss %}
-{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting schedule{% endblock %}
+{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %}
{% block js %}
{% endblock js %}
@@ -80,7 +77,7 @@
{% for day in days %}
- {{ day.day|date:"l" }}
+ {{ day.day|date:"l" }}
{{ day.day|date:"N j, Y" }}
@@ -94,9 +91,9 @@
- {% for assignment, session in t.session_assignments %}
- {% include "meeting/edit_meeting_schedule_session.html" %}
- {% endfor %}
+ {% for assignment, session in t.session_assignments %}
+ {% include "meeting/edit_meeting_schedule_session.html" %}
+ {% endfor %}
{% endfor %}
@@ -173,5 +170,34 @@
+
+
{% endblock %}
From c78ffbcd18a1cb8dd047d8da87b89c7c1371cb5a Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Tue, 11 Aug 2020 17:34:32 +0000
Subject: [PATCH 08/17] Introduce support for setting a base schedule on a
schedule. All assignments on the base schedule are shown in the pages for the
schedule, read-only.
This allows managing things like breaks and misc sessions separately
from the regular WG sessions.
Base schedules are not allowed to be the base of other base schedules
(the hierarchy can only be one level deep) to simplify the mental
model and the code.
Add link for creating new schedules instead of relying on copying
Empty-Schedule and change the meeting creation code to no longer
create the special Empty-Schedule. Instead a "base" schedule is
created and a first schedule with the name and permissions of the user
creating the meeting, using "base" as base.
Speed up a couple of the Secretariat/AD agenda views by adding
prefetches.
- Legacy-Id: 18355
---
ietf/bin/create-break-sessions | 8 +-
ietf/meeting/admin.py | 4 +-
ietf/meeting/helpers.py | 12 +-
.../commands/create_dummy_meeting.py | 7 +-
.../migrations/0032_add_schedule_base.py | 25 +++
ietf/meeting/models.py | 18 +-
ietf/meeting/test_data.py | 9 +-
ietf/meeting/tests_js.py | 2 +
ietf/meeting/tests_views.py | 123 +++++++----
ietf/meeting/urls.py | 3 +-
ietf/meeting/utils.py | 2 +-
ietf/meeting/views.py | 208 ++++++++++++------
ietf/secr/meetings/tests.py | 26 ++-
ietf/secr/meetings/views.py | 43 ++--
ietf/secr/proceedings/proc_utils.py | 5 +-
ietf/secr/proceedings/views.py | 9 +-
ietf/secr/static/secr/css/custom.css | 5 +
.../templates/meetings/misc_sessions.html | 18 +-
ietf/secr/utils/meeting.py | 4 +-
ietf/static/ietf/css/ietf.css | 5 +-
ietf/static/ietf/js/edit-meeting-schedule.js | 2 +-
ietf/templates/meeting/agenda_by_room.html | 2 +-
ietf/templates/meeting/agenda_by_type.html | 8 +-
.../meeting/edit_meeting_schedule.html | 4 +-
.../edit_meeting_schedule_session.html | 2 +-
.../meeting/interim_announcement.txt | 4 +-
.../meeting/interim_request_details.html | 2 +-
...chedule.html => new_meeting_schedule.html} | 4 +-
ietf/templates/meeting/room-view.html | 7 +-
ietf/templates/meeting/schedule_list.html | 42 ++--
30 files changed, 400 insertions(+), 213 deletions(-)
create mode 100644 ietf/meeting/migrations/0032_add_schedule_base.py
rename ietf/templates/meeting/{copy_meeting_schedule.html => new_meeting_schedule.html} (62%)
diff --git a/ietf/bin/create-break-sessions b/ietf/bin/create-break-sessions
index 8ee8f1f2c..52ce044d8 100755
--- a/ietf/bin/create-break-sessions
+++ b/ietf/bin/create-break-sessions
@@ -33,11 +33,11 @@ for meeting in Meeting.objects.filter(type="ietf").order_by("date"):
for schedule in meeting.schedule_set.all():
print " Checking for missing Break and Reg sessions in %s" % schedule
for timeslot in meeting.timeslot_set.all():
- if timeslot.type_id == 'break':
- assignment, created = ScheduleTimeslotSSessionAssignment.objects.get_or_create(timeslot=timeslot, session=brk, schedule=schedule)
+ if timeslot.type_id == 'break' and not (schedule.base and SchedTimeSessAssignment.objects.filter(timeslot=timeslot, session=brk, schedule=schedule.base).exists()):
+ assignment, created = SchedTimeSessAssignment.objects.get_or_create(timeslot=timeslot, session=brk, schedule=schedule)
if created:
print " Added %s break assignment" % timeslot
- if timeslot.type_id == 'reg':
- assignment, created = ScheduleTimeslotSSessionAssignment.objects.get_or_create(timeslot=timeslot, session=reg, schedule=schedule)
+ if timeslot.type_id == 'reg' and not (schedule.base and SchedTimeSessAssignment.objects.filter(timeslot=timeslot, session=reg, schedule=schedule.base).exists()):
+ assignment, created = SchedTimeSessAssignment.objects.get_or_create(timeslot=timeslot, session=reg, schedule=schedule)
if created:
print " Added %s registration assignment" % timeslot
diff --git a/ietf/meeting/admin.py b/ietf/meeting/admin.py
index f8477ce2f..59ac39b10 100644
--- a/ietf/meeting/admin.py
+++ b/ietf/meeting/admin.py
@@ -126,8 +126,8 @@ admin.site.register(SchedulingEvent, SchedulingEventAdmin)
class ScheduleAdmin(admin.ModelAdmin):
list_display = ["name", "meeting", "owner", "visible", "public", "badness"]
- list_filter = ["meeting", ]
- raw_id_fields = ["meeting", "owner", ]
+ list_filter = ["meeting"]
+ raw_id_fields = ["meeting", "owner", "origin", "base"]
search_fields = ["meeting__number", "name", "owner__name"]
ordering = ["-meeting", "name"]
diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py
index 18644f46e..b401cc9e5 100644
--- a/ietf/meeting/helpers.py
+++ b/ietf/meeting/helpers.py
@@ -143,13 +143,6 @@ def get_schedule(meeting, name=None):
schedule = get_object_or_404(meeting.schedule_set, name=name)
return schedule
-def get_schedule_by_id(meeting, schedid):
- if schedid is None:
- schedule = meeting.schedule
- else:
- schedule = get_object_or_404(meeting.schedule_set, id=int(schedid))
- return schedule
-
# seems this belongs in ietf/person/utils.py?
def get_person_by_email(email):
# email == None may actually match people who haven't set an email!
@@ -428,6 +421,11 @@ def get_announcement_initial(meeting, is_change=False):
type = group.type.slug.upper()
if group.type.slug == 'wg' and group.state.slug == 'bof':
type = 'BOF'
+
+ assignments = SchedTimeSessAssignment.objects.filter(
+ schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
+ ).order_by('timeslot__time')
+
initial['subject'] = '{name} ({acronym}) {type} {desc} Meeting: {date}{change}'.format(
name=group.name,
acronym=group.acronym,
diff --git a/ietf/meeting/management/commands/create_dummy_meeting.py b/ietf/meeting/management/commands/create_dummy_meeting.py
index bab20b847..375f9a9a9 100644
--- a/ietf/meeting/management/commands/create_dummy_meeting.py
+++ b/ietf/meeting/management/commands/create_dummy_meeting.py
@@ -85,8 +85,11 @@ class Command(BaseCommand):
date=datetime.date(2019, 11, 16),
days=7,
)
- schedule = Schedule.objects.create(meeting=m, name='Empty-Schedule', owner_id=1,
- visible=True, public=True)
+ base_schedule = Schedule.objects.create(meeting=m, name='base', owner_id=1,
+ visible=True, public=True)
+
+ schedule = Schedule.objects.create(meeting=m, name='first1', owner_id=1,
+ visible=True, public=True, base=base_schedule)
m.schedule = schedule
m.save()
diff --git a/ietf/meeting/migrations/0032_add_schedule_base.py b/ietf/meeting/migrations/0032_add_schedule_base.py
new file mode 100644
index 000000000..2c41ebe68
--- /dev/null
+++ b/ietf/meeting/migrations/0032_add_schedule_base.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.0.13 on 2020-08-07 09:30
+
+from django.db import migrations
+import django.db.models.deletion
+import ietf.utils.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('meeting', '0031_add_session_origin'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='schedule',
+ name='base',
+ field=ietf.utils.models.ForeignKey(blank=True, help_text='Sessions scheduled in the base show up in this schedule.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='derivedschedule_set', to='meeting.Schedule'),
+ ),
+ migrations.AlterField(
+ model_name='schedule',
+ name='origin',
+ field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='meeting.Schedule'),
+ ),
+ ]
diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py
index 44dd3a27c..24f3832ea 100644
--- a/ietf/meeting/models.py
+++ b/ietf/meeting/models.py
@@ -291,7 +291,9 @@ class Meeting(models.Model):
min_time = datetime.datetime(1970, 1, 1, 0, 0, 0) # should be Meeting.modified, but we don't have that
timeslots_updated = self.timeslot_set.aggregate(Max('modified'))["modified__max"] or min_time
sessions_updated = self.session_set.aggregate(Max('modified'))["modified__max"] or min_time
- assignments_updated = (self.schedule.assignments.aggregate(Max('modified'))["modified__max"] or min_time) if self.schedule else min_time
+ assignments_updated = min_time
+ if self.schedule:
+ assignments_updated = SchedTimeSessAssignment.objects.filter(schedule__in=[self.schedule, self.schedule.base if self.schedule else None]).aggregate(Max('modified'))["modified__max"] or min_time
ts = max(timeslots_updated, sessions_updated, assignments_updated)
tz = pytz.timezone(settings.PRODUCTION_TIMEZONE)
ts = tz.localize(ts)
@@ -450,7 +452,7 @@ class TimeSlot(models.Model):
@property
def session(self):
if not hasattr(self, "_session_cache"):
- self._session_cache = self.sessions.filter(timeslotassignments__schedule=self.meeting.schedule).first()
+ self._session_cache = self.sessions.filter(timeslotassignments__schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting else None]).first()
return self._session_cache
@property
@@ -626,7 +628,10 @@ class Schedule(models.Model):
public = models.BooleanField(default=True, help_text="Allow others to see this agenda.")
badness = models.IntegerField(null=True, blank=True)
notes = models.TextField(blank=True)
- origin = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL)
+ origin = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL, related_name="+")
+ base = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL,
+ help_text="Sessions scheduled in the base schedule show up in this schedule too.", related_name="derivedschedule_set",
+ limit_choices_to={'base': None}) # prevent the inheritance from being more than one layer deep (no recursion)
def __str__(self):
return u"%s:%s(%s)" % (self.meeting, self.name, self.owner)
@@ -1047,7 +1052,7 @@ class Session(models.Model):
ss0name = "(%s)" % SessionStatusName.objects.get(slug=status_id).name
else:
ss0name = "(unscheduled)"
- ss = self.timeslotassignments.filter(schedule=self.meeting.schedule).order_by('timeslot__time')
+ ss = self.timeslotassignments.filter(schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting.schedule else None]).order_by('timeslot__time')
if ss:
ss0name = ','.join(x.timeslot.time.strftime("%a-%H%M") for x in ss)
return "%s: %s %s %s" % (self.meeting, self.group.acronym, self.name, ss0name)
@@ -1080,11 +1085,8 @@ class Session(models.Model):
def reverse_constraints(self):
return Constraint.objects.filter(target=self.group, meeting=self.meeting).order_by('name__name')
- def timeslotassignment_for_schedule(self, schedule):
- return self.timeslotassignments.filter(schedule=schedule).first()
-
def official_timeslotassignment(self):
- return self.timeslotassignment_for_schedule(self.meeting.schedule)
+ return self.timeslotassignments.filter(schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting.schedule else None]).first()
def constraints_dict(self, host_scheme):
constraint_list = []
diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py
index f15bc3dfe..a9b8535c3 100644
--- a/ietf/meeting/test_data.py
+++ b/ietf/meeting/test_data.py
@@ -76,8 +76,9 @@ def make_meeting_test_data(meeting=None):
if not meeting:
meeting = Meeting.objects.get(number="72", type="ietf")
- schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-schedule", visible=True, public=True)
- unofficial_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-unofficial-schedule", visible=True, public=True)
+ base_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="base", visible=True, public=True)
+ schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-schedule", visible=True, public=True, base=base_schedule)
+ unofficial_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-unofficial-schedule", visible=True, public=True, base=base_schedule)
# test room
pname = RoomResourceName.objects.create(name='projector',slug='proj')
@@ -146,7 +147,7 @@ def make_meeting_test_data(meeting=None):
requested_duration=datetime.timedelta(minutes=480),
type_id="reg")
SchedulingEvent.objects.create(session=reg_session, status_id='schedw', by=system_person)
- SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=schedule)
+ SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=base_schedule)
# Break
break_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"),
@@ -154,7 +155,7 @@ def make_meeting_test_data(meeting=None):
requested_duration=datetime.timedelta(minutes=30),
type_id="break")
SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person)
- SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=schedule)
+ SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=base_schedule)
meeting.schedule = schedule
meeting.save()
diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py
index 879dd1d73..09e5f449a 100644
--- a/ietf/meeting/tests_js.py
+++ b/ietf/meeting/tests_js.py
@@ -133,6 +133,8 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email()))
self.driver.get(url)
+ WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.edit-meeting-schedule')))
+
self.assertEqual(len(self.driver.find_elements_by_css_selector('.session')), 3)
# select - show session info
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index 51493ecc8..ab1d4cf55 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -127,6 +127,8 @@ class MeetingTests(TestCase):
future_meeting = Meeting.objects.create(date=datetime.date(future_year, 7, 22), number=future_num, type_id='ietf',
city="Panama City", country="PA", time_zone='America/Panama')
+ registration_text = "Registration"
+
# utc
time_interval = "%s-%s" % (slot.utc_start_time().strftime("%H:%M").lstrip("0"), (slot.utc_start_time() + slot.duration).strftime("%H:%M").lstrip("0"))
@@ -152,6 +154,7 @@ class MeetingTests(TestCase):
self.assertIn(session.group.parent.acronym.upper(), agenda_content)
self.assertIn(slot.location.name, agenda_content)
self.assertIn(time_interval, agenda_content)
+ self.assertIn(registration_text, agenda_content)
# Make sure there's a frame for the agenda and it points to the right place
self.assertTrue(any([session.materials.get(type='agenda').get_href() in x.attrib["data-src"] for x in q('tr div.modal-body div.frame')]))
@@ -191,6 +194,7 @@ class MeetingTests(TestCase):
self.assertContains(r, session.group.name)
self.assertContains(r, session.group.parent.acronym.upper())
self.assertContains(r, slot.location.name)
+ self.assertContains(r, registration_text)
self.assertContains(r, session.materials.get(type='agenda').uploaded_filename)
self.assertContains(r, session.materials.filter(type='slides').exclude(states__type__slug='slides',states__slug='deleted').first().uploaded_filename)
@@ -215,6 +219,7 @@ class MeetingTests(TestCase):
self.assertNotContains(r, 'CANCELLED')
self.assertContains(r, session.group.acronym)
self.assertContains(r, slot.location.name)
+ self.assertContains(r, registration_text)
# week view with a cancelled session
SchedulingEvent.objects.create(
@@ -585,16 +590,22 @@ class MeetingTests(TestCase):
self.client.login(username='secretary',password='secretary+password')
response = self.client.get(url)
self.assertEqual(response.status_code,200)
+
+ new_base = Schedule.objects.create(name="newbase", owner=schedule.owner, meeting=schedule.meeting)
response = self.client.post(url, {
'name':schedule.name,
'visible':True,
'public':True,
+ 'notes': "New Notes",
+ 'base': new_base.pk,
}
)
- self.assertEqual(response.status_code,302)
- schedule = Schedule.objects.get(pk=schedule.pk)
+ self.assertNoFormPostErrors(response)
+ schedule.refresh_from_db()
self.assertTrue(schedule.visible)
self.assertTrue(schedule.public)
+ self.assertEqual(schedule.notes, "New Notes")
+ self.assertEqual(schedule.base_id, new_base.pk)
def test_agenda_by_type_ics(self):
session=SessionFactory(meeting__type_id='ietf',type_id='lead')
@@ -985,7 +996,21 @@ class EditTests(TestCase):
person=p,
name=ConstraintName.objects.get(slug="bethere"),
)
-
+
+ room = Room.objects.get(meeting=meeting, session_types='regular')
+ base_timeslot = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room,
+ duration=datetime.timedelta(minutes=50),
+ time=datetime.datetime.combine(meeting.date + datetime.timedelta(days=2), datetime.time(9, 30)))
+
+ timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('time'))
+
+ base_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="irg"),
+ attendees=20, requested_duration=datetime.timedelta(minutes=30),
+ type_id='regular')
+ SchedulingEvent.objects.create(session=base_session, status_id='schedw', by=Person.objects.get(user__username='secretary'))
+ SchedTimeSessAssignment.objects.create(timeslot=base_timeslot, session=base_session, schedule=meeting.schedule.base)
+
+
# check we have the grid and everything set up as a baseline -
# the Javascript tests check that the Javascript can work with
# it
@@ -993,11 +1018,9 @@ class EditTests(TestCase):
r = self.client.get(url)
q = PyQuery(r.content)
- room = Room.objects.get(meeting=meeting, session_types='regular')
self.assertTrue(q(".room-name:contains(\"{}\")".format(room.name)))
self.assertTrue(q(".room-name:contains(\"{}\")".format(room.capacity)))
- timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular'))
self.assertTrue(q("#timeslot{}".format(timeslots[0].pk)))
for s in [s1, s2]:
@@ -1031,12 +1054,14 @@ class EditTests(TestCase):
if s.comments:
self.assertIn(s.comments, e.find(".comments").text())
- formatted_constraints = e.find(".session-info .formatted-constraints > *")
- if s == s1:
- self.assertIn(s_other.group.acronym, formatted_constraints.eq(0).html())
- self.assertIn(p.name, formatted_constraints.eq(1).html())
- elif s == s2:
- self.assertIn(p.name, formatted_constraints.eq(0).html())
+ formatted_constraints1 = q("#session{} .session-info .formatted-constraints > *".format(s1.pk))
+ self.assertIn(s2.group.acronym, formatted_constraints1.eq(0).html())
+ self.assertIn(p.name, formatted_constraints1.eq(1).html())
+
+ formatted_constraints2 = q("#session{} .session-info .formatted-constraints > *".format(s2.pk))
+ self.assertIn(p.name, formatted_constraints2.eq(0).html())
+
+ self.assertEqual(len(q("#session{}.readonly".format(base_session.pk))), 1)
self.assertTrue(q("em:contains(\"You can't edit this schedule\")"))
@@ -1106,7 +1131,7 @@ class EditTests(TestCase):
self.assertEqual(tombstone_event.status_id, 'resched')
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s_tombstone).timeslot, timeslots[1])
- self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.tombstone".format(s_tombstone.pk)).html())
+ self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.readonly".format(s_tombstone.pk)).html())
# unassign
r = self.client.post(url, {
@@ -1117,16 +1142,9 @@ class EditTests(TestCase):
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), [])
# try swapping days
- timeslots.append(TimeSlot.objects.create(
- meeting=meeting, type_id='regular', location=timeslots[0].location,
- duration=timeslots[0].duration - datetime.timedelta(minutes=5),
- time=timeslots[0].time + datetime.timedelta(days=1),
- ))
-
- SchedTimeSessAssignment.objects.create(schedule=schedule, session=s1, timeslot=timeslots[1])
-
- self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[0])), 1)
- self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[1])), 1)
+ SchedTimeSessAssignment.objects.create(schedule=schedule, session=s1, timeslot=timeslots[0])
+ self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1)
+ self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[1])), 1)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), [])
r = self.client.post(url, {
@@ -1138,8 +1156,8 @@ class EditTests(TestCase):
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[0])), [])
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), [])
- self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), [])
- self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[2])), 1)
+ self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[2])), 1)
+ self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2)), [])
# swap back
r = self.client.post(url, {
@@ -1149,37 +1167,59 @@ class EditTests(TestCase):
})
self.assertEqual(r.status_code, 302)
- self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[0])), 1)
+ self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), [])
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), [])
- def test_copy_meeting_schedule(self):
+ def test_new_meeting_schedule(self):
meeting = make_meeting_test_data()
self.client.login(username="secretary", password="secretary+password")
- url = urlreverse("ietf.meeting.views.copy_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
+ # new from scratch
+ url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
- # copy
r = self.client.post(url, {
- 'name': "newtest",
+ 'name': "scratch",
'public': "on",
- 'notes': "New test",
+ 'visible': "on",
+ 'notes': "New scratch",
+ 'base': meeting.schedule.base_id,
})
self.assertNoFormPostErrors(r)
- new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='newtest')
+ new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='scratch')
+ self.assertEqual(new_schedule.public, True)
+ self.assertEqual(new_schedule.visible, True)
+ self.assertEqual(new_schedule.notes, "New scratch")
+ self.assertEqual(new_schedule.origin, None)
+ self.assertEqual(new_schedule.base_id, meeting.schedule.base_id)
+
+ # copy
+ url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+
+ r = self.client.post(url, {
+ 'name': "copy",
+ 'public': "on",
+ 'notes': "New copy",
+ 'base': meeting.schedule.base_id,
+ })
+ self.assertNoFormPostErrors(r)
+
+ new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='copy')
self.assertEqual(new_schedule.public, True)
self.assertEqual(new_schedule.visible, False)
- self.assertEqual(new_schedule.notes, "New test")
+ self.assertEqual(new_schedule.notes, "New copy")
self.assertEqual(new_schedule.origin, meeting.schedule)
+ self.assertEqual(new_schedule.base_id, meeting.schedule.base_id)
old_assignments = {(a.session_id, a.timeslot_id) for a in SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule)}
for a in SchedTimeSessAssignment.objects.filter(schedule=new_schedule):
self.assertIn((a.session_id, a.timeslot_id), old_assignments)
- # FIXME: test extendedfrom is copied correctly
def test_save_agenda_as_and_read_permissions(self):
meeting = make_meeting_test_data()
@@ -1390,7 +1430,7 @@ class SessionDetailsTests(TestCase):
class EditScheduleListTests(TestCase):
def setUp(self):
self.mtg = MeetingFactory(type_id='ietf')
- ScheduleFactory(meeting=self.mtg,name='Empty-Schedule')
+ ScheduleFactory(meeting=self.mtg, name='secretary1')
def test_list_schedules(self):
url = urlreverse('ietf.meeting.views.list_schedules',kwargs={'num':self.mtg.number})
@@ -1423,8 +1463,8 @@ class EditScheduleListTests(TestCase):
)
# copy
- copy_url = urlreverse("ietf.meeting.views.copy_meeting_schedule", kwargs=dict(num=meeting.number, owner=from_schedule.owner_email(), name=from_schedule.name))
- r = self.client.post(copy_url, {
+ new_url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=from_schedule.owner_email(), name=from_schedule.name))
+ r = self.client.post(new_url, {
'name': "newtest",
'public': "on",
})
@@ -1436,22 +1476,20 @@ class EditScheduleListTests(TestCase):
edit_url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=to_schedule.owner_email(), name=to_schedule.name))
- # schedule
+ # schedule session
r = self.client.post(edit_url, {
'action': 'assign',
'timeslot': slot3.pk,
'session': session3.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
-
- # unschedule
+ # unschedule session
r = self.client.post(edit_url, {
'action': 'unassign',
'session': session1.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
-
- # move
+ # move session
r = self.client.post(edit_url, {
'action': 'assign',
'timeslot': slot2.pk,
@@ -1459,7 +1497,7 @@ class EditScheduleListTests(TestCase):
})
self.assertEqual(json.loads(r.content)['success'], True)
- # get differences
+ # now get differences
r = self.client.get(url, {
'from_schedule': from_schedule.name,
'to_schedule': to_schedule.name,
@@ -1584,6 +1622,7 @@ class InterimTests(TestCase):
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
meeting.time_zone = 'America/Los_Angeles'
meeting.save()
+
url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number})
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py
index 8a38f5655..4dcdc5ff6 100644
--- a/ietf/meeting/urls.py
+++ b/ietf/meeting/urls.py
@@ -41,7 +41,7 @@ type_ietf_only_patterns = [
url(r'^agenda/%(owner)s/%(schedule_name)s/session/(?P\d+).json$' % settings.URL_REGEXPS, ajax.assignment_json),
url(r'^agenda/%(owner)s/%(schedule_name)s/sessions.json$' % settings.URL_REGEXPS, ajax.assignments_json),
url(r'^agenda/%(owner)s/%(schedule_name)s.json$' % settings.URL_REGEXPS, ajax.schedule_infourl),
- url(r'^agenda/%(owner)s/%(schedule_name)s/copy/$' % settings.URL_REGEXPS, views.copy_meeting_schedule),
+ url(r'^agenda/%(owner)s/%(schedule_name)s/new/$' % settings.URL_REGEXPS, views.new_meeting_schedule),
url(r'^agenda/by-room$', views.agenda_by_room),
url(r'^agenda/by-type$', views.agenda_by_type),
url(r'^agenda/by-type/(?P[a-z]+)$', views.agenda_by_type),
@@ -49,6 +49,7 @@ type_ietf_only_patterns = [
url(r'^agendas/list$', views.list_schedules),
url(r'^agendas/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_schedules', permanent=True)),
url(r'^agendas/diff/$', views.diff_schedules),
+ url(r'^agenda/new/$', views.new_meeting_schedule),
url(r'^timeslots/edit$', views.edit_timeslots),
url(r'^timeslot/(?P\d+)/edittype$', views.edit_timeslot_type),
url(r'^rooms$', ajax.timeslot_roomsurl),
diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py
index a3888fd0e..d54eed3d5 100644
--- a/ietf/meeting/utils.py
+++ b/ietf/meeting/utils.py
@@ -28,7 +28,7 @@ from ietf.person.models import Person, Email
from ietf.secr.proceedings.proc_utils import import_audio_files
def session_time_for_sorting(session, use_meeting_date):
- official_timeslot = TimeSlot.objects.filter(sessionassignments__session=session, sessionassignments__schedule=session.meeting.schedule).first()
+ official_timeslot = TimeSlot.objects.filter(sessionassignments__session=session, sessionassignments__schedule__in=[session.meeting.schedule, session.meeting.schedule.base if session.meeting.schedule else None]).first()
if official_timeslot:
return official_timeslot.time
elif use_meeting_date and session.meeting.date:
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index dfb02c6e1..21f0d39df 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -144,7 +144,7 @@ def materials(request, num=None):
sessions = add_event_info_to_session_qs(Session.objects.filter(
meeting__number=meeting.number,
- timeslotassignments__schedule=schedule
+ timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None]
).distinct().select_related('meeting__schedule', 'group__state', 'group__parent'))
plenaries = sessions.filter(name__icontains='plenary')
@@ -297,6 +297,8 @@ def schedule_create(request, num=None, owner=None, name=None):
newschedule = Schedule(name=savedname,
owner=request.user.person,
meeting=meeting,
+ base=schedule.base,
+ origin=schedule,
visible=False,
public=False)
@@ -354,14 +356,15 @@ def edit_timeslots(request, num=None):
"ts_list":ts_list,
})
-class CopyScheduleForm(forms.ModelForm):
+class NewScheduleForm(forms.ModelForm):
class Meta:
model = Schedule
- fields = ['name', 'visible', 'public', 'notes']
+ fields = ['name', 'visible', 'public', 'notes', 'base']
- def __init__(self, schedule, new_owner, *args, **kwargs):
- super(CopyScheduleForm, self).__init__(*args, **kwargs)
+ def __init__(self, meeting, schedule, new_owner, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.meeting = meeting
self.schedule = schedule
self.new_owner = new_owner
@@ -370,7 +373,7 @@ class CopyScheduleForm(forms.ModelForm):
name_suggestion = username
counter = 2
- existing_names = set(Schedule.objects.filter(meeting=schedule.meeting_id, owner=new_owner).values_list('name', flat=True))
+ existing_names = set(Schedule.objects.filter(meeting=meeting, owner=new_owner).values_list('name', flat=True))
while name_suggestion in existing_names:
name_suggestion = username + str(counter)
counter += 1
@@ -378,55 +381,68 @@ class CopyScheduleForm(forms.ModelForm):
self.fields['name'].initial = name_suggestion
self.fields['name'].label = "Name of new agenda"
+ self.fields['base'].queryset = self.fields['base'].queryset.filter(meeting=meeting)
+
+ if schedule:
+ self.fields['visible'].initial = schedule.visible
+ self.fields['public'].initial = schedule.public
+ self.fields['base'].queryset = self.fields['base'].queryset.exclude(pk=schedule.pk)
+ self.fields['base'].initial = schedule.base_id
+ else:
+ base = Schedule.objects.filter(meeting=meeting, name='base').first()
+ if base:
+ self.fields['base'].initial = base.pk
+
def clean_name(self):
name = self.cleaned_data.get('name')
- if name and Schedule.objects.filter(meeting=self.schedule.meeting_id, owner=self.new_owner, name=name):
+ if name and Schedule.objects.filter(meeting=self.meeting, owner=self.new_owner, name=name):
raise forms.ValidationError("Schedule with this name already exists.")
return name
@role_required('Area Director','Secretariat')
-def copy_meeting_schedule(request, num, owner, name):
+def new_meeting_schedule(request, num, owner=None, name=None):
meeting = get_meeting(num)
- schedule = get_object_or_404(meeting.schedule_set, owner__email__address=owner, name=name)
+ schedule = get_schedule_by_name(meeting, get_person_by_email(owner), name)
if request.method == 'POST':
- form = CopyScheduleForm(schedule, request.user.person, request.POST)
+ form = NewScheduleForm(meeting, schedule, request.user.person, request.POST)
if form.is_valid():
new_schedule = form.save(commit=False)
- new_schedule.meeting = schedule.meeting
+ new_schedule.meeting = meeting
new_schedule.owner = request.user.person
new_schedule.origin = schedule
new_schedule.save()
- # keep a mapping so that extendedfrom references can be chased
- old_pk_to_new_pk = {}
- extendedfroms = {}
- for assignment in schedule.assignments.all():
- extendedfrom_id = assignment.extendedfrom_id
+ if schedule:
+ # keep a mapping so that extendedfrom references can be chased
+ old_pk_to_new_pk = {}
+ extendedfroms = {}
+ for assignment in schedule.assignments.all():
+ extendedfrom_id = assignment.extendedfrom_id
- # clone by resetting primary key
- old_pk = assignment.pk
- assignment.pk = None
- assignment.schedule = new_schedule
- assignment.extendedfrom = None
- assignment.save()
+ # clone by resetting primary key
+ old_pk = assignment.pk
+ assignment.pk = None
+ assignment.schedule = new_schedule
+ assignment.extendedfrom = None
+ assignment.save()
- old_pk_to_new_pk[old_pk] = assignment.pk
- if extendedfrom_id is not None:
- extendedfroms[assignment.pk] = extendedfrom_id
+ old_pk_to_new_pk[old_pk] = assignment.pk
+ if extendedfrom_id is not None:
+ extendedfroms[assignment.pk] = extendedfrom_id
- for pk, extendedfrom_id in extendedfroms.values():
- if extendedfrom_id in old_pk_to_new_pk:
- SchedTimeSessAssignment.objects.filter(pk=pk).update(extendedfrom=old_pk_to_new_pk[extendedfrom_id])
+ for pk, extendedfrom_id in extendedfroms.values():
+ if extendedfrom_id in old_pk_to_new_pk:
+ SchedTimeSessAssignment.objects.filter(pk=pk).update(extendedfrom=old_pk_to_new_pk[extendedfrom_id])
# now redirect to this new schedule
return redirect(edit_meeting_schedule, meeting.number, new_schedule.owner_email(), new_schedule.name)
else:
- form = CopyScheduleForm(schedule, request.user.person)
+ form = NewScheduleForm(meeting, schedule, request.user.person)
- return render(request, "meeting/copy_meeting_schedule.html", {
+ return render(request, "meeting/new_meeting_schedule.html", {
'meeting': meeting,
'schedule': schedule,
'form': form,
@@ -462,7 +478,15 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
"hide_menu": True
}, status=403, content_type="text/html")
- assignments = get_all_assignments_from_schedule(schedule)
+ assignments = SchedTimeSessAssignment.objects.filter(
+ schedule__in=[schedule, schedule.base],
+ timeslot__location__isnull=False,
+ session__type='regular',
+ ).order_by('timeslot__time','timeslot__name')
+
+ assignments_by_session = defaultdict(list)
+ for a in assignments:
+ assignments_by_session[a.session_id].append(a)
rooms = meeting.room_set.filter(session_types__slug='regular').distinct().order_by("capacity")
@@ -483,7 +507,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups',
)
- timeslots_qs = meeting.timeslot_set.filter(type='regular').prefetch_related('type', 'sessions').order_by('location', 'time', 'name')
+ timeslots_qs = TimeSlot.objects.filter(meeting=meeting, type='regular').prefetch_related('type').order_by('location', 'time', 'name')
min_duration = min(t.duration for t in timeslots_qs)
max_duration = max(t.duration for t in timeslots_qs)
@@ -549,7 +573,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
s.other_sessions = [s_other for s_other in sessions_for_group.get(s.group_id) if s != s_other]
- s.is_tombstone = s.current_status in tombstone_states
+ s.readonly = s.current_status in tombstone_states or any(a.schedule_id != schedule.pk for a in assignments_by_session.get(s.pk, []))
if request.method == 'POST':
@@ -622,7 +646,9 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
source_day = swap_days_form.cleaned_data['source_day']
target_day = swap_days_form.cleaned_data['target_day']
- swap_meeting_schedule_timeslot_assignments(schedule, [ts for ts in timeslots_qs if ts.time.date() == source_day], [ts for ts in timeslots_qs if ts.time.date() == target_day], target_day - source_day)
+ source_timeslots = [ts for ts in timeslots_qs if ts.time.date() == source_day]
+ target_timeslots = [ts for ts in timeslots_qs if ts.time.date() == target_day]
+ swap_meeting_schedule_timeslot_assignments(schedule, source_timeslots, target_timeslots, target_day - source_day)
return HttpResponseRedirect(request.get_full_path())
@@ -670,10 +696,6 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
ts.session_assignments = []
timeslots_by_pk = {ts.pk: ts for ts in timeslots_qs}
- assignments_by_session = defaultdict(list)
- for a in assignments:
- assignments_by_session[a.session_id].append(a)
-
unassigned_sessions = []
for s in sessions:
assigned = False
@@ -798,7 +820,17 @@ def edit_schedule(request, num=None, owner=None, name=None):
})
-SchedulePropertiesForm = modelform_factory(Schedule, fields=['name', 'notes', 'visible', 'public'])
+class SchedulePropertiesForm(forms.ModelForm):
+ class Meta:
+ model = Schedule
+ fields = ['name', 'notes', 'visible', 'public', 'base']
+
+ def __init__(self, meeting, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.fields['base'].queryset = self.fields['base'].queryset.filter(meeting=meeting)
+ if self.instance.pk is not None:
+ self.fields['base'].queryset = self.fields['base'].queryset.exclude(pk=self.instance.pk)
@role_required('Area Director','Secretariat')
def edit_schedule_properties(request, num, owner, name):
@@ -816,14 +848,14 @@ def edit_schedule_properties(request, num, owner, name):
return HttpResponseForbidden("You may not edit this schedule")
if request.method == 'POST':
- form = SchedulePropertiesForm(instance=schedule, data=request.POST)
+ form = SchedulePropertiesForm(meeting, instance=schedule, data=request.POST)
if form.is_valid():
form.save()
if request.GET.get('next'):
return HttpResponseRedirect(request.GET.get('next'))
return redirect('ietf.meeting.views.edit_schedule', num=num, owner=owner, name=name)
else:
- form = SchedulePropertiesForm(instance=schedule)
+ form = SchedulePropertiesForm(meeting, instance=schedule)
return render(request, "meeting/properties_edit.html", {
"schedule": schedule,
@@ -842,7 +874,7 @@ def list_schedules(request, num):
schedules = Schedule.objects.filter(
meeting=meeting
- ).prefetch_related('owner', 'assignments', 'origin', 'origin__assignments').order_by('owner', '-name', '-public').distinct()
+ ).prefetch_related('owner', 'assignments', 'origin', 'origin__assignments', 'base').order_by('owner', '-name', '-public').distinct()
if not has_role(request.user, 'Secretariat'):
schedules = schedules.filter(Q(visible=True) | Q(owner=request.user.person))
@@ -859,7 +891,7 @@ def list_schedules(request, num):
if s.origin:
s.changes_from_origin = len(diff_meeting_schedules(s.origin, s))
- if s.pk == meeting.schedule_id:
+ if s in [meeting.schedule, meeting.schedule.base if meeting.schedule else None]:
official_schedules.append(s)
elif user_is_person(request.user, s.owner):
own_schedules.append(s)
@@ -869,13 +901,13 @@ def list_schedules(request, num):
other_private_schedules.append(s)
schedule_groups = [
- ("Official Agenda", official_schedules),
- ("Own Draft Agendas", own_schedules),
- ("Other Draft Agendas", other_public_schedules),
- ("Other Private Draft Agendas", other_private_schedules),
+ (official_schedules, False, "Official Agenda"),
+ (own_schedules, True, "Own Draft Agendas"),
+ (other_public_schedules, False, "Other Draft Agendas"),
+ (other_private_schedules, False, "Other Private Draft Agendas"),
]
- schedule_groups = [(label, sorted(l, reverse=True, key=lambda s: natural_sort_key(s.name))) for label, l in schedule_groups if l]
+ schedule_groups = [(sorted(l, reverse=True, key=lambda s: natural_sort_key(s.name)), own, *t) for l, own, *t in schedule_groups if l or own]
return render(request, "meeting/schedule_list.html", {
'meeting': meeting,
@@ -967,7 +999,11 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
return render(request, "meeting/no-"+base+ext, {'meeting':meeting }, content_type=mimetype[ext])
updated = meeting.updated()
- filtered_assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda'])
+ filtered_assignments = SchedTimeSessAssignment.objects.filter(
+ schedule__in=[schedule, schedule.base]
+ ).exclude(
+ timeslot__type__in=['lead', 'offagenda']
+ )
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
if ext == ".csv":
@@ -1095,10 +1131,15 @@ def agenda_by_room(request, num=None, name=None, owner=None):
else:
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
+
+ assignments = SchedTimeSessAssignment.objects.filter(
+ schedule__in=[schedule, schedule.base if schedule else None]
+ ).prefetch_related('timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent')
+
ss_by_day = OrderedDict()
- for day in schedule.assignments.dates('timeslot__time','day'):
+ for day in assignments.dates('timeslot__time','day'):
ss_by_day[day]=[]
- for ss in schedule.assignments.order_by('timeslot__location__functional_name','timeslot__location__name','timeslot__time'):
+ for ss in assignments.order_by('timeslot__location__functional_name','timeslot__location__name','timeslot__time'):
day = ss.timeslot.time.date()
ss_by_day[day].append(ss)
return render(request,"meeting/agenda_by_room.html",{"meeting":meeting,"schedule":schedule,"ss_by_day":ss_by_day})
@@ -1111,7 +1152,12 @@ def agenda_by_type(request, num=None, type=None, name=None, owner=None):
else:
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
- assignments = schedule.assignments.order_by('session__type__slug','timeslot__time','session__group__acronym')
+ assignments = SchedTimeSessAssignment.objects.filter(
+ schedule__in=[schedule, schedule.base if schedule else None]
+ ).prefetch_related(
+ 'timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent'
+ ).order_by('session__type__slug','timeslot__time','session__group__acronym')
+
if type:
assignments = assignments.filter(session__type__slug=type)
return render(request,"meeting/agenda_by_type.html",{"meeting":meeting,"schedule":schedule,"assignments":assignments})
@@ -1120,7 +1166,11 @@ def agenda_by_type(request, num=None, type=None, name=None, owner=None):
def agenda_by_type_ics(request,num=None,type=None):
meeting = get_meeting(num)
schedule = get_schedule(meeting)
- assignments = schedule.assignments.order_by('session__type__slug','timeslot__time')
+ assignments = SchedTimeSessAssignment.objects.filter(
+ schedule__in=[schedule, schedule.base if schedule else None]
+ ).prefetch_related(
+ 'timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent'
+ ).order_by('session__type__slug','timeslot__time')
if type:
assignments = assignments.filter(session__type__slug=type)
updated = meeting.updated()
@@ -1240,7 +1290,11 @@ def week_view(request, num=None, name=None, owner=None):
if not schedule:
raise Http404
- filtered_assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda'])
+ filtered_assignments = SchedTimeSessAssignment.objects.filter(
+ schedule__in=[schedule, schedule.base]
+ ).exclude(
+ timeslot__type__in=['lead','offagenda']
+ )
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
items = []
@@ -1309,7 +1363,11 @@ def room_view(request, num=None, name=None, owner=None):
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
- assignments = schedule.assignments.all()
+ assignments = SchedTimeSessAssignment.objects.filter(
+ schedule__in=[schedule, schedule.base if schedule else None]
+ ).prefetch_related(
+ 'timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent'
+ )
unavailable = meeting.timeslot_set.filter(type__slug='unavail')
if not (assignments.exists() or unavailable.exists()):
return HttpResponse("No sessions/timeslots available yet")
@@ -1401,7 +1459,7 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
elif len(item) > 1 and item[0] == '~':
include_types |= set([item[1:]])
- assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda'])
+ assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base]).exclude(timeslot__type__in=['lead','offagenda'])
assignments = preprocess_assignments_for_agenda(assignments, meeting)
if q:
@@ -1427,13 +1485,17 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
}, content_type="text/calendar")
@cache_page(15 * 60)
-def json_agenda(request, num=None ):
+def json_agenda(request, num=None):
meeting = get_meeting(num, type_in=['ietf','interim'])
sessions = []
locations = set()
parent_acronyms = set()
- assignments = meeting.schedule.assignments.exclude(session__type__in=['lead','offagenda','break','reg'])
+ assignments = SchedTimeSessAssignment.objects.filter(
+ schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
+ ).exclude(
+ session__type__in=['lead','offagenda','break','reg']
+ )
# Update the assignments with historic information, i.e., valid at the
# time of the meeting
assignments = preprocess_assignments_for_agenda(assignments, meeting, extra_prefetches=[
@@ -1609,7 +1671,7 @@ def session_details(request, num, acronym):
session.historic_group.historic_parent = None
session.type_counter = Counter()
- ss = session.timeslotassignments.filter(schedule=meeting.schedule).order_by('timeslot__time')
+ ss = session.timeslotassignments.filter(schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]).order_by('timeslot__time')
if ss:
if meeting.type_id == 'interim' and not (meeting.city or meeting.country):
session.times = [ x.timeslot.utc_start_time() for x in ss ]
@@ -2320,15 +2382,23 @@ def make_schedule_official(request, num, owner, name):
schedule.public = True
schedule.visible = True
schedule.save()
+ if schedule.base and not (schedule.base.public and schedule.base.visible):
+ schedule.base.public = True
+ schedule.base.visible = True
+ schedule.base.save()
meeting.schedule = schedule
meeting.save()
return HttpResponseRedirect(reverse('ietf.meeting.views.list_schedules',kwargs={'num':num}))
if not schedule.public:
messages.warning(request,"This schedule will be made public as it is made official.")
-
if not schedule.visible:
messages.warning(request,"This schedule will be made visible as it is made official.")
+ if schedule.base:
+ if not schedule.base.public:
+ messages.warning(request,"The base schedule will be made public as it is made official.")
+ if not schedule.base.visible:
+ messages.warning(request,"The base schedule will be made visible as it is made official.")
return render(request, "meeting/make_schedule_official.html",
{ 'schedule' : schedule,
@@ -2344,12 +2414,14 @@ def delete_schedule(request, num, owner, name):
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
- if schedule.name=='Empty-Schedule':
- return HttpResponseForbidden('You may not delete the default empty schedule')
-
+ # FIXME: we ought to put these checks in a function and only show
+ # the delete button if the checks pass
if schedule == meeting.schedule:
return HttpResponseForbidden('You may not delete the official schedule for %s'%meeting)
+ if Schedule.objects.filter(base=schedule).exists():
+ return HttpResponseForbidden('You may not delete a schedule serving as the base for other schedules')
+
if not ( has_role(request.user, 'Secretariat') or person.user == request.user ):
return HttpResponseForbidden("You may not delete other user's schedules")
@@ -2660,10 +2732,12 @@ def interim_request_details(request, number):
return redirect(interim_pending)
first_session = sessions.first()
+ assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None])
return render(request, "meeting/interim_request_details.html", {
"meeting": meeting,
"sessions": sessions,
+ "assignments": assignments,
"group": first_session.group,
"requester": session_requested_by(first_session),
"session_status": current_session_status(first_session),
@@ -2783,10 +2857,10 @@ def upcoming_ical(request):
today = datetime.date.today()
# get meetings starting 7 days ago -- we'll filter out sessions in the past further down
- meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).order_by('date'))
+ meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).prefetch_related('schedule').order_by('date'))
assignments = list(SchedTimeSessAssignment.objects.filter(
- schedule__meeting__schedule=F('schedule'),
+ schedule__in=[m.schedule_id for m in meetings] + [m.schedule.base_id for m in meetings if m.schedule],
session__in=[s.pk for m in meetings for s in m.sessions],
timeslot__time__gte=today,
).order_by(
@@ -2879,7 +2953,7 @@ def proceedings(request, num=None):
sessions = add_event_info_to_session_qs(
Session.objects.filter(meeting__number=meeting.number)
).filter(
- Q(timeslotassignments__schedule=schedule) | Q(current_status='notmeet')
+ 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')
@@ -3101,7 +3175,7 @@ def edit_timeslot_type(request, num, slot_id):
else:
form = TimeSlotTypeForm(instance=timeslot)
- sessions = timeslot.sessions.filter(timeslotassignments__schedule=meeting.schedule)
+ sessions = timeslot.sessions.filter(timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None])
return render(request, 'meeting/edit_timeslot_type.html', {'timeslot':timeslot,'form':form,'sessions':sessions})
diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py
index 40d2ac9f4..526488a9e 100644
--- a/ietf/secr/meetings/tests.py
+++ b/ietf/secr/meetings/tests.py
@@ -65,7 +65,7 @@ class SecrMeetingTestCase(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
- self.assertEqual(len(q('#id_schedule_selector option')),3)
+ self.assertEqual({option.get('value') for option in q('#id_schedule_selector option:not([value=""])')}, {'base', 'test-schedule', 'test-unofficial-schedule'})
def test_add_meeting(self):
"Add Meeting"
@@ -92,6 +92,9 @@ class SecrMeetingTestCase(TestCase):
new_meeting = Meeting.objects.get(number=number)
self.assertTrue(new_meeting.schedule)
+ self.assertEqual(new_meeting.schedule.name, 'secretary1')
+ self.assertTrue(new_meeting.schedule.base)
+ self.assertEqual(new_meeting.schedule.base.name, 'base')
self.assertEqual(new_meeting.attendees, None)
def test_edit_meeting(self):
@@ -197,8 +200,7 @@ class SecrMeetingTestCase(TestCase):
# test delete
# first unschedule sessions so we can delete
- SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule).delete()
- SchedTimeSessAssignment.objects.filter(schedule=meeting.unofficial_schedule).delete()
+ SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base, meeting.unofficial_schedule]).delete()
self.client.login(username="secretary", password="secretary+password")
post_dict = {
'room-TOTAL_FORMS': q('input[name="room-TOTAL_FORMS"]').val(),
@@ -339,27 +341,29 @@ class SecrMeetingTestCase(TestCase):
def test_meetings_misc_session_delete(self):
meeting = make_meeting_test_data()
- slot = meeting.schedule.assignments.filter(timeslot__type='reg').first().timeslot
- url = reverse('ietf.secr.meetings.views.misc_session_delete', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id})
- target = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name})
+ schedule = meeting.schedule.base
+ slot = schedule.assignments.filter(timeslot__type='reg').first().timeslot
+ url = reverse('ietf.secr.meetings.views.misc_session_delete', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name,'slot_id':slot.id})
+ target = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name})
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response = self.client.post(url, {'post':'yes'})
self.assertRedirects(response, target)
- self.assertFalse(meeting.schedule.assignments.filter(timeslot=slot))
+ self.assertFalse(schedule.assignments.filter(timeslot=slot))
def test_meetings_misc_session_cancel(self):
meeting = make_meeting_test_data()
- slot = meeting.schedule.assignments.filter(timeslot__type='reg').first().timeslot
- url = reverse('ietf.secr.meetings.views.misc_session_cancel', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id})
- redirect_url = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name})
+ schedule = meeting.schedule.base
+ slot = schedule.assignments.filter(timeslot__type='reg').first().timeslot
+ url = reverse('ietf.secr.meetings.views.misc_session_cancel', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name,'slot_id':slot.id})
+ redirect_url = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name})
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response = self.client.post(url, {'post':'yes'})
self.assertRedirects(response, redirect_url)
- session = slot.sessionassignments.filter(schedule=meeting.schedule).first().session
+ session = slot.sessionassignments.filter(schedule=schedule).first().session
self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'canceled')
def test_meetings_regular_session_edit(self):
diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py
index 8e79efdb6..13c7c33f6 100644
--- a/ietf/secr/meetings/views.py
+++ b/ietf/secr/meetings/views.py
@@ -22,7 +22,6 @@ from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.meeting.utils import only_sessions_that_can_meet
from ietf.name.models import SessionStatusName
from ietf.group.models import Group, GroupEvent
-from ietf.person.models import Person
from ietf.secr.meetings.blue_sheets import create_blue_sheets
from ietf.secr.meetings.forms import ( BaseMeetingRoomFormSet, MeetingModelForm, MeetingSelectForm,
MeetingRoomForm, MiscSessionForm, TimeSlotForm, RegularSessionEditForm,
@@ -85,21 +84,19 @@ def check_misc_sessions(meeting,schedule):
Ensure misc session timeslots exist and have appropriate SchedTimeSessAssignment objects
for the specified schedule.
'''
+ # FIXME: this is a legacy function: delete it once base schedules are rolled out
+
+ if Schedule.objects.filter(meeting=meeting, base__isnull=False).exists():
+ return
+
slots = TimeSlot.objects.filter(meeting=meeting,type__in=('break','reg','other','plenary','lead','offagenda'))
plenary = slots.filter(type='plenary').first()
if plenary:
assignments = plenary.sessionassignments.all()
if not assignments.filter(schedule=schedule):
source = assignments.first().schedule
- copy_assignments(slots,source,schedule)
-
-def copy_assignments(slots,source,target):
- '''
- Copy SchedTimeSessAssignment objects from source schedule to target schedule. Slots is
- a queryset of slots
- '''
- for ss in SchedTimeSessAssignment.objects.filter(schedule=source,timeslot__in=slots):
- SchedTimeSessAssignment.objects.create(schedule=target,session=ss.session,timeslot=ss.timeslot)
+ for ss in SchedTimeSessAssignment.objects.filter(schedule=source,timeslot__in=slots):
+ SchedTimeSessAssignment.objects.create(schedule=schedule,session=ss.session,timeslot=ss.timeslot)
def get_last_meeting(meeting):
last_number = int(meeting.number) - 1
@@ -221,13 +218,23 @@ def add(request):
if form.is_valid():
meeting = form.save()
+ base_schedule = Schedule.objects.create(
+ meeting=meeting,
+ name='base',
+ owner=request.user.person,
+ visible=True,
+ public=True
+ )
+
schedule = Schedule.objects.create(meeting = meeting,
- name = 'Empty-Schedule',
- owner = Person.objects.get(name='(System)'),
+ name = "{}1".format(request.user.username),
+ owner = request.user.person,
visible = True,
- public = True)
+ public = True,
+ base = base_schedule,
+ )
meeting.schedule = schedule
-
+
# we want to carry session request lock status over from previous meeting
previous_meeting = get_meeting( int(meeting.number) - 1 )
meeting.session_request_lock_message = previous_meeting.session_request_lock_message
@@ -296,7 +303,7 @@ def blue_sheet_generate(request, meeting_id):
# TODO: Why aren't 'ag' in here as well?
groups = Group.objects.filter(
type__in=['wg','rg'],
- session__timeslotassignments__schedule=meeting.schedule).order_by('acronym')
+ session__timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]).order_by('acronym')
create_blue_sheets(meeting, groups)
messages.success(request, 'Blue Sheets generated')
@@ -380,7 +387,7 @@ def misc_sessions(request, meeting_id, schedule_name):
check_misc_sessions(meeting,schedule)
misc_session_types = ['break','reg','other','plenary','lead']
- assignments = schedule.assignments.filter(timeslot__type__in=misc_session_types)
+ assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base], timeslot__type__in=misc_session_types)
assignments = assignments.order_by('-timeslot__type__name','timeslot__time')
if request.method == 'POST':
@@ -571,7 +578,7 @@ def notifications(request, meeting_id):
meeting = get_object_or_404(Meeting, number=meeting_id)
last_notice = GroupEvent.objects.filter(type='sent_notification').first()
groups = set()
- for ss in meeting.schedule.assignments.filter(timeslot__type='regular'):
+ for ss in SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None], timeslot__type='regular'):
last_notice = ss.session.group.latest_event(type='sent_notification')
if last_notice and ss.modified > last_notice.time:
groups.add(ss.session.group)
@@ -652,7 +659,7 @@ def regular_sessions(request, meeting_id, schedule_name):
schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name)
sessions = add_event_info_to_session_qs(
- only_sessions_that_can_meet(schedule.meeting.session_set)
+ only_sessions_that_can_meet(meeting.session_set)
).order_by('group__acronym')
if request.method == 'POST':
diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py
index 9e49c7ac8..d6dfce910 100644
--- a/ietf/secr/proceedings/proc_utils.py
+++ b/ietf/secr/proceedings/proc_utils.py
@@ -32,11 +32,10 @@ VIDEO_TITLE_RE = re.compile(r'IETF(?P[\d]+)-(?P.*)-(?P\d{8})
def _get_session(number,name,date,time):
'''Lookup session using data from video title'''
meeting = Meeting.objects.get(number=number)
- schedule = meeting.schedule
timeslot_time = datetime.datetime.strptime(date + time,'%Y%m%d%H%M')
try:
assignment = SchedTimeSessAssignment.objects.get(
- schedule = schedule,
+ schedule__in = [meeting.schedule, meeting.schedule.base],
session__group__acronym = name.lower(),
timeslot__time = timeslot_time,
)
@@ -108,7 +107,7 @@ def get_timeslot_for_filename(filename):
meeting=meeting,
location__name=room_mapping[match.groupdict()['room']],
time=time,
- sessionassignments__schedule=meeting.schedule,
+ sessionassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
).distinct()
uncancelled_slots = [t for t in slots if not add_event_info_to_session_qs(t.sessions.all()).filter(current_status='canceled').exists()]
return uncancelled_slots[0]
diff --git a/ietf/secr/proceedings/views.py b/ietf/secr/proceedings/views.py
index 916118217..c1fa06cb5 100644
--- a/ietf/secr/proceedings/views.py
+++ b/ietf/secr/proceedings/views.py
@@ -232,9 +232,12 @@ def recording(request, meeting_num):
session.
'''
meeting = get_object_or_404(Meeting, number=meeting_num)
- assignments = meeting.schedule.assignments.exclude(session__type__in=('reg','break')).order_by('session__group__acronym')
- sessions = [ x.session for x in assignments ]
-
+ sessions = Session.objects.filter(
+ timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
+ ).exclude(
+ type__in=['reg','break']
+ ).order_by('group__acronym')
+
if request.method == 'POST':
form = RecordingForm(request.POST,meeting=meeting)
if form.is_valid():
diff --git a/ietf/secr/static/secr/css/custom.css b/ietf/secr/static/secr/css/custom.css
index 94cbf48cb..c3cc4078a 100644
--- a/ietf/secr/static/secr/css/custom.css
+++ b/ietf/secr/static/secr/css/custom.css
@@ -461,6 +461,11 @@ input.draft-file-input {
Meeting Tool
========================================================================== */
+#misc-sessions .from-base-schedule {
+ text-align: centeR;
+ opacity: 0.7;
+}
+
#misc-session-edit-form input[type="text"] {
width: 30em;
}
diff --git a/ietf/secr/templates/meetings/misc_sessions.html b/ietf/secr/templates/meetings/misc_sessions.html
index cb2545ae8..c946c338e 100644
--- a/ietf/secr/templates/meetings/misc_sessions.html
+++ b/ietf/secr/templates/meetings/misc_sessions.html
@@ -33,13 +33,17 @@
{{ assignment.timeslot.location }} |
{{ assignment.timeslot.show_location }} |
{{ assignment.timeslot.type }} |
- Edit |
-
- {% if not assignment.session.type.slug == "break" %}
- Cancel
- {% endif %}
- |
- Delete |
+ {% if assignment.schedule_id == schedule.pk %}
+ Edit |
+
+ {% if assignment.session.type.slug != "break" %}
+ Cancel
+ {% endif %}
+ |
+ Delete |
+ {% else %}
+ (from base schedule) |
+ {% endif %}
{% endfor %}
diff --git a/ietf/secr/utils/meeting.py b/ietf/secr/utils/meeting.py
index d3e41831e..63c4a6dcc 100644
--- a/ietf/secr/utils/meeting.py
+++ b/ietf/secr/utils/meeting.py
@@ -52,7 +52,7 @@ def get_session(timeslot, schedule=None):
# todo, doesn't account for shared timeslot
if not schedule:
schedule = timeslot.meeting.schedule
- qs = timeslot.sessions.filter(timeslotassignments__schedule=schedule) #.exclude(states__slug='deleted')
+ qs = timeslot.sessions.filter(timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None]) #.exclude(states__slug='deleted')
if qs:
return qs[0]
else:
@@ -66,7 +66,7 @@ def get_timeslot(session, schedule=None):
'''
if not schedule:
schedule = session.meeting.schedule
- ss = session.timeslotassignments.filter(schedule=schedule)
+ ss = session.timeslotassignments.filter(schedule__in=[schedule, schedule.base if schedule else None])
if ss:
return ss[0].timeslot
else:
diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css
index 8bde6d099..8870f923c 100644
--- a/ietf/static/ietf/css/ietf.css
+++ b/ietf/static/ietf/css/ietf.css
@@ -1010,6 +1010,9 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
margin-left: 0.2em;
}
+.from-base-schedule {
+ opacity: 0.7;
+}
/* === Edit Meeting Schedule ====================================== */
@@ -1138,7 +1141,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
cursor: default;
}
-.edit-meeting-schedule .session.tombstone {
+.edit-meeting-schedule .session.readonly {
cursor: default;
background-color: #ddd;
}
diff --git a/ietf/static/ietf/js/edit-meeting-schedule.js b/ietf/static/ietf/js/edit-meeting-schedule.js
index 82f531d5c..75f307199 100644
--- a/ietf/static/ietf/js/edit-meeting-schedule.js
+++ b/ietf/static/ietf/js/edit-meeting-schedule.js
@@ -8,7 +8,7 @@ jQuery(document).ready(function () {
alert("Error: " + errorText);
}
- let sessions = content.find(".session").not(".tombstone");
+ let sessions = content.find(".session").not(".readonly");
let timeslots = content.find(".timeslot");
let days = content.find(".day-flow .day");
diff --git a/ietf/templates/meeting/agenda_by_room.html b/ietf/templates/meeting/agenda_by_room.html
index 5d67c8ec2..2e0a68bd7 100644
--- a/ietf/templates/meeting/agenda_by_room.html
+++ b/ietf/templates/meeting/agenda_by_room.html
@@ -29,7 +29,7 @@ ul.sessionlist { list-style:none; padding-left:2em; margin-bottom:10px;}
{{room.grouper|default:"Location Unavailable"}}
{% for ss in room.list %}
- - {{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}
+ - {{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}
{% endfor %}
diff --git a/ietf/templates/meeting/agenda_by_type.html b/ietf/templates/meeting/agenda_by_type.html
index afe1eb462..84b4258ac 100644
--- a/ietf/templates/meeting/agenda_by_type.html
+++ b/ietf/templates/meeting/agenda_by_type.html
@@ -29,7 +29,7 @@ li.daylistentry { margin-left:2em; font-weight: 400; }
{% block content %}
{% include "meeting/meeting_heading.html" with updated=meeting.updated selected="by-type" title_extra="by Session Type" %}
-{% regroup assignments by session.type.slug as type_list %}
+{% regroup assignments by session.type_id as type_list %}
{% for type in type_list %}
-
@@ -41,11 +41,11 @@ li.daylistentry { margin-left:2em; font-weight: 400; }
{{ day.grouper }}
{% for ss in day.list %}
-
+
{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} |
{{ss.timeslot.get_hidden_location}} |
- {{ss.session.short_name}} |
- {% if ss.session.type_id == 'regular' or ss.session.type_id == 'plenary' or ss.session.type_id == 'other' %} Materials{% else %} {% endif %} |
+ {{ss.session.short_name}} |
+ {% if ss.session.type_id == 'regular' or ss.session.type_id == 'plenary' or ss.session.type_id == 'other' %} Materials{% else %} {% endif %} |
{% endfor %}
diff --git a/ietf/templates/meeting/edit_meeting_schedule.html b/ietf/templates/meeting/edit_meeting_schedule.html
index 71321a193..d32eb2654 100644
--- a/ietf/templates/meeting/edit_meeting_schedule.html
+++ b/ietf/templates/meeting/edit_meeting_schedule.html
@@ -30,7 +30,7 @@
·
{% endif %}
- Copy agenda
+ New agenda
·
Other Agendas
@@ -46,7 +46,7 @@
{% if not can_edit %}
·
- You can't edit this schedule. Take a copy first.
+ You can't edit this schedule. Make a new agenda from this.
{% endif %}
diff --git a/ietf/templates/meeting/edit_meeting_schedule_session.html b/ietf/templates/meeting/edit_meeting_schedule_session.html
index 01f6cce1e..a5a5d5950 100644
--- a/ietf/templates/meeting/edit_meeting_schedule_session.html
+++ b/ietf/templates/meeting/edit_meeting_schedule_session.html
@@ -1,4 +1,4 @@
-
+
{{ session.scheduling_label }}
diff --git a/ietf/templates/meeting/interim_announcement.txt b/ietf/templates/meeting/interim_announcement.txt
index be9608d66..d832b7be7 100644
--- a/ietf/templates/meeting/interim_announcement.txt
+++ b/ietf/templates/meeting/interim_announcement.txt
@@ -1,10 +1,10 @@
{% load ietf_filters %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW.
{% endif %}The {{ group.name }} ({{ group.acronym }}) {% if group.type.slug == 'wg' and group.state.slug == 'bof' %}BOF{% else %}{{group.type.name}}{% endif %} will hold
-{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ meeting.schedule.assignments.first.timeslot.time | date:"H:i" }} to {{ meeting.schedule.assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ meeting.schedule.assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ meeting.schedule.assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}.
+{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}.
{% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting.
-{% for assignment in meeting.schedule.assignments.all %}Session {{ forloop.counter }}:
+{% for assignment in assignments %}Session {{ forloop.counter }}:
{{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.utc_start_time | date:"H:i" }} to {{ assignment.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}
{% endfor %}{% endif %}
{% if meeting.city %}Meeting Location:
diff --git a/ietf/templates/meeting/interim_request_details.html b/ietf/templates/meeting/interim_request_details.html
index 9d3aaf0ac..66a3d8886 100644
--- a/ietf/templates/meeting/interim_request_details.html
+++ b/ietf/templates/meeting/interim_request_details.html
@@ -26,7 +26,7 @@
{{ meeting.country }}
Timezone
{{ meeting.time_zone }}
- {% for assignment in meeting.schedule.assignments.all %}
+ {% for assignment in assignments %}
Date
{{ assignment.timeslot.time|date:"Y-m-d" }}
diff --git a/ietf/templates/meeting/copy_meeting_schedule.html b/ietf/templates/meeting/new_meeting_schedule.html
similarity index 62%
rename from ietf/templates/meeting/copy_meeting_schedule.html
rename to ietf/templates/meeting/new_meeting_schedule.html
index 3b28fcc66..c9ace8cb4 100644
--- a/ietf/templates/meeting/copy_meeting_schedule.html
+++ b/ietf/templates/meeting/new_meeting_schedule.html
@@ -7,14 +7,14 @@
{% block content %}
{% origin %}
- {% block title %}Copy agenda {{ schedule.name }}{% endblock %}
+ {% block title %}{% if schedule %}Copy agenda {{ schedule.name }} to new agenda{% else %}New agenda{% endif %}{% endblock %}
{% endblock %}
diff --git a/ietf/templates/meeting/room-view.html b/ietf/templates/meeting/room-view.html
index ea577d5c8..c1fb43d8c 100644
--- a/ietf/templates/meeting/room-view.html
+++ b/ietf/templates/meeting/room-view.html
@@ -8,13 +8,13 @@
+{% endblock js %}
+
+
+{% block content %}
+ {% origin %}
+
+
+
+ Other Agendas
+
+
+
+ Meeting time slots and misc. sessions for agenda: {{ schedule.name }} {% if not can_edit %}(you do not have permission to edit time slots){% endif %}
+
+
+
+ {% for day in day_grid %}
+
+
+ {{ day.day|date:"l" }}
+ {{ day.day|date:"N j, Y" }}
+
+
+ {% for room, timeslots in day.room_timeslots %}
+
+
+ {{ room.name }}
+ {% if room.capacity %}{{ room.capacity }}{% endif %}
+
+
+
+ {% for t in timeslots %}
+
+ {% for s in t.assigned_sessions %}
+
+ {% if s.name %}
+ {{ s.name }}
+ {% if s.group %}
+ ({{ s.group.acronym }})
+ {% endif %}
+ {% elif s.group %}
+ {{ s.group.acronym }}
+ {% endif %}
+
+ {% empty %}
+ {% if t.type_id == 'regular' %}
+ (session)
+ {% elif t.name %}
+ {{ t.name }}
+ {% else %}
+ {{ t.type.name }}
+ {% endif %}
+ {% endfor %}
+ {{ t.time|date:"G:i" }}-{{ t.end_time|date:"G:i" }}
+
+ {% endfor %}
+
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+ {% include "meeting/edit_timeslot_form.html" with timeslot_form_action='add' timeslot_form=empty_timeslot_form %}
+
+
+
+
+
+
+ {% if edit_timeslot_form %}
+ {% include "meeting/edit_timeslot_form.html" with timeslot_form_action='edit' timeslot_form=edit_timeslot_form %}
+ {% elif add_timeslot_form %}
+ {% include "meeting/edit_timeslot_form.html" with timeslot_form_action='add' timeslot_form=add_timeslot_form %}
+ {% endif %}
+
+
+
+
+{% endblock %}
diff --git a/ietf/templates/meeting/edit_timeslot_form.html b/ietf/templates/meeting/edit_timeslot_form.html
new file mode 100644
index 000000000..efe6ca0c8
--- /dev/null
+++ b/ietf/templates/meeting/edit_timeslot_form.html
@@ -0,0 +1,41 @@
+{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
+{% load origin %}
+{% load bootstrap3 %}
+{% if not timeslot_form.active_assignment or timeslot_form.active_assignment.schedule_id == schedule.pk %}
+
+{% elif schedule.base %}
+ You cannot edit this session here - it is set up in the base schedule
+{% endif %}
diff --git a/ietf/templates/meeting/private_schedule.html b/ietf/templates/meeting/private_schedule.html
index de5362546..9aa647659 100644
--- a/ietf/templates/meeting/private_schedule.html
+++ b/ietf/templates/meeting/private_schedule.html
@@ -1,21 +1,15 @@
{% extends "base.html" %}
-{# Copyright The IETF Trust 2015, All Rights Reserved #}
+{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
{% load origin %}
{% block title %}IETF {{ meeting.number }} Meeting Agenda: {{ schedule.owner }} / {{ schedule.name }}{% endblock %}
-{% block morecss %}
- {% for area in area_list %}
- .{{ area.upcase_acronym}}-scheme, .meeting_event th.{{ area.upcase_acronym}}-scheme, #{{ area.upcase_acronym }}-groups, #selector-{{ area.upcase_acronym }} { color:{{ area.fg_color }}; background-color: {{ area.bg_color }} }
- {% endfor %}
-{% endblock morecss %}
-
{% block content %}
{% origin %}
diff --git a/ietf/templates/meeting/schedule_list.html b/ietf/templates/meeting/schedule_list.html
index 194b02ad1..c37c22745 100644
--- a/ietf/templates/meeting/schedule_list.html
+++ b/ietf/templates/meeting/schedule_list.html
@@ -8,12 +8,6 @@
{% block title %}Possible Meeting Agendas for IETF {{ meeting.number }}{% endblock %}
- {% comment %}
-
- {% endcomment %}
-
{% for schedules, own, label in schedule_groups %}
@@ -35,16 +29,12 @@
Notes |
Visible |
Public |
+
|
{% for schedule in schedules %}
- {{ schedule.name }}
- {% if schedule.can_edit_properties %}
-
-
-
- {% endif %}
+ {{ schedule.name }}
|
{{ schedule.owner }} |
@@ -55,7 +45,7 @@
|
{% if schedule.base %}
- {{ schedule.base.name }}
+ {{ schedule.base.name }}
{% endif %}
|
{{ schedule.notes|linebreaksbr }} |
@@ -73,6 +63,17 @@
private
{% endif %}
+
+ {% if schedule.can_edit_properties %}
+
+
+
+ {% endif %}
+
+
+
+
+ |
{% endfor %}
From 5ea30519da0dec081ee7ae7cc254401a26a25478 Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Tue, 18 Aug 2020 13:37:18 +0000
Subject: [PATCH 10/17] Add explanation to filter on regular sessions -
Legacy-Id: 18380
---
ietf/meeting/views.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 9bbe17072..d80ccb3d9 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -1035,8 +1035,13 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name
for r in rooms:
ts = []
for t in timeslots_by_day_and_room.get((d, r.pk), []):
+ # FIXME: the database (as of 2020) contains spurious
+ # regular time slots in rooms not intended for regular
+ # sessions - once those are gone, this filter can go
+ # away
if t.type_id == 'regular' and not any(t.slug == 'regular' for t in r.session_types.all()):
continue
+
t.assigned_sessions = []
for a in assignments_by_timeslot.get(t.pk, []):
s = sessions_by_pk.get(a.session_id)
From 2029fb74fa896a20dcc2ebb9ab8cab0cd482b752 Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Fri, 21 Aug 2020 12:15:52 +0000
Subject: [PATCH 11/17] Add private bit to time slot types so that it can be
configured in the admin instead of hardcoding the list of private sessions
types in the code. - Legacy-Id: 18402
---
ietf/meeting/views.py | 20 +++++++-------
ietf/name/fixtures/names.json | 9 +++++++
.../0018_add_timeslottypename_private.py | 26 +++++++++++++++++++
...y => 0019_add_rescheduled_session_name.py} | 2 +-
ietf/name/models.py | 1 +
5 files changed, 48 insertions(+), 10 deletions(-)
create mode 100644 ietf/name/migrations/0018_add_timeslottypename_private.py
rename ietf/name/migrations/{0018_add_rescheduled_session_name.py => 0019_add_rescheduled_session_name.py} (90%)
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index c47f9e190..8364ded5c 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -1344,9 +1344,8 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
updated = meeting.updated()
filtered_assignments = SchedTimeSessAssignment.objects.filter(
- schedule__in=[schedule, schedule.base]
- ).exclude(
- timeslot__type__in=['lead', 'offagenda']
+ schedule__in=[schedule, schedule.base],
+ timeslot__type__private=False,
)
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
@@ -1641,9 +1640,8 @@ def week_view(request, num=None, name=None, owner=None):
raise Http404
filtered_assignments = SchedTimeSessAssignment.objects.filter(
- schedule__in=[schedule, schedule.base]
- ).exclude(
- timeslot__type__in=['lead','offagenda']
+ schedule__in=[schedule, schedule.base],
+ timeslot__type__private=False,
)
# Only show assignments from the traditional meeting "week" (Sat-Fri).
# We'll determine this using the saturday before the first scheduled regular session.
@@ -1815,7 +1813,10 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
elif len(item) > 1 and item[0] == '~':
include_types |= set([item[1:]])
- assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base]).exclude(timeslot__type__in=['lead','offagenda'])
+ assignments = SchedTimeSessAssignment.objects.filter(
+ schedule__in=[schedule, schedule.base],
+ timeslot__type__private=False,
+ )
assignments = preprocess_assignments_for_agenda(assignments, meeting)
if q:
@@ -1848,9 +1849,10 @@ def agenda_json(request, num=None):
locations = set()
parent_acronyms = set()
assignments = SchedTimeSessAssignment.objects.filter(
- schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
+ schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
+ timeslot__type__private=False,
).exclude(
- session__type__in=['lead','offagenda','break','reg']
+ session__type__in=['break', 'reg']
)
# Update the assignments with historic information, i.e., valid at the
# time of the meeting
diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json
index d9d255eb1..28484eb60 100644
--- a/ietf/name/fixtures/names.json
+++ b/ietf/name/fixtures/names.json
@@ -12372,6 +12372,7 @@
"desc": "",
"name": "Break",
"order": 0,
+ "private": false,
"used": true
},
"model": "name.timeslottypename",
@@ -12382,6 +12383,7 @@
"desc": "Leadership Meetings",
"name": "Leadership",
"order": 0,
+ "private": true,
"used": true
},
"model": "name.timeslottypename",
@@ -12392,6 +12394,7 @@
"desc": "Other Meetings Not Published on Agenda",
"name": "Off Agenda",
"order": 0,
+ "private": true,
"used": true
},
"model": "name.timeslottypename",
@@ -12402,6 +12405,7 @@
"desc": "",
"name": "Other",
"order": 0,
+ "private": false,
"used": true
},
"model": "name.timeslottypename",
@@ -12412,6 +12416,7 @@
"desc": "",
"name": "Plenary",
"order": 0,
+ "private": false,
"used": true
},
"model": "name.timeslottypename",
@@ -12422,6 +12427,7 @@
"desc": "",
"name": "Registration",
"order": 0,
+ "private": false,
"used": true
},
"model": "name.timeslottypename",
@@ -12432,6 +12438,7 @@
"desc": "",
"name": "Regular",
"order": 0,
+ "private": false,
"used": true
},
"model": "name.timeslottypename",
@@ -12442,6 +12449,7 @@
"desc": "A room has been reserved for use by another body the timeslot indicated",
"name": "Room Reserved",
"order": 0,
+ "private": false,
"used": true
},
"model": "name.timeslottypename",
@@ -12452,6 +12460,7 @@
"desc": "A room was not booked for the timeslot indicated",
"name": "Room Unavailable",
"order": 0,
+ "private": false,
"used": true
},
"model": "name.timeslottypename",
diff --git a/ietf/name/migrations/0018_add_timeslottypename_private.py b/ietf/name/migrations/0018_add_timeslottypename_private.py
new file mode 100644
index 000000000..b28d3a12c
--- /dev/null
+++ b/ietf/name/migrations/0018_add_timeslottypename_private.py
@@ -0,0 +1,26 @@
+# Generated by Django 2.2.15 on 2020-08-20 02:40
+
+from django.db import migrations, models
+
+def set_private_bit_on_timeslottypename(apps, schema_editor):
+ TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
+
+ TimeSlotTypeName.objects.filter(slug__in=['lead', 'offagenda']).update(private=True)
+
+def noop(apps, schema_editor):
+ pass
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('name', '0017_update_constraintname_order_and_label'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='timeslottypename',
+ name='private',
+ field=models.BooleanField(default=False, help_text='Whether sessions of this type should be kept off the public agenda'),
+ ),
+ migrations.RunPython(set_private_bit_on_timeslottypename, noop),
+ ]
diff --git a/ietf/name/migrations/0018_add_rescheduled_session_name.py b/ietf/name/migrations/0019_add_rescheduled_session_name.py
similarity index 90%
rename from ietf/name/migrations/0018_add_rescheduled_session_name.py
rename to ietf/name/migrations/0019_add_rescheduled_session_name.py
index eec77270a..b99774211 100644
--- a/ietf/name/migrations/0018_add_rescheduled_session_name.py
+++ b/ietf/name/migrations/0019_add_rescheduled_session_name.py
@@ -6,7 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('name', '0017_update_constraintname_order_and_label'),
+ ('name', '0018_add_timeslottypename_private'),
]
def add_rescheduled_session_status_name(apps, schema_editor):
diff --git a/ietf/name/models.py b/ietf/name/models.py
index 648f07abd..a4d733034 100644
--- a/ietf/name/models.py
+++ b/ietf/name/models.py
@@ -66,6 +66,7 @@ class SessionStatusName(NameModel):
"""Waiting for Approval, Approved, Waiting for Scheduling, Scheduled, Cancelled, Disapproved"""
class TimeSlotTypeName(NameModel):
"""Session, Break, Registration, Other, Reserved, unavail"""
+ private = models.BooleanField(default=False, help_text="Whether sessions of this type should be kept off the public agenda")
class ConstraintName(NameModel):
"""conflict, conflic2, conflic3, bethere, timerange, time_relation, wg_adjacent"""
penalty = models.IntegerField(default=0, help_text="The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)")
From ca057911ea1d8053c7fc8045b3a81b322ee73714 Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Tue, 25 Aug 2020 14:56:40 +0000
Subject: [PATCH 12/17] Compute session order from available data instead of
going through the database again (saves ~2800 queries on the IETF 106 agenda
page) - Legacy-Id: 18414
---
ietf/meeting/helpers.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py
index 28a8b7670..04a3db3da 100644
--- a/ietf/meeting/helpers.py
+++ b/ietf/meeting/helpers.py
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
+from collections import defaultdict
import datetime
import io
import os
@@ -197,6 +198,11 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
if a.session.group and a.session.group not in groups:
groups.append(a.session.group)
+ sessions_for_groups = defaultdict(list)
+ for a in assignments:
+ if a.session and a.session.group:
+ sessions_for_groups[(a.session.group, a.session.type_id)].append(a)
+
group_replacements = find_history_replacements_active_at(groups, meeting_time)
parent_id_set = set()
@@ -210,7 +216,8 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
if a.session.historic_group.parent_id:
parent_id_set.add(a.session.historic_group.parent_id)
- a.session.order_number = a.session.order_in_meeting()
+ l = sessions_for_groups.get((a.session.group, a.session.type_id), [])
+ a.session.order_number = l.index(a) + 1 if a in l else 0
parents = Group.objects.filter(pk__in=parent_id_set)
parent_replacements = find_history_replacements_active_at(parents, meeting_time)
From 9df0994827115eae09e70e700acf838aea6368ef Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Wed, 26 Aug 2020 16:01:19 +0000
Subject: [PATCH 13/17] Add ical download/subscription for important dates for
meetings,
similar to the existing meeting view
- Legacy-Id: 18419
---
ietf/meeting/tests_views.py | 10 ++++++++
ietf/meeting/urls.py | 2 +-
ietf/meeting/views.py | 22 ++++++++++++++---
ietf/templates/meeting/important-dates.html | 11 ++++++---
ietf/templates/meeting/important_dates.ics | 26 +++++++++++++++++++++
5 files changed, 64 insertions(+), 7 deletions(-)
create mode 100644 ietf/templates/meeting/important_dates.ics
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index 92f6b4483..80cb85bb5 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -585,6 +585,16 @@ class MeetingTests(TestCase):
post_date = meeting.importantdate_set.get(name=idn).date
self.assertEqual(pre_date, post_date+datetime.timedelta(days=1))
+ def test_important_dates_ical(self):
+ meeting = MeetingFactory(type_id='ietf')
+ meeting.show_important_dates = True
+ meeting.save()
+ populate_important_dates(meeting)
+ url = urlreverse('ietf.meeting.views.important_dates', kwargs={'num': meeting.number, 'output_format': 'ics'})
+ r = self.client.get(url)
+ for d in meeting.importantdate_set.all():
+ self.assertContains(r, d.date.isoformat())
+
def test_group_ical(self):
meeting = make_meeting_test_data()
s1 = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py
index 21eafba56..c914a5ceb 100644
--- a/ietf/meeting/urls.py
+++ b/ietf/meeting/urls.py
@@ -104,7 +104,7 @@ type_ietf_only_patterns_id_optional = [
url(r'^proceedings/overview/$', views.proceedings_overview),
url(r'^proceedings/progress-report/$', views.proceedings_progress_report),
url(r'^important-dates/$', views.important_dates),
-
+ url(r'^important-dates.(?Pics)$', views.important_dates),
]
urlpatterns = [
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 8364ded5c..9f3fc251b 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -3582,7 +3582,7 @@ def api_upload_bluesheet(request):
return HttpResponse("Done", status=200, content_type='text/plain')
-def important_dates(request, num=None):
+def important_dates(request, num=None, output_format=None):
assert num is None or num.isdigit()
preview_roles = ['Area Director', 'Secretariat', 'IETF Chair', 'IAD', ]
@@ -3602,8 +3602,24 @@ def important_dates(request, num=None):
or (user and user.is_authenticated and has_role(user, preview_roles))):
meetings.append(future_meeting)
- context={'meetings':meetings}
- return render(request, 'meeting/important-dates.html', context)
+ if output_format == 'ics':
+ for m in meetings:
+ m.cached_updated = m.updated()
+ m.important_dates = m.importantdate_set.prefetch_related("name")
+ for d in m.important_dates:
+ d.midnight_cutoff = "UTC 23:59" in d.name.name
+
+ ics = render_to_string('meeting/important_dates.ics', {
+ 'meetings': meetings,
+ }, request=request)
+ # icalendar response file should have '\r\n' line endings per RFC5545
+ response = HttpResponse(re.sub("\r(?!\n)|(?Important Dates
+ Important Dates
+
+ iCalendar: webcal subscription
+ · download
+
+
{% for meeting in meetings %}
- IETF {{meeting.number}} : {{ meeting.date}}, {{meeting.city}}, {{meeting.country}}
+ IETF {{meeting.number}}: {{ meeting.date}}, {{meeting.city}}, {{meeting.country}}
{% with first=forloop.first %}
{% for d in meeting.importantdate_set.all %}
diff --git a/ietf/templates/meeting/important_dates.ics b/ietf/templates/meeting/important_dates.ics
new file mode 100644
index 000000000..13f82efe0
--- /dev/null
+++ b/ietf/templates/meeting/important_dates.ics
@@ -0,0 +1,26 @@
+{% load humanize %}{% autoescape off %}{% load ietf_filters %}BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:PUBLISH
+PRODID:-//IETF//datatracker.ietf.org ical importantdates//EN
+{% for meeting in meetings %}{% for d in meeting.important_dates %}BEGIN:VEVENT
+UID:ietf-{{ meeting.number }}-{{ d.name_id }}-{{ d.date.isoformat }}
+SUMMARY:IETF {{ meeting.number }}: {{ d.name.name }}
+CLASS:PUBLIC
+DTSTART{% if not d.midnight_cutoff %};VALUE=DATE{% endif %}:{{ d.date|date:"Ymd" }}{% if d.midnight_cutoff %}235900Z{% endif %}
+DTSTAMP:{{ meeting.cached_updated|date:"Ymd" }}T{{ meeting.cached_updated|date:"His" }}Z
+DESCRIPTION:{{ d.name.desc }} {% if first and d.name.slug == 'openreg' or first and d.name.slug == 'earlybird' %}
+Register here: https://www.ietf.org/how/meetings/register/{% endif %}{% if d.name.slug == 'opensched' %}
+To request a Working Group session, use the IETF Meeting Session Request Tool:
+{{ request.scheme }}://{{ request.get_host}}{% url 'ietf.secr.sreq.views.main' %}
+If you are working on a BoF request, it is highly recommended to tell the IESG
+now by sending an email to iesg@ietf.org to get advance help with the request.{% endif %}{% if d.name.slug == 'cutoffwgreq' %}
+To request a Working Group session, use the IETF Meeting Session Request Tool:
+{{ request.scheme }}://{{ request.get_host }}{% url 'ietf.secr.sreq.views.main' %}{% endif %}{% if d.name.slug == 'cutoffbofreq' %}
+To request a BOF, please see instructions on Requesting a BOF:
+https://www.ietf.org/how/bofs/bof-procedures/{% endif %}{% if d.name.slug == 'idcutoff' %}
+Upload using the ID Submission Tool:
+{{ request.scheme }}://{{ request.get_host }}{% url 'ietf.submit.views.upload_submission' %}{% endif %}{% if d.name.slug == 'draftwgagenda' or d.name.slug == 'revwgagenda' or d.name.slug == 'procsub' or d.name.slug == 'revslug' %}
+Upload using the Meeting Materials Management Tool:
+{{ request.scheme }}://{{ request.get_host }}{% url 'ietf.meeting.views.materials' num=meeting.number %}{% endif %}
+END:VEVENT
+{% endfor %}{% endfor %}END:VCALENDAR{% endautoescape %}
From 0aa0f7d4e2cdea9868091b1177464957d8bc867f Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Thu, 27 Aug 2020 16:35:38 +0000
Subject: [PATCH 14/17] Make the IPR search form initialize the state field
upon form initialization instead of evaluating the queryset upon importing
the module. This is probably never a problem in practice in this case in the
live environment, but it's a confusing practice, and when running the tests
sometimes a bug seems to throw Django off and the error is then shadowed by
Django crashing when trying to access the (non-existing) database. -
Legacy-Id: 18425
---
ietf/ipr/forms.py | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/ietf/ipr/forms.py b/ietf/ipr/forms.py
index 6898137e3..6a83cb191 100644
--- a/ietf/ipr/forms.py
+++ b/ietf/ipr/forms.py
@@ -24,12 +24,6 @@ from ietf.message.models import Message
from ietf.utils.fields import DatepickerDateField
from ietf.utils.text import dict_to_text
-# ----------------------------------------------------------------
-# Globals
-# ----------------------------------------------------------------
-STATE_CHOICES = [ (x.slug, x.name) for x in IprDisclosureStateName.objects.all() ]
-STATE_CHOICES.insert(0,('all','All States'))
-
# ----------------------------------------------------------------
# Base Classes
# ----------------------------------------------------------------
@@ -418,7 +412,7 @@ class ThirdPartyIprDisclosureForm(IprDisclosureFormBase):
return obj
class SearchForm(forms.Form):
- state = forms.MultipleChoiceField(choices=STATE_CHOICES,widget=forms.CheckboxSelectMultiple,required=False)
+ state = forms.MultipleChoiceField(choices=[], widget=forms.CheckboxSelectMultiple,required=False)
draft = forms.CharField(label="Draft name", max_length=128, required=False)
rfc = forms.IntegerField(label="RFC number", required=False)
holder = forms.CharField(label="Name of patent owner/applicant", max_length=128,required=False)
@@ -427,6 +421,10 @@ class SearchForm(forms.Form):
doctitle = forms.CharField(label="Words in document title", max_length=128,required=False)
iprtitle = forms.CharField(label="Words in IPR disclosure title", max_length=128,required=False)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['state'].choices = [('all','All States')] + [(n.pk, n.name) for n in IprDisclosureStateName.objects.all()]
+
class StateForm(forms.Form):
state = forms.ModelChoiceField(queryset=IprDisclosureStateName.objects,label="New State",empty_label=None)
comment = forms.CharField(required=False, widget=forms.Textarea, help_text="You may add a comment to be included in the disclosure history.", strip=False)
From 8759339e655cbb5531e833eaaf99a48e43b63ff9 Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Fri, 28 Aug 2020 12:28:29 +0000
Subject: [PATCH 15/17] Make the upcoming meetings iCal group the IETF meetings
into one block and add important dates. Also fix a couple of bugs found by
running the generated .ics through the icalendar.org validator. - Legacy-Id:
18434
---
ietf/meeting/tests_views.py | 18 ++++++++++----
ietf/meeting/utils.py | 8 +++++++
ietf/meeting/views.py | 24 ++++++++-----------
ietf/templates/meeting/important_dates.ics | 23 +-----------------
.../meeting/important_dates_for_meeting.ics | 22 +++++++++++++++++
ietf/templates/meeting/upcoming.ics | 16 ++++++++++---
6 files changed, 67 insertions(+), 44 deletions(-)
create mode 100644 ietf/templates/meeting/important_dates_for_meeting.ics
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index 80cb85bb5..579fd93d9 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -1931,7 +1931,6 @@ class InterimTests(TestCase):
make_meeting_test_data(create_interims=True)
url = urlreverse("ietf.meeting.views.upcoming")
today = datetime.date.today()
- add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first()
mars_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting
ames_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting
r = self.client.get(url)
@@ -1944,19 +1943,28 @@ class InterimTests(TestCase):
self.check_interim_tabs(url)
def test_upcoming_ical(self):
- make_meeting_test_data(create_interims=True)
+ meeting = make_meeting_test_data(create_interims=True)
+ populate_important_dates(meeting)
+
url = urlreverse("ietf.meeting.views.upcoming_ical")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
+
+ today = datetime.date.today()
+ mars_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting
+ ames_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting
+ self.assertContains(r, mars_interim.number)
+ self.assertContains(r, ames_interim.number)
+ self.assertContains(r, 'IETF 72')
self.assertEqual(r.get('Content-Type'), "text/calendar")
- self.assertEqual(r.content.count(b'UID'), 7)
+ self.assertEqual(r.content.count(b'UID'), 3 + meeting.importantdate_set.count())
+
# check filtered output
url = url + '?filters=mars'
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.get('Content-Type'), "text/calendar")
- # print r.content
- self.assertEqual(r.content.count(b'UID'), 2)
+ self.assertEqual(r.content.count(b'UID'), 2 + meeting.importantdate_set.count())
def test_upcoming_json(self):
diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py
index 15f97f880..27567541b 100644
--- a/ietf/meeting/utils.py
+++ b/ietf/meeting/utils.py
@@ -560,3 +560,11 @@ def swap_meeting_schedule_timeslot_assignments(schedule, source_timeslots, targe
if not swapped:
for a in lts_assignments:
a.delete()
+
+def preprocess_meeting_important_dates(meetings):
+ for m in meetings:
+ m.cached_updated = m.updated()
+ m.important_dates = m.importantdate_set.prefetch_related("name")
+ for d in m.important_dates:
+ d.midnight_cutoff = "UTC 23:59" in d.name.name
+
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 9f3fc251b..5320970c4 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -81,6 +81,7 @@ from ietf.meeting.utils import data_for_meetings_overview
from ietf.meeting.utils import preprocess_constraints_for_meeting_schedule_editor
from ietf.meeting.utils import diff_meeting_schedules, prefetch_schedule_diff_objects
from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments
+from ietf.meeting.utils import preprocess_meeting_important_dates
from ietf.message.utils import infer_message
from ietf.secr.proceedings.utils import handle_upload_file
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
@@ -3246,7 +3247,7 @@ def upcoming_ical(request):
assignments = list(SchedTimeSessAssignment.objects.filter(
schedule__in=[m.schedule_id for m in meetings] + [m.schedule.base_id for m in meetings if m.schedule],
- session__in=[s.pk for m in meetings for s in m.sessions],
+ session__in=[s.pk for m in meetings for s in m.sessions if m.type_id != 'ietf'],
timeslot__time__gte=today,
).order_by(
'schedule__meeting__date', 'session__type', 'timeslot__time'
@@ -3270,17 +3271,16 @@ def upcoming_ical(request):
a.session = sessions.get(a.session_id) or a.session
a.session.ical_status = ical_session_status(a)
- # gather vtimezones
- vtimezones = set()
- for meeting in meetings:
- if meeting.vtimezone():
- vtimezones.add(meeting.vtimezone())
- vtimezones = ''.join(vtimezones)
+ # handle IETFs separately
+ ietfs = [m for m in meetings if m.type_id == 'ietf']
+ preprocess_meeting_important_dates(ietfs)
# icalendar response file should have '\r\n' line endings per RFC5545
response = render_to_string('meeting/upcoming.ics', {
- 'vtimezones': vtimezones,
- 'assignments': assignments})
+ 'vtimezones': ''.join({meeting.vtimezone() for meeting in meetings if meeting.vtimezone()}),
+ 'assignments': assignments,
+ 'ietfs': ietfs,
+ }, request=request)
response = re.sub("\r(?!\n)|(?
Date: Fri, 28 Aug 2020 12:30:51 +0000
Subject: [PATCH 16/17] Fix quoting of TZID in the .ics according to the
validator at icalendar.org - Legacy-Id: 18435
---
ietf/templates/meeting/agenda.ics | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ietf/templates/meeting/agenda.ics b/ietf/templates/meeting/agenda.ics
index 296bc8095..d0a23972f 100644
--- a/ietf/templates/meeting/agenda.ics
+++ b/ietf/templates/meeting/agenda.ics
@@ -8,8 +8,8 @@ SUMMARY:{% if item.session.name %}{{item.session.name|ics_esc}}{% else %}{% if n
{% if item.timeslot.show_location %}LOCATION:{{item.timeslot.get_location}}
{% endif %}STATUS:{{item.session.ical_status}}
CLASS:PUBLIC
-DTSTART{% if schedule.meeting.time_zone %};TZID="{{schedule.meeting.time_zone}}"{%endif%}:{{ item.timeslot.time|date:"Ymd" }}T{{item.timeslot.time|date:"Hi"}}00
-DTEND{% if schedule.meeting.time_zone %};TZID="{{schedule.meeting.time_zone}}"{%endif%}:{{ item.timeslot.end_time|date:"Ymd" }}T{{item.timeslot.end_time|date:"Hi"}}00
+DTSTART{% if schedule.meeting.time_zone %};TZID={{schedule.meeting.time_zone|ics_esc}}{%endif%}:{{ item.timeslot.time|date:"Ymd" }}T{{item.timeslot.time|date:"Hi"}}00
+DTEND{% if schedule.meeting.time_zone %};TZID={{schedule.meeting.time_zone|ics_esc}}{%endif%}:{{ item.timeslot.end_time|date:"Ymd" }}T{{item.timeslot.end_time|date:"Hi"}}00
DTSTAMP:{{ item.timeslot.modified|date:"Ymd" }}T{{ item.timeslot.modified|date:"His" }}Z{% if item.session.agenda %}
URL:{{item.session.agenda.get_versionless_href}}{% endif %}
DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %}
From 72665a3f0e020d87285c423dd554403e6d421d8b Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Fri, 28 Aug 2020 12:59:11 +0000
Subject: [PATCH 17/17] Small clean up to remove the explicit support for
copying extendedfrom in the new meeting editor - extendedfrom's have not been
in use for years, and the new UI doesn't support making them (one should just
make another session instead or modify the timeslot, which is now possible to
do individually). - Legacy-Id: 18438
---
ietf/meeting/views.py | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 5320970c4..1129f641f 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -435,27 +435,13 @@ def new_meeting_schedule(request, num, owner=None, name=None):
new_schedule.save()
if schedule:
- # keep a mapping so that extendedfrom references can be chased
- old_pk_to_new_pk = {}
- extendedfroms = {}
for assignment in schedule.assignments.all():
- extendedfrom_id = assignment.extendedfrom_id
-
# clone by resetting primary key
- old_pk = assignment.pk
assignment.pk = None
assignment.schedule = new_schedule
assignment.extendedfrom = None
assignment.save()
- old_pk_to_new_pk[old_pk] = assignment.pk
- if extendedfrom_id is not None:
- extendedfroms[assignment.pk] = extendedfrom_id
-
- for pk, extendedfrom_id in extendedfroms.values():
- if extendedfrom_id in old_pk_to_new_pk:
- SchedTimeSessAssignment.objects.filter(pk=pk).update(extendedfrom=old_pk_to_new_pk[extendedfrom_id])
-
# now redirect to this new schedule
return redirect(edit_meeting_schedule, meeting.number, new_schedule.owner_email(), new_schedule.name)