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 @@