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
This commit is contained in:
Ole Laursen 2020-08-11 17:34:32 +00:00
parent 923cb35755
commit c78ffbcd18
30 changed files with 400 additions and 213 deletions

View file

@ -33,11 +33,11 @@ for meeting in Meeting.objects.filter(type="ietf").order_by("date"):
for schedule in meeting.schedule_set.all(): for schedule in meeting.schedule_set.all():
print " Checking for missing Break and Reg sessions in %s" % schedule print " Checking for missing Break and Reg sessions in %s" % schedule
for timeslot in meeting.timeslot_set.all(): for timeslot in meeting.timeslot_set.all():
if timeslot.type_id == 'break': if timeslot.type_id == 'break' and not (schedule.base and SchedTimeSessAssignment.objects.filter(timeslot=timeslot, session=brk, schedule=schedule.base).exists()):
assignment, created = ScheduleTimeslotSSessionAssignment.objects.get_or_create(timeslot=timeslot, session=brk, schedule=schedule) assignment, created = SchedTimeSessAssignment.objects.get_or_create(timeslot=timeslot, session=brk, schedule=schedule)
if created: if created:
print " Added %s break assignment" % timeslot print " Added %s break assignment" % timeslot
if timeslot.type_id == 'reg': if timeslot.type_id == 'reg' and not (schedule.base and SchedTimeSessAssignment.objects.filter(timeslot=timeslot, session=reg, schedule=schedule.base).exists()):
assignment, created = ScheduleTimeslotSSessionAssignment.objects.get_or_create(timeslot=timeslot, session=reg, schedule=schedule) assignment, created = SchedTimeSessAssignment.objects.get_or_create(timeslot=timeslot, session=reg, schedule=schedule)
if created: if created:
print " Added %s registration assignment" % timeslot print " Added %s registration assignment" % timeslot

View file

@ -126,8 +126,8 @@ admin.site.register(SchedulingEvent, SchedulingEventAdmin)
class ScheduleAdmin(admin.ModelAdmin): class ScheduleAdmin(admin.ModelAdmin):
list_display = ["name", "meeting", "owner", "visible", "public", "badness"] list_display = ["name", "meeting", "owner", "visible", "public", "badness"]
list_filter = ["meeting", ] list_filter = ["meeting"]
raw_id_fields = ["meeting", "owner", ] raw_id_fields = ["meeting", "owner", "origin", "base"]
search_fields = ["meeting__number", "name", "owner__name"] search_fields = ["meeting__number", "name", "owner__name"]
ordering = ["-meeting", "name"] ordering = ["-meeting", "name"]

View file

@ -143,13 +143,6 @@ def get_schedule(meeting, name=None):
schedule = get_object_or_404(meeting.schedule_set, name=name) schedule = get_object_or_404(meeting.schedule_set, name=name)
return schedule 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? # seems this belongs in ietf/person/utils.py?
def get_person_by_email(email): def get_person_by_email(email):
# email == None may actually match people who haven't set an 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() type = group.type.slug.upper()
if group.type.slug == 'wg' and group.state.slug == 'bof': if group.type.slug == 'wg' and group.state.slug == 'bof':
type = '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( initial['subject'] = '{name} ({acronym}) {type} {desc} Meeting: {date}{change}'.format(
name=group.name, name=group.name,
acronym=group.acronym, acronym=group.acronym,

View file

@ -85,8 +85,11 @@ class Command(BaseCommand):
date=datetime.date(2019, 11, 16), date=datetime.date(2019, 11, 16),
days=7, days=7,
) )
schedule = Schedule.objects.create(meeting=m, name='Empty-Schedule', owner_id=1, base_schedule = Schedule.objects.create(meeting=m, name='base', owner_id=1,
visible=True, public=True) 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.schedule = schedule
m.save() m.save()

View file

@ -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'),
),
]

View file

@ -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 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 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 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) ts = max(timeslots_updated, sessions_updated, assignments_updated)
tz = pytz.timezone(settings.PRODUCTION_TIMEZONE) tz = pytz.timezone(settings.PRODUCTION_TIMEZONE)
ts = tz.localize(ts) ts = tz.localize(ts)
@ -450,7 +452,7 @@ class TimeSlot(models.Model):
@property @property
def session(self): def session(self):
if not hasattr(self, "_session_cache"): 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 return self._session_cache
@property @property
@ -626,7 +628,10 @@ class Schedule(models.Model):
public = models.BooleanField(default=True, help_text="Allow others to see this agenda.") public = models.BooleanField(default=True, help_text="Allow others to see this agenda.")
badness = models.IntegerField(null=True, blank=True) badness = models.IntegerField(null=True, blank=True)
notes = models.TextField(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): def __str__(self):
return u"%s:%s(%s)" % (self.meeting, self.name, self.owner) 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 ss0name = "(%s)" % SessionStatusName.objects.get(slug=status_id).name
else: else:
ss0name = "(unscheduled)" 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: if ss:
ss0name = ','.join(x.timeslot.time.strftime("%a-%H%M") for x in 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) 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): def reverse_constraints(self):
return Constraint.objects.filter(target=self.group, meeting=self.meeting).order_by('name__name') 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): 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): def constraints_dict(self, host_scheme):
constraint_list = [] constraint_list = []

View file

@ -76,8 +76,9 @@ def make_meeting_test_data(meeting=None):
if not meeting: if not meeting:
meeting = Meeting.objects.get(number="72", type="ietf") meeting = Meeting.objects.get(number="72", type="ietf")
schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-schedule", visible=True, public=True) base_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="base", visible=True, public=True)
unofficial_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-unofficial-schedule", 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 # test room
pname = RoomResourceName.objects.create(name='projector',slug='proj') 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), requested_duration=datetime.timedelta(minutes=480),
type_id="reg") type_id="reg")
SchedulingEvent.objects.create(session=reg_session, status_id='schedw', by=system_person) 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
break_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"), 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), requested_duration=datetime.timedelta(minutes=30),
type_id="break") type_id="break")
SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person) 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.schedule = schedule
meeting.save() meeting.save()

View file

@ -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())) 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) 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) self.assertEqual(len(self.driver.find_elements_by_css_selector('.session')), 3)
# select - show session info # select - show session info

View file

@ -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', 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') city="Panama City", country="PA", time_zone='America/Panama')
registration_text = "Registration"
# utc # 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")) 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(session.group.parent.acronym.upper(), agenda_content)
self.assertIn(slot.location.name, agenda_content) self.assertIn(slot.location.name, agenda_content)
self.assertIn(time_interval, 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 # 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')])) 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.name)
self.assertContains(r, session.group.parent.acronym.upper()) self.assertContains(r, session.group.parent.acronym.upper())
self.assertContains(r, slot.location.name) 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.get(type='agenda').uploaded_filename)
self.assertContains(r, session.materials.filter(type='slides').exclude(states__type__slug='slides',states__slug='deleted').first().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.assertNotContains(r, 'CANCELLED')
self.assertContains(r, session.group.acronym) self.assertContains(r, session.group.acronym)
self.assertContains(r, slot.location.name) self.assertContains(r, slot.location.name)
self.assertContains(r, registration_text)
# week view with a cancelled session # week view with a cancelled session
SchedulingEvent.objects.create( SchedulingEvent.objects.create(
@ -585,16 +590,22 @@ class MeetingTests(TestCase):
self.client.login(username='secretary',password='secretary+password') self.client.login(username='secretary',password='secretary+password')
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code,200) self.assertEqual(response.status_code,200)
new_base = Schedule.objects.create(name="newbase", owner=schedule.owner, meeting=schedule.meeting)
response = self.client.post(url, { response = self.client.post(url, {
'name':schedule.name, 'name':schedule.name,
'visible':True, 'visible':True,
'public':True, 'public':True,
'notes': "New Notes",
'base': new_base.pk,
} }
) )
self.assertEqual(response.status_code,302) self.assertNoFormPostErrors(response)
schedule = Schedule.objects.get(pk=schedule.pk) schedule.refresh_from_db()
self.assertTrue(schedule.visible) self.assertTrue(schedule.visible)
self.assertTrue(schedule.public) 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): def test_agenda_by_type_ics(self):
session=SessionFactory(meeting__type_id='ietf',type_id='lead') session=SessionFactory(meeting__type_id='ietf',type_id='lead')
@ -985,7 +996,21 @@ class EditTests(TestCase):
person=p, person=p,
name=ConstraintName.objects.get(slug="bethere"), 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 - # check we have the grid and everything set up as a baseline -
# the Javascript tests check that the Javascript can work with # the Javascript tests check that the Javascript can work with
# it # it
@ -993,11 +1018,9 @@ class EditTests(TestCase):
r = self.client.get(url) r = self.client.get(url)
q = PyQuery(r.content) 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.name)))
self.assertTrue(q(".room-name:contains(\"{}\")".format(room.capacity))) 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))) self.assertTrue(q("#timeslot{}".format(timeslots[0].pk)))
for s in [s1, s2]: for s in [s1, s2]:
@ -1031,12 +1054,14 @@ class EditTests(TestCase):
if s.comments: if s.comments:
self.assertIn(s.comments, e.find(".comments").text()) self.assertIn(s.comments, e.find(".comments").text())
formatted_constraints = e.find(".session-info .formatted-constraints > *") formatted_constraints1 = q("#session{} .session-info .formatted-constraints > *".format(s1.pk))
if s == s1: self.assertIn(s2.group.acronym, formatted_constraints1.eq(0).html())
self.assertIn(s_other.group.acronym, formatted_constraints.eq(0).html()) self.assertIn(p.name, formatted_constraints1.eq(1).html())
self.assertIn(p.name, formatted_constraints.eq(1).html())
elif s == s2: formatted_constraints2 = q("#session{} .session-info .formatted-constraints > *".format(s2.pk))
self.assertIn(p.name, formatted_constraints.eq(0).html()) 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\")")) 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(tombstone_event.status_id, 'resched')
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s_tombstone).timeslot, timeslots[1]) 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 # unassign
r = self.client.post(url, { r = self.client.post(url, {
@ -1117,16 +1142,9 @@ class EditTests(TestCase):
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), []) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), [])
# try swapping days # try swapping days
timeslots.append(TimeSlot.objects.create( SchedTimeSessAssignment.objects.create(schedule=schedule, session=s1, timeslot=timeslots[0])
meeting=meeting, type_id='regular', location=timeslots[0].location, self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1)
duration=timeslots[0].duration - datetime.timedelta(minutes=5), self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[1])), 1)
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)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), []) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), [])
r = self.client.post(url, { 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[0])), [])
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), []) 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=s1, timeslot=timeslots[2])), 1)
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[2])), 1) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2)), [])
# swap back # swap back
r = self.client.post(url, { r = self.client.post(url, {
@ -1149,37 +1167,59 @@ class EditTests(TestCase):
}) })
self.assertEqual(r.status_code, 302) 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[1])), [])
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), []) 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() meeting = make_meeting_test_data()
self.client.login(username="secretary", password="secretary+password") 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) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
# copy
r = self.client.post(url, { r = self.client.post(url, {
'name': "newtest", 'name': "scratch",
'public': "on", 'public': "on",
'notes': "New test", 'visible': "on",
'notes': "New scratch",
'base': meeting.schedule.base_id,
}) })
self.assertNoFormPostErrors(r) 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.public, True)
self.assertEqual(new_schedule.visible, False) 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.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)} 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): for a in SchedTimeSessAssignment.objects.filter(schedule=new_schedule):
self.assertIn((a.session_id, a.timeslot_id), old_assignments) 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): def test_save_agenda_as_and_read_permissions(self):
meeting = make_meeting_test_data() meeting = make_meeting_test_data()
@ -1390,7 +1430,7 @@ class SessionDetailsTests(TestCase):
class EditScheduleListTests(TestCase): class EditScheduleListTests(TestCase):
def setUp(self): def setUp(self):
self.mtg = MeetingFactory(type_id='ietf') self.mtg = MeetingFactory(type_id='ietf')
ScheduleFactory(meeting=self.mtg,name='Empty-Schedule') ScheduleFactory(meeting=self.mtg, name='secretary1')
def test_list_schedules(self): def test_list_schedules(self):
url = urlreverse('ietf.meeting.views.list_schedules',kwargs={'num':self.mtg.number}) url = urlreverse('ietf.meeting.views.list_schedules',kwargs={'num':self.mtg.number})
@ -1423,8 +1463,8 @@ class EditScheduleListTests(TestCase):
) )
# copy # copy
copy_url = urlreverse("ietf.meeting.views.copy_meeting_schedule", kwargs=dict(num=meeting.number, owner=from_schedule.owner_email(), name=from_schedule.name)) 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(copy_url, { r = self.client.post(new_url, {
'name': "newtest", 'name': "newtest",
'public': "on", '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)) 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, { r = self.client.post(edit_url, {
'action': 'assign', 'action': 'assign',
'timeslot': slot3.pk, 'timeslot': slot3.pk,
'session': session3.pk, 'session': session3.pk,
}) })
self.assertEqual(json.loads(r.content)['success'], True) self.assertEqual(json.loads(r.content)['success'], True)
# unschedule session
# unschedule
r = self.client.post(edit_url, { r = self.client.post(edit_url, {
'action': 'unassign', 'action': 'unassign',
'session': session1.pk, 'session': session1.pk,
}) })
self.assertEqual(json.loads(r.content)['success'], True) self.assertEqual(json.loads(r.content)['success'], True)
# move session
# move
r = self.client.post(edit_url, { r = self.client.post(edit_url, {
'action': 'assign', 'action': 'assign',
'timeslot': slot2.pk, 'timeslot': slot2.pk,
@ -1459,7 +1497,7 @@ class EditScheduleListTests(TestCase):
}) })
self.assertEqual(json.loads(r.content)['success'], True) self.assertEqual(json.loads(r.content)['success'], True)
# get differences # now get differences
r = self.client.get(url, { r = self.client.get(url, {
'from_schedule': from_schedule.name, 'from_schedule': from_schedule.name,
'to_schedule': to_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 = 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.time_zone = 'America/Los_Angeles'
meeting.save() meeting.save()
url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number}) url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number})
login_testing_unauthorized(self, "secretary", url) login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url) r = self.client.get(url)

View file

@ -41,7 +41,7 @@ type_ietf_only_patterns = [
url(r'^agenda/%(owner)s/%(schedule_name)s/session/(?P<assignment_id>\d+).json$' % settings.URL_REGEXPS, ajax.assignment_json), url(r'^agenda/%(owner)s/%(schedule_name)s/session/(?P<assignment_id>\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/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.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-room$', views.agenda_by_room),
url(r'^agenda/by-type$', views.agenda_by_type), url(r'^agenda/by-type$', views.agenda_by_type),
url(r'^agenda/by-type/(?P<type>[a-z]+)$', views.agenda_by_type), url(r'^agenda/by-type/(?P<type>[a-z]+)$', views.agenda_by_type),
@ -49,6 +49,7 @@ type_ietf_only_patterns = [
url(r'^agendas/list$', views.list_schedules), 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/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_schedules', permanent=True)),
url(r'^agendas/diff/$', views.diff_schedules), url(r'^agendas/diff/$', views.diff_schedules),
url(r'^agenda/new/$', views.new_meeting_schedule),
url(r'^timeslots/edit$', views.edit_timeslots), url(r'^timeslots/edit$', views.edit_timeslots),
url(r'^timeslot/(?P<slot_id>\d+)/edittype$', views.edit_timeslot_type), url(r'^timeslot/(?P<slot_id>\d+)/edittype$', views.edit_timeslot_type),
url(r'^rooms$', ajax.timeslot_roomsurl), url(r'^rooms$', ajax.timeslot_roomsurl),

View file

@ -28,7 +28,7 @@ from ietf.person.models import Person, Email
from ietf.secr.proceedings.proc_utils import import_audio_files from ietf.secr.proceedings.proc_utils import import_audio_files
def session_time_for_sorting(session, use_meeting_date): 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: if official_timeslot:
return official_timeslot.time return official_timeslot.time
elif use_meeting_date and session.meeting.date: elif use_meeting_date and session.meeting.date:

View file

@ -144,7 +144,7 @@ def materials(request, num=None):
sessions = add_event_info_to_session_qs(Session.objects.filter( sessions = add_event_info_to_session_qs(Session.objects.filter(
meeting__number=meeting.number, 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')) ).distinct().select_related('meeting__schedule', 'group__state', 'group__parent'))
plenaries = sessions.filter(name__icontains='plenary') plenaries = sessions.filter(name__icontains='plenary')
@ -297,6 +297,8 @@ def schedule_create(request, num=None, owner=None, name=None):
newschedule = Schedule(name=savedname, newschedule = Schedule(name=savedname,
owner=request.user.person, owner=request.user.person,
meeting=meeting, meeting=meeting,
base=schedule.base,
origin=schedule,
visible=False, visible=False,
public=False) public=False)
@ -354,14 +356,15 @@ def edit_timeslots(request, num=None):
"ts_list":ts_list, "ts_list":ts_list,
}) })
class CopyScheduleForm(forms.ModelForm): class NewScheduleForm(forms.ModelForm):
class Meta: class Meta:
model = Schedule model = Schedule
fields = ['name', 'visible', 'public', 'notes'] fields = ['name', 'visible', 'public', 'notes', 'base']
def __init__(self, schedule, new_owner, *args, **kwargs): def __init__(self, meeting, schedule, new_owner, *args, **kwargs):
super(CopyScheduleForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.meeting = meeting
self.schedule = schedule self.schedule = schedule
self.new_owner = new_owner self.new_owner = new_owner
@ -370,7 +373,7 @@ class CopyScheduleForm(forms.ModelForm):
name_suggestion = username name_suggestion = username
counter = 2 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: while name_suggestion in existing_names:
name_suggestion = username + str(counter) name_suggestion = username + str(counter)
counter += 1 counter += 1
@ -378,55 +381,68 @@ class CopyScheduleForm(forms.ModelForm):
self.fields['name'].initial = name_suggestion self.fields['name'].initial = name_suggestion
self.fields['name'].label = "Name of new agenda" 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): def clean_name(self):
name = self.cleaned_data.get('name') 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.") raise forms.ValidationError("Schedule with this name already exists.")
return name return name
@role_required('Area Director','Secretariat') @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) 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': 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(): if form.is_valid():
new_schedule = form.save(commit=False) new_schedule = form.save(commit=False)
new_schedule.meeting = schedule.meeting new_schedule.meeting = meeting
new_schedule.owner = request.user.person new_schedule.owner = request.user.person
new_schedule.origin = schedule new_schedule.origin = schedule
new_schedule.save() new_schedule.save()
# keep a mapping so that extendedfrom references can be chased if schedule:
old_pk_to_new_pk = {} # keep a mapping so that extendedfrom references can be chased
extendedfroms = {} old_pk_to_new_pk = {}
for assignment in schedule.assignments.all(): extendedfroms = {}
extendedfrom_id = assignment.extendedfrom_id for assignment in schedule.assignments.all():
extendedfrom_id = assignment.extendedfrom_id
# clone by resetting primary key # clone by resetting primary key
old_pk = assignment.pk old_pk = assignment.pk
assignment.pk = None assignment.pk = None
assignment.schedule = new_schedule assignment.schedule = new_schedule
assignment.extendedfrom = None assignment.extendedfrom = None
assignment.save() assignment.save()
old_pk_to_new_pk[old_pk] = assignment.pk old_pk_to_new_pk[old_pk] = assignment.pk
if extendedfrom_id is not None: if extendedfrom_id is not None:
extendedfroms[assignment.pk] = extendedfrom_id extendedfroms[assignment.pk] = extendedfrom_id
for pk, extendedfrom_id in extendedfroms.values(): for pk, extendedfrom_id in extendedfroms.values():
if extendedfrom_id in old_pk_to_new_pk: if extendedfrom_id in old_pk_to_new_pk:
SchedTimeSessAssignment.objects.filter(pk=pk).update(extendedfrom=old_pk_to_new_pk[extendedfrom_id]) SchedTimeSessAssignment.objects.filter(pk=pk).update(extendedfrom=old_pk_to_new_pk[extendedfrom_id])
# now redirect to this new schedule # now redirect to this new schedule
return redirect(edit_meeting_schedule, meeting.number, new_schedule.owner_email(), new_schedule.name) return redirect(edit_meeting_schedule, meeting.number, new_schedule.owner_email(), new_schedule.name)
else: 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, 'meeting': meeting,
'schedule': schedule, 'schedule': schedule,
'form': form, 'form': form,
@ -462,7 +478,15 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
"hide_menu": True "hide_menu": True
}, status=403, content_type="text/html") }, 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") 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', '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) min_duration = min(t.duration for t in timeslots_qs)
max_duration = max(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.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': 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'] source_day = swap_days_form.cleaned_data['source_day']
target_day = swap_days_form.cleaned_data['target_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()) return HttpResponseRedirect(request.get_full_path())
@ -670,10 +696,6 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
ts.session_assignments = [] ts.session_assignments = []
timeslots_by_pk = {ts.pk: ts for ts in timeslots_qs} 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 = [] unassigned_sessions = []
for s in sessions: for s in sessions:
assigned = False 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') @role_required('Area Director','Secretariat')
def edit_schedule_properties(request, num, owner, name): 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") return HttpResponseForbidden("You may not edit this schedule")
if request.method == 'POST': if request.method == 'POST':
form = SchedulePropertiesForm(instance=schedule, data=request.POST) form = SchedulePropertiesForm(meeting, instance=schedule, data=request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
if request.GET.get('next'): if request.GET.get('next'):
return HttpResponseRedirect(request.GET.get('next')) return HttpResponseRedirect(request.GET.get('next'))
return redirect('ietf.meeting.views.edit_schedule', num=num, owner=owner, name=name) return redirect('ietf.meeting.views.edit_schedule', num=num, owner=owner, name=name)
else: else:
form = SchedulePropertiesForm(instance=schedule) form = SchedulePropertiesForm(meeting, instance=schedule)
return render(request, "meeting/properties_edit.html", { return render(request, "meeting/properties_edit.html", {
"schedule": schedule, "schedule": schedule,
@ -842,7 +874,7 @@ def list_schedules(request, num):
schedules = Schedule.objects.filter( schedules = Schedule.objects.filter(
meeting=meeting 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'): if not has_role(request.user, 'Secretariat'):
schedules = schedules.filter(Q(visible=True) | Q(owner=request.user.person)) schedules = schedules.filter(Q(visible=True) | Q(owner=request.user.person))
@ -859,7 +891,7 @@ def list_schedules(request, num):
if s.origin: if s.origin:
s.changes_from_origin = len(diff_meeting_schedules(s.origin, s)) 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) official_schedules.append(s)
elif user_is_person(request.user, s.owner): elif user_is_person(request.user, s.owner):
own_schedules.append(s) own_schedules.append(s)
@ -869,13 +901,13 @@ def list_schedules(request, num):
other_private_schedules.append(s) other_private_schedules.append(s)
schedule_groups = [ schedule_groups = [
("Official Agenda", official_schedules), (official_schedules, False, "Official Agenda"),
("Own Draft Agendas", own_schedules), (own_schedules, True, "Own Draft Agendas"),
("Other Draft Agendas", other_public_schedules), (other_public_schedules, False, "Other Draft Agendas"),
("Other Private Draft Agendas", other_private_schedules), (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", { return render(request, "meeting/schedule_list.html", {
'meeting': meeting, '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]) return render(request, "meeting/no-"+base+ext, {'meeting':meeting }, content_type=mimetype[ext])
updated = meeting.updated() 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) filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
if ext == ".csv": if ext == ".csv":
@ -1095,10 +1131,15 @@ def agenda_by_room(request, num=None, name=None, owner=None):
else: else:
person = get_person_by_email(owner) person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name) 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() 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]=[] 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() day = ss.timeslot.time.date()
ss_by_day[day].append(ss) ss_by_day[day].append(ss)
return render(request,"meeting/agenda_by_room.html",{"meeting":meeting,"schedule":schedule,"ss_by_day":ss_by_day}) 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: else:
person = get_person_by_email(owner) person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name) 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: if type:
assignments = assignments.filter(session__type__slug=type) assignments = assignments.filter(session__type__slug=type)
return render(request,"meeting/agenda_by_type.html",{"meeting":meeting,"schedule":schedule,"assignments":assignments}) 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): def agenda_by_type_ics(request,num=None,type=None):
meeting = get_meeting(num) meeting = get_meeting(num)
schedule = get_schedule(meeting) 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: if type:
assignments = assignments.filter(session__type__slug=type) assignments = assignments.filter(session__type__slug=type)
updated = meeting.updated() updated = meeting.updated()
@ -1240,7 +1290,11 @@ def week_view(request, num=None, name=None, owner=None):
if not schedule: if not schedule:
raise Http404 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) filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
items = [] items = []
@ -1309,7 +1363,11 @@ def room_view(request, num=None, name=None, owner=None):
person = get_person_by_email(owner) person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name) 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') unavailable = meeting.timeslot_set.filter(type__slug='unavail')
if not (assignments.exists() or unavailable.exists()): if not (assignments.exists() or unavailable.exists()):
return HttpResponse("No sessions/timeslots available yet") 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] == '~': elif len(item) > 1 and item[0] == '~':
include_types |= set([item[1:]]) 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) assignments = preprocess_assignments_for_agenda(assignments, meeting)
if q: if q:
@ -1427,13 +1485,17 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
}, content_type="text/calendar") }, content_type="text/calendar")
@cache_page(15 * 60) @cache_page(15 * 60)
def json_agenda(request, num=None ): def json_agenda(request, num=None):
meeting = get_meeting(num, type_in=['ietf','interim']) meeting = get_meeting(num, type_in=['ietf','interim'])
sessions = [] sessions = []
locations = set() locations = set()
parent_acronyms = 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 # Update the assignments with historic information, i.e., valid at the
# time of the meeting # time of the meeting
assignments = preprocess_assignments_for_agenda(assignments, meeting, extra_prefetches=[ 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.historic_group.historic_parent = None
session.type_counter = Counter() 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 ss:
if meeting.type_id == 'interim' and not (meeting.city or meeting.country): if meeting.type_id == 'interim' and not (meeting.city or meeting.country):
session.times = [ x.timeslot.utc_start_time() for x in ss ] 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.public = True
schedule.visible = True schedule.visible = True
schedule.save() 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.schedule = schedule
meeting.save() meeting.save()
return HttpResponseRedirect(reverse('ietf.meeting.views.list_schedules',kwargs={'num':num})) return HttpResponseRedirect(reverse('ietf.meeting.views.list_schedules',kwargs={'num':num}))
if not schedule.public: if not schedule.public:
messages.warning(request,"This schedule will be made public as it is made official.") messages.warning(request,"This schedule will be made public as it is made official.")
if not schedule.visible: if not schedule.visible:
messages.warning(request,"This schedule will be made visible as it is made official.") 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", return render(request, "meeting/make_schedule_official.html",
{ 'schedule' : schedule, { 'schedule' : schedule,
@ -2344,12 +2414,14 @@ def delete_schedule(request, num, owner, name):
person = get_person_by_email(owner) person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name) schedule = get_schedule_by_name(meeting, person, name)
if schedule.name=='Empty-Schedule': # FIXME: we ought to put these checks in a function and only show
return HttpResponseForbidden('You may not delete the default empty schedule') # the delete button if the checks pass
if schedule == meeting.schedule: if schedule == meeting.schedule:
return HttpResponseForbidden('You may not delete the official schedule for %s'%meeting) 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 ): if not ( has_role(request.user, 'Secretariat') or person.user == request.user ):
return HttpResponseForbidden("You may not delete other user's schedules") return HttpResponseForbidden("You may not delete other user's schedules")
@ -2660,10 +2732,12 @@ def interim_request_details(request, number):
return redirect(interim_pending) return redirect(interim_pending)
first_session = sessions.first() 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", { return render(request, "meeting/interim_request_details.html", {
"meeting": meeting, "meeting": meeting,
"sessions": sessions, "sessions": sessions,
"assignments": assignments,
"group": first_session.group, "group": first_session.group,
"requester": session_requested_by(first_session), "requester": session_requested_by(first_session),
"session_status": current_session_status(first_session), "session_status": current_session_status(first_session),
@ -2783,10 +2857,10 @@ def upcoming_ical(request):
today = datetime.date.today() today = datetime.date.today()
# get meetings starting 7 days ago -- we'll filter out sessions in the past further down # 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( 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], session__in=[s.pk for m in meetings for s in m.sessions],
timeslot__time__gte=today, timeslot__time__gte=today,
).order_by( ).order_by(
@ -2879,7 +2953,7 @@ def proceedings(request, num=None):
sessions = add_event_info_to_session_qs( sessions = add_event_info_to_session_qs(
Session.objects.filter(meeting__number=meeting.number) Session.objects.filter(meeting__number=meeting.number)
).filter( ).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') ).select_related().order_by('-current_status')
plenaries = sessions.filter(name__icontains='plenary').exclude(current_status='notmeet') plenaries = sessions.filter(name__icontains='plenary').exclude(current_status='notmeet')
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu') 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: else:
form = TimeSlotTypeForm(instance=timeslot) 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}) return render(request, 'meeting/edit_timeslot_type.html', {'timeslot':timeslot,'form':form,'sessions':sessions})

View file

@ -65,7 +65,7 @@ class SecrMeetingTestCase(TestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
q = PyQuery(response.content) 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): def test_add_meeting(self):
"Add Meeting" "Add Meeting"
@ -92,6 +92,9 @@ class SecrMeetingTestCase(TestCase):
new_meeting = Meeting.objects.get(number=number) new_meeting = Meeting.objects.get(number=number)
self.assertTrue(new_meeting.schedule) 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) self.assertEqual(new_meeting.attendees, None)
def test_edit_meeting(self): def test_edit_meeting(self):
@ -197,8 +200,7 @@ class SecrMeetingTestCase(TestCase):
# test delete # test delete
# first unschedule sessions so we can delete # first unschedule sessions so we can delete
SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule).delete() SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base, meeting.unofficial_schedule]).delete()
SchedTimeSessAssignment.objects.filter(schedule=meeting.unofficial_schedule).delete()
self.client.login(username="secretary", password="secretary+password") self.client.login(username="secretary", password="secretary+password")
post_dict = { post_dict = {
'room-TOTAL_FORMS': q('input[name="room-TOTAL_FORMS"]').val(), 'room-TOTAL_FORMS': q('input[name="room-TOTAL_FORMS"]').val(),
@ -339,27 +341,29 @@ class SecrMeetingTestCase(TestCase):
def test_meetings_misc_session_delete(self): def test_meetings_misc_session_delete(self):
meeting = make_meeting_test_data() meeting = make_meeting_test_data()
slot = meeting.schedule.assignments.filter(timeslot__type='reg').first().timeslot schedule = meeting.schedule.base
url = reverse('ietf.secr.meetings.views.misc_session_delete', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id}) slot = schedule.assignments.filter(timeslot__type='reg').first().timeslot
target = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name}) 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") self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(url, {'post':'yes'}) response = self.client.post(url, {'post':'yes'})
self.assertRedirects(response, target) 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): def test_meetings_misc_session_cancel(self):
meeting = make_meeting_test_data() meeting = make_meeting_test_data()
slot = meeting.schedule.assignments.filter(timeslot__type='reg').first().timeslot schedule = meeting.schedule.base
url = reverse('ietf.secr.meetings.views.misc_session_cancel', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id}) slot = schedule.assignments.filter(timeslot__type='reg').first().timeslot
redirect_url = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name}) 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") self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(url, {'post':'yes'}) response = self.client.post(url, {'post':'yes'})
self.assertRedirects(response, redirect_url) 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') self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'canceled')
def test_meetings_regular_session_edit(self): def test_meetings_regular_session_edit(self):

View file

@ -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.meeting.utils import only_sessions_that_can_meet
from ietf.name.models import SessionStatusName from ietf.name.models import SessionStatusName
from ietf.group.models import Group, GroupEvent 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.blue_sheets import create_blue_sheets
from ietf.secr.meetings.forms import ( BaseMeetingRoomFormSet, MeetingModelForm, MeetingSelectForm, from ietf.secr.meetings.forms import ( BaseMeetingRoomFormSet, MeetingModelForm, MeetingSelectForm,
MeetingRoomForm, MiscSessionForm, TimeSlotForm, RegularSessionEditForm, MeetingRoomForm, MiscSessionForm, TimeSlotForm, RegularSessionEditForm,
@ -85,21 +84,19 @@ def check_misc_sessions(meeting,schedule):
Ensure misc session timeslots exist and have appropriate SchedTimeSessAssignment objects Ensure misc session timeslots exist and have appropriate SchedTimeSessAssignment objects
for the specified schedule. 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')) slots = TimeSlot.objects.filter(meeting=meeting,type__in=('break','reg','other','plenary','lead','offagenda'))
plenary = slots.filter(type='plenary').first() plenary = slots.filter(type='plenary').first()
if plenary: if plenary:
assignments = plenary.sessionassignments.all() assignments = plenary.sessionassignments.all()
if not assignments.filter(schedule=schedule): if not assignments.filter(schedule=schedule):
source = assignments.first().schedule source = assignments.first().schedule
copy_assignments(slots,source,schedule) for ss in SchedTimeSessAssignment.objects.filter(schedule=source,timeslot__in=slots):
SchedTimeSessAssignment.objects.create(schedule=schedule,session=ss.session,timeslot=ss.timeslot)
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)
def get_last_meeting(meeting): def get_last_meeting(meeting):
last_number = int(meeting.number) - 1 last_number = int(meeting.number) - 1
@ -221,13 +218,23 @@ def add(request):
if form.is_valid(): if form.is_valid():
meeting = form.save() 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, schedule = Schedule.objects.create(meeting = meeting,
name = 'Empty-Schedule', name = "{}1".format(request.user.username),
owner = Person.objects.get(name='(System)'), owner = request.user.person,
visible = True, visible = True,
public = True) public = True,
base = base_schedule,
)
meeting.schedule = schedule meeting.schedule = schedule
# we want to carry session request lock status over from previous meeting # we want to carry session request lock status over from previous meeting
previous_meeting = get_meeting( int(meeting.number) - 1 ) previous_meeting = get_meeting( int(meeting.number) - 1 )
meeting.session_request_lock_message = previous_meeting.session_request_lock_message 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? # TODO: Why aren't 'ag' in here as well?
groups = Group.objects.filter( groups = Group.objects.filter(
type__in=['wg','rg'], 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) create_blue_sheets(meeting, groups)
messages.success(request, 'Blue Sheets generated') messages.success(request, 'Blue Sheets generated')
@ -380,7 +387,7 @@ def misc_sessions(request, meeting_id, schedule_name):
check_misc_sessions(meeting,schedule) check_misc_sessions(meeting,schedule)
misc_session_types = ['break','reg','other','plenary','lead'] 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') assignments = assignments.order_by('-timeslot__type__name','timeslot__time')
if request.method == 'POST': if request.method == 'POST':
@ -571,7 +578,7 @@ def notifications(request, meeting_id):
meeting = get_object_or_404(Meeting, number=meeting_id) meeting = get_object_or_404(Meeting, number=meeting_id)
last_notice = GroupEvent.objects.filter(type='sent_notification').first() last_notice = GroupEvent.objects.filter(type='sent_notification').first()
groups = set() 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') last_notice = ss.session.group.latest_event(type='sent_notification')
if last_notice and ss.modified > last_notice.time: if last_notice and ss.modified > last_notice.time:
groups.add(ss.session.group) 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) schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name)
sessions = add_event_info_to_session_qs( 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') ).order_by('group__acronym')
if request.method == 'POST': if request.method == 'POST':

View file

@ -32,11 +32,10 @@ VIDEO_TITLE_RE = re.compile(r'IETF(?P<number>[\d]+)-(?P<name>.*)-(?P<date>\d{8})
def _get_session(number,name,date,time): def _get_session(number,name,date,time):
'''Lookup session using data from video title''' '''Lookup session using data from video title'''
meeting = Meeting.objects.get(number=number) meeting = Meeting.objects.get(number=number)
schedule = meeting.schedule
timeslot_time = datetime.datetime.strptime(date + time,'%Y%m%d%H%M') timeslot_time = datetime.datetime.strptime(date + time,'%Y%m%d%H%M')
try: try:
assignment = SchedTimeSessAssignment.objects.get( assignment = SchedTimeSessAssignment.objects.get(
schedule = schedule, schedule__in = [meeting.schedule, meeting.schedule.base],
session__group__acronym = name.lower(), session__group__acronym = name.lower(),
timeslot__time = timeslot_time, timeslot__time = timeslot_time,
) )
@ -108,7 +107,7 @@ def get_timeslot_for_filename(filename):
meeting=meeting, meeting=meeting,
location__name=room_mapping[match.groupdict()['room']], location__name=room_mapping[match.groupdict()['room']],
time=time, time=time,
sessionassignments__schedule=meeting.schedule, sessionassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
).distinct() ).distinct()
uncancelled_slots = [t for t in slots if not add_event_info_to_session_qs(t.sessions.all()).filter(current_status='canceled').exists()] 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] return uncancelled_slots[0]

View file

@ -232,9 +232,12 @@ def recording(request, meeting_num):
session. session.
''' '''
meeting = get_object_or_404(Meeting, number=meeting_num) 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 = Session.objects.filter(
sessions = [ x.session for x in assignments ] 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': if request.method == 'POST':
form = RecordingForm(request.POST,meeting=meeting) form = RecordingForm(request.POST,meeting=meeting)
if form.is_valid(): if form.is_valid():

View file

@ -461,6 +461,11 @@ input.draft-file-input {
Meeting Tool Meeting Tool
========================================================================== */ ========================================================================== */
#misc-sessions .from-base-schedule {
text-align: centeR;
opacity: 0.7;
}
#misc-session-edit-form input[type="text"] { #misc-session-edit-form input[type="text"] {
width: 30em; width: 30em;
} }

View file

@ -33,13 +33,17 @@
<td>{{ assignment.timeslot.location }}</td> <td>{{ assignment.timeslot.location }}</td>
<td>{{ assignment.timeslot.show_location }}</td> <td>{{ assignment.timeslot.show_location }}</td>
<td>{{ assignment.timeslot.type }}</td> <td>{{ assignment.timeslot.type }}</td>
<td><a href="{% url "ietf.secr.meetings.views.misc_session_edit" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Edit</a></td> {% if assignment.schedule_id == schedule.pk %}
<td> <td><a href="{% url "ietf.secr.meetings.views.misc_session_edit" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Edit</a></td>
{% if not assignment.session.type.slug == "break" %} <td>
<a href="{% url "ietf.secr.meetings.views.misc_session_cancel" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Cancel</a> {% if assignment.session.type.slug != "break" %}
{% endif %} <a href="{% url "ietf.secr.meetings.views.misc_session_cancel" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Cancel</a>
</td> {% endif %}
<td><a href="{% url "ietf.secr.meetings.views.misc_session_delete" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Delete</a></td> </td>
<td><a href="{% url "ietf.secr.meetings.views.misc_session_delete" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Delete</a></td>
{% else %}
<td colspan="3" class="from-base-schedule">(from base schedule)</td>
{% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -52,7 +52,7 @@ def get_session(timeslot, schedule=None):
# todo, doesn't account for shared timeslot # todo, doesn't account for shared timeslot
if not schedule: if not schedule:
schedule = timeslot.meeting.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: if qs:
return qs[0] return qs[0]
else: else:
@ -66,7 +66,7 @@ def get_timeslot(session, schedule=None):
''' '''
if not schedule: if not schedule:
schedule = session.meeting.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: if ss:
return ss[0].timeslot return ss[0].timeslot
else: else:

View file

@ -1010,6 +1010,9 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
margin-left: 0.2em; margin-left: 0.2em;
} }
.from-base-schedule {
opacity: 0.7;
}
/* === Edit Meeting Schedule ====================================== */ /* === Edit Meeting Schedule ====================================== */
@ -1138,7 +1141,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
cursor: default; cursor: default;
} }
.edit-meeting-schedule .session.tombstone { .edit-meeting-schedule .session.readonly {
cursor: default; cursor: default;
background-color: #ddd; background-color: #ddd;
} }

View file

@ -8,7 +8,7 @@ jQuery(document).ready(function () {
alert("Error: " + errorText); alert("Error: " + errorText);
} }
let sessions = content.find(".session").not(".tombstone"); let sessions = content.find(".session").not(".readonly");
let timeslots = content.find(".timeslot"); let timeslots = content.find(".timeslot");
let days = content.find(".day-flow .day"); let days = content.find(".day-flow .day");

View file

@ -29,7 +29,7 @@ ul.sessionlist { list-style:none; padding-left:2em; margin-bottom:10px;}
<li class="roomlistentry"><h3>{{room.grouper|default:"Location Unavailable"}}</h3> <li class="roomlistentry"><h3>{{room.grouper|default:"Location Unavailable"}}</h3>
<ul class="sessionlist"> <ul class="sessionlist">
{% for ss in room.list %} {% for ss in room.list %}
<li class="sessionlistentry type-{{ss.timeslot.type.slug}}">{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}</li> <li class="sessionlistentry type-{{ss.timeslot.type_id}} {% if ss.schedule_id != meeting.schedule_id %}from-base-schedule{% endif %}">{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</li> </li>

View file

@ -29,7 +29,7 @@ li.daylistentry { margin-left:2em; font-weight: 400; }
{% block content %} {% block content %}
{% include "meeting/meeting_heading.html" with updated=meeting.updated selected="by-type" title_extra="by Session Type" %} {% 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 %}
<ul class="typelist"> <ul class="typelist">
{% for type in type_list %} {% for type in type_list %}
<li class="typelistentry {% cycle 'even' 'odd' %}"> <li class="typelistentry {% cycle 'even' 'odd' %}">
@ -41,11 +41,11 @@ li.daylistentry { margin-left:2em; font-weight: 400; }
<h3>{{ day.grouper }}</h3> <h3>{{ day.grouper }}</h3>
<table class="sessiontable"> <table class="sessiontable">
{% for ss in day.list %} {% for ss in day.list %}
<tr> <tr {% if ss.schedule_id != meeting.schedule_id %}class="from-base-schedule"{% endif %}>
<td>{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}}</td> <td>{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}}</td>
<td>{{ss.timeslot.get_hidden_location}}</td> <td>{{ss.timeslot.get_hidden_location}}</td>
<td class="type-{{ss.session.type.slug}}">{{ss.session.short_name}} </td> <td class="type-{{ss.session.type_id}}">{{ss.session.short_name}} </td>
<td>{% if ss.session.type_id == 'regular' or ss.session.type_id == 'plenary' or ss.session.type_id == 'other' %} <a href="{% url 'ietf.meeting.views.session_details' num=ss.session.meeting.number acronym=ss.session.group.acronym %}">Materials</a>{% else %}&nbsp;{% endif %}</td> <td>{% if ss.session.type_id == 'regular' or ss.session.type_id == 'plenary' or ss.session.type_id == 'other' %} <a href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=ss.session.group.acronym %}">Materials</a>{% else %}&nbsp;{% endif %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -30,7 +30,7 @@
&middot; &middot;
{% endif %} {% endif %}
<a href="{% url "ietf.meeting.views.copy_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">Copy agenda</a> <a href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">New agenda</a>
&middot; &middot;
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other Agendas</a> <a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other Agendas</a>
@ -46,7 +46,7 @@
{% if not can_edit %} {% if not can_edit %}
&middot; &middot;
<strong><em>You can't edit this schedule. Take a copy first.</em></strong> <strong><em>You can't edit this schedule. Make a <a href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">new agenda from this</a>.</em></strong>
{% endif %} {% endif %}
</p> </p>

View file

@ -1,4 +1,4 @@
<div id="session{{ session.pk }}" class="session {% if not session.group.parent.scheduling_color %}untoggleable{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %} {% if session.is_tombstone %}tombstone{% endif %}" style="width:{{ session.layout_width }}em;" data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}data-attendees="{{ session.attendees }}"{% endif %}> <div id="session{{ session.pk }}" class="session {% if not session.group.parent.scheduling_color %}untoggleable{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %} {% if session.readonly %}readonly{% endif %}" style="width:{{ session.layout_width }}em;" data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}data-attendees="{{ session.attendees }}"{% endif %}>
<div class="session-label {% if session.group and session.group.is_bof %}bof-session{% endif %}"> <div class="session-label {% if session.group and session.group.is_bof %}bof-session{% endif %}">
{{ session.scheduling_label }} {{ session.scheduling_label }}
</div> </div>

View file

@ -1,10 +1,10 @@
{% load ietf_filters %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW. {% 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 {% 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. {% 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 %} {{ 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 %} {% endfor %}{% endif %}
{% if meeting.city %}Meeting Location: {% if meeting.city %}Meeting Location:

View file

@ -26,7 +26,7 @@
<dd>{{ meeting.country }}</dd> <dd>{{ meeting.country }}</dd>
<dt>Timezone</dt> <dt>Timezone</dt>
<dd>{{ meeting.time_zone }}</dd> <dd>{{ meeting.time_zone }}</dd>
{% for assignment in meeting.schedule.assignments.all %} {% for assignment in assignments %}
<br> <br>
<dt>Date</dt> <dt>Date</dt>
<dd>{{ assignment.timeslot.time|date:"Y-m-d" }} <dd>{{ assignment.timeslot.time|date:"Y-m-d" }}

View file

@ -7,14 +7,14 @@
{% block content %} {% block content %}
{% origin %} {% origin %}
<h1>{% block title %}Copy agenda {{ schedule.name }}{% endblock %}</h1> <h1>{% block title %}{% if schedule %}Copy agenda {{ schedule.name }} to new agenda{% else %}New agenda{% endif %}{% endblock %}</h1>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
{% buttons %} {% buttons %}
<button type="submit" class="btn btn-default">Copy agenda</button> <button type="submit" class="btn btn-primary">Create agenda</button>
{% endbuttons %} {% endbuttons %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -8,13 +8,13 @@
<script src="{% static 'ietf/bootstrap/js/bootstrap.min.js' %}"></script> <script src="{% static 'ietf/bootstrap/js/bootstrap.min.js' %}"></script>
<script type="text/javascript"> <script type="text/javascript">
{% autoescape off %}
var room_names = [{% for room in rooms %}"{{room.name}}"{% if not forloop.last %},{% endif %}{% endfor %}]; var room_names = [{% for room in rooms %}"{{room.name}}"{% if not forloop.last %},{% endif %}{% endfor %}];
var room_functional_names = [{% for room in rooms %}"{{room.functional_name}}"{% if not forloop.last %},{% endif %}{% endfor %}]; var room_functional_names = [{% for room in rooms %}"{{room.functional_name}}"{% if not forloop.last %},{% endif %}{% endfor %}];
var room_typelabels = [{% for room in rooms %}"{% for type in room.session_types.all %}{{type.name}}{% if not forloop.last %}, {% endif %}{% endfor %}"{% if not forloop.last %},{% endif %}{% endfor %}]; var room_typelabels = [{% for room in rooms %}"{% for type in room.session_types.all %}{{type.name}}{% if not forloop.last %}, {% endif %}{% endfor %}"{% if not forloop.last %},{% endif %}{% endfor %}];
var items = new Array(); var items = new Array();
{% autoescape off %}
{% for slot in unavailable %} {% for slot in unavailable %}
if (room_names.indexOf("{{slot.get_hidden_location}}") >= 0 ) if (room_names.indexOf("{{slot.get_hidden_location}}") >= 0 )
{ {
@ -24,7 +24,7 @@
{% for ss in assignments %} {% for ss in assignments %}
if (room_names.indexOf("{{ss.timeslot.get_hidden_location}}") >= 0 ) if (room_names.indexOf("{{ss.timeslot.get_hidden_location}}") >= 0 )
{ {
items.push({room_index:room_names.indexOf("{{ss.timeslot.get_hidden_location}}"),day:{{ss.day}}, delta_from_beginning:{{ss.delta_from_beginning}},time:"{{ss.timeslot.time|date:"Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}", verbose_time:"{{ss.timeslot.time|date:"D M d Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}",duration:{{ss.timeslot.duration.total_seconds}}, type:"{{ss.timeslot.type}}", {% if ss.session.name %}name:"{{ss.session.name|escapejs}}",{% if ss.session.group.acronym %} wg:"{{ss.session.group.acronym}}",{%endif%}{% else %}{% if ss.timeslot.type.name == "Break" %}name:"{{ss.timeslot.name|escapejs}}", area:"break", wg:"break",{% elif ss.timeslot.type.slug == "unavail" %}name:"Unavailable",{% else %}name:"{{ss.session.group.name|escapejs}}{%if ss.session.group.state.name == "BOF"%} BOF{%endif%}",wg:"{{ss.session.group.acronym}}",state:"{{ss.session.group.state}}",area:"{{ss.session.group.parent.acronym}}",{% endif %}{% endif %} dayname:"{{ ss.timeslot.time|date:"l"|upper }}, {{ ss.timeslot.time|date:"F j, Y" }}"{% if ss.session.agenda %}, agenda:"{{ss.session.agenda.get_href}}"{% endif %} }); items.push({room_index:room_names.indexOf("{{ss.timeslot.get_hidden_location}}"),day:{{ss.day}}, delta_from_beginning:{{ss.delta_from_beginning}},time:"{{ss.timeslot.time|date:"Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}", verbose_time:"{{ss.timeslot.time|date:"D M d Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}",duration:{{ss.timeslot.duration.total_seconds}}, type:"{{ss.timeslot.type}}", {% if ss.session.name %}name:"{{ss.session.name|escapejs}}",{% if ss.session.group.acronym %} wg:"{{ss.session.group.acronym}}",{%endif%}{% else %}{% if ss.timeslot.type.name == "Break" %}name:"{{ss.timeslot.name|escapejs}}", area:"break", wg:"break",{% elif ss.timeslot.type_id == "unavail" %}name:"Unavailable",{% else %}name:"{{ss.session.group.name|escapejs}}{%if ss.session.group.state.name == "BOF"%} BOF{%endif%}",wg:"{{ss.session.group.acronym}}",state:"{{ss.session.group.state}}",area:"{{ss.session.group.parent.acronym}}",{% endif %}{% endif %} dayname:"{{ ss.timeslot.time|date:"l"|upper }}, {{ ss.timeslot.time|date:"F j, Y" }}"{% if ss.session.agenda %}, agenda:"{{ss.session.agenda.get_href}}"{% endif %}, from_base_schedule: {% if ss.schedule_id != meeting.schedule_id %}true{% else %}false{% endif %} });
} }
{% endfor %} {% endfor %}
{% endautoescape %} {% endautoescape %}
@ -353,6 +353,9 @@
e.style.padding=padding; e.style.padding=padding;
e.style.fontFamily="sans-serif"; e.style.fontFamily="sans-serif";
e.style.fontSize="8pt"; e.style.fontSize="8pt";
if (items[i].from_base_schedule)
e.style.opacity = 0.5;
e.id=i; e.id=i;
e.onmouseover=resize_func(e,sess_top,room_left, e.onmouseover=resize_func(e,sess_top,room_left,

View file

@ -5,25 +5,34 @@
{% block content %} {% block content %}
{% origin %} {% origin %}
<h1>{% block title %}Possible Meeting Agendas for IETF {{ meeting.number }}{% endblock %}</h1> <h1>{% block title %}Possible Meeting Agendas for IETF {{ meeting.number }}{% endblock %}</h1>
{% comment %} {% comment %}
<div> <div>
<p><a href="{% url "ietf.meeting.views.edit_timeslots" meeting.number %}">Edit Timeslots</a></p> <p><a href="{% url "ietf.meeting.views.edit_timeslots" meeting.number %}">Edit Timeslots</a></p>
</div> </div>
{% endcomment %} {% endcomment %}
<div> <div>
{% for label, schedules in schedule_groups %} {% for schedules, own, label in schedule_groups %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">{{ label }}</div> <div class="panel-heading">
{{ label }}
{% if own %}
<div class="pull-right" >
<a href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number %}"><i class="fa fa-plus"></i> New Agenda</a>
</div>
{% endif %}
</div>
<div class="panel-body"> <div class="panel-body">
<table class="table table-condensed table-striped"> <table class="table table-condensed table-striped">
<tr> <tr>
<th class="col-md-2">Name</th> <th class="col-md-2">Name</th>
<th class="col-md-2">Owner</th> <th class="col-md-2">Owner</th>
<th class="col-md-1">Origin</th> <th class="col-md-1">Origin</th>
<th class="col-md-4">Notes</th> <th class="col-md-1">Base</th>
<th class="col-md-3">Notes</th>
<th class="col-md-1">Visible</th> <th class="col-md-1">Visible</th>
<th class="col-md-1">Public</th> <th class="col-md-1">Public</th>
</tr> </tr>
@ -40,14 +49,19 @@
<td>{{ schedule.owner }}</td> <td>{{ schedule.owner }}</td>
<td> <td>
{% if schedule.origin %} {% if schedule.origin %}
<a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.origin.owner_email schedule.name %}">{{ schedule.origin.name }}</a> <a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.origin.owner_email schedule.origin.name %}">{{ schedule.origin.name }}</a>
<a href="{% url "ietf.meeting.views.diff_schedules" meeting.number %}?from_schedule={{ schedule.origin.name|urlencode }}&to_schedule={{ schedule.name|urlencode }}" title="{{ schedule.changes_from_origin }} change{{ schedule.changes_from_origin|pluralize }} from {{ schedule.origin.name }}">+{{ schedule.changes_from_origin }}</a> <a href="{% url "ietf.meeting.views.diff_schedules" meeting.number %}?from_schedule={{ schedule.origin.name|urlencode }}&to_schedule={{ schedule.name|urlencode }}" title="{{ schedule.changes_from_origin }} change{{ schedule.changes_from_origin|pluralize }} from {{ schedule.origin.name }}">+{{ schedule.changes_from_origin }}</a>
{% endif %} {% endif %}
</td> </td>
<td>{{ schedule.notes|linebreaksbr }}</td> <td>
<td> {% if schedule.base %}
{% if schedule.visible %} <a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.base.owner_email schedule.base.name %}">{{ schedule.base.name }}</a>
<div class="label label-success">visible</div> {% endif %}
</td>
<td>{{ schedule.notes|linebreaksbr }}</td>
<td>
{% if schedule.visible %}
<div class="label label-success">visible</div>
{% else %} {% else %}
<div class="label label-danger">hidden</div> <div class="label label-danger">hidden</div>
{% endif %} {% endif %}