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():
print " Checking for missing Break and Reg sessions in %s" % schedule
for timeslot in meeting.timeslot_set.all():
if timeslot.type_id == 'break':
assignment, created = ScheduleTimeslotSSessionAssignment.objects.get_or_create(timeslot=timeslot, session=brk, schedule=schedule)
if timeslot.type_id == 'break' and not (schedule.base and SchedTimeSessAssignment.objects.filter(timeslot=timeslot, session=brk, schedule=schedule.base).exists()):
assignment, created = SchedTimeSessAssignment.objects.get_or_create(timeslot=timeslot, session=brk, schedule=schedule)
if created:
print " Added %s break assignment" % timeslot
if timeslot.type_id == 'reg':
assignment, created = ScheduleTimeslotSSessionAssignment.objects.get_or_create(timeslot=timeslot, session=reg, schedule=schedule)
if timeslot.type_id == 'reg' and not (schedule.base and SchedTimeSessAssignment.objects.filter(timeslot=timeslot, session=reg, schedule=schedule.base).exists()):
assignment, created = SchedTimeSessAssignment.objects.get_or_create(timeslot=timeslot, session=reg, schedule=schedule)
if created:
print " Added %s registration assignment" % timeslot

View file

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

View file

@ -143,13 +143,6 @@ def get_schedule(meeting, name=None):
schedule = get_object_or_404(meeting.schedule_set, name=name)
return schedule
def get_schedule_by_id(meeting, schedid):
if schedid is None:
schedule = meeting.schedule
else:
schedule = get_object_or_404(meeting.schedule_set, id=int(schedid))
return schedule
# seems this belongs in ietf/person/utils.py?
def get_person_by_email(email):
# email == None may actually match people who haven't set an email!
@ -428,6 +421,11 @@ def get_announcement_initial(meeting, is_change=False):
type = group.type.slug.upper()
if group.type.slug == 'wg' and group.state.slug == 'bof':
type = 'BOF'
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
).order_by('timeslot__time')
initial['subject'] = '{name} ({acronym}) {type} {desc} Meeting: {date}{change}'.format(
name=group.name,
acronym=group.acronym,

View file

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

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
timeslots_updated = self.timeslot_set.aggregate(Max('modified'))["modified__max"] or min_time
sessions_updated = self.session_set.aggregate(Max('modified'))["modified__max"] or min_time
assignments_updated = (self.schedule.assignments.aggregate(Max('modified'))["modified__max"] or min_time) if self.schedule else min_time
assignments_updated = min_time
if self.schedule:
assignments_updated = SchedTimeSessAssignment.objects.filter(schedule__in=[self.schedule, self.schedule.base if self.schedule else None]).aggregate(Max('modified'))["modified__max"] or min_time
ts = max(timeslots_updated, sessions_updated, assignments_updated)
tz = pytz.timezone(settings.PRODUCTION_TIMEZONE)
ts = tz.localize(ts)
@ -450,7 +452,7 @@ class TimeSlot(models.Model):
@property
def session(self):
if not hasattr(self, "_session_cache"):
self._session_cache = self.sessions.filter(timeslotassignments__schedule=self.meeting.schedule).first()
self._session_cache = self.sessions.filter(timeslotassignments__schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting else None]).first()
return self._session_cache
@property
@ -626,7 +628,10 @@ class Schedule(models.Model):
public = models.BooleanField(default=True, help_text="Allow others to see this agenda.")
badness = models.IntegerField(null=True, blank=True)
notes = models.TextField(blank=True)
origin = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL)
origin = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL, related_name="+")
base = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL,
help_text="Sessions scheduled in the base schedule show up in this schedule too.", related_name="derivedschedule_set",
limit_choices_to={'base': None}) # prevent the inheritance from being more than one layer deep (no recursion)
def __str__(self):
return u"%s:%s(%s)" % (self.meeting, self.name, self.owner)
@ -1047,7 +1052,7 @@ class Session(models.Model):
ss0name = "(%s)" % SessionStatusName.objects.get(slug=status_id).name
else:
ss0name = "(unscheduled)"
ss = self.timeslotassignments.filter(schedule=self.meeting.schedule).order_by('timeslot__time')
ss = self.timeslotassignments.filter(schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting.schedule else None]).order_by('timeslot__time')
if ss:
ss0name = ','.join(x.timeslot.time.strftime("%a-%H%M") for x in ss)
return "%s: %s %s %s" % (self.meeting, self.group.acronym, self.name, ss0name)
@ -1080,11 +1085,8 @@ class Session(models.Model):
def reverse_constraints(self):
return Constraint.objects.filter(target=self.group, meeting=self.meeting).order_by('name__name')
def timeslotassignment_for_schedule(self, schedule):
return self.timeslotassignments.filter(schedule=schedule).first()
def official_timeslotassignment(self):
return self.timeslotassignment_for_schedule(self.meeting.schedule)
return self.timeslotassignments.filter(schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting.schedule else None]).first()
def constraints_dict(self, host_scheme):
constraint_list = []

View file

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

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()))
self.driver.get(url)
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.edit-meeting-schedule')))
self.assertEqual(len(self.driver.find_elements_by_css_selector('.session')), 3)
# select - show session info

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',
city="Panama City", country="PA", time_zone='America/Panama')
registration_text = "Registration"
# utc
time_interval = "%s-%s" % (slot.utc_start_time().strftime("%H:%M").lstrip("0"), (slot.utc_start_time() + slot.duration).strftime("%H:%M").lstrip("0"))
@ -152,6 +154,7 @@ class MeetingTests(TestCase):
self.assertIn(session.group.parent.acronym.upper(), agenda_content)
self.assertIn(slot.location.name, agenda_content)
self.assertIn(time_interval, agenda_content)
self.assertIn(registration_text, agenda_content)
# Make sure there's a frame for the agenda and it points to the right place
self.assertTrue(any([session.materials.get(type='agenda').get_href() in x.attrib["data-src"] for x in q('tr div.modal-body div.frame')]))
@ -191,6 +194,7 @@ class MeetingTests(TestCase):
self.assertContains(r, session.group.name)
self.assertContains(r, session.group.parent.acronym.upper())
self.assertContains(r, slot.location.name)
self.assertContains(r, registration_text)
self.assertContains(r, session.materials.get(type='agenda').uploaded_filename)
self.assertContains(r, session.materials.filter(type='slides').exclude(states__type__slug='slides',states__slug='deleted').first().uploaded_filename)
@ -215,6 +219,7 @@ class MeetingTests(TestCase):
self.assertNotContains(r, 'CANCELLED')
self.assertContains(r, session.group.acronym)
self.assertContains(r, slot.location.name)
self.assertContains(r, registration_text)
# week view with a cancelled session
SchedulingEvent.objects.create(
@ -585,16 +590,22 @@ class MeetingTests(TestCase):
self.client.login(username='secretary',password='secretary+password')
response = self.client.get(url)
self.assertEqual(response.status_code,200)
new_base = Schedule.objects.create(name="newbase", owner=schedule.owner, meeting=schedule.meeting)
response = self.client.post(url, {
'name':schedule.name,
'visible':True,
'public':True,
'notes': "New Notes",
'base': new_base.pk,
}
)
self.assertEqual(response.status_code,302)
schedule = Schedule.objects.get(pk=schedule.pk)
self.assertNoFormPostErrors(response)
schedule.refresh_from_db()
self.assertTrue(schedule.visible)
self.assertTrue(schedule.public)
self.assertEqual(schedule.notes, "New Notes")
self.assertEqual(schedule.base_id, new_base.pk)
def test_agenda_by_type_ics(self):
session=SessionFactory(meeting__type_id='ietf',type_id='lead')
@ -985,7 +996,21 @@ class EditTests(TestCase):
person=p,
name=ConstraintName.objects.get(slug="bethere"),
)
room = Room.objects.get(meeting=meeting, session_types='regular')
base_timeslot = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room,
duration=datetime.timedelta(minutes=50),
time=datetime.datetime.combine(meeting.date + datetime.timedelta(days=2), datetime.time(9, 30)))
timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('time'))
base_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="irg"),
attendees=20, requested_duration=datetime.timedelta(minutes=30),
type_id='regular')
SchedulingEvent.objects.create(session=base_session, status_id='schedw', by=Person.objects.get(user__username='secretary'))
SchedTimeSessAssignment.objects.create(timeslot=base_timeslot, session=base_session, schedule=meeting.schedule.base)
# check we have the grid and everything set up as a baseline -
# the Javascript tests check that the Javascript can work with
# it
@ -993,11 +1018,9 @@ class EditTests(TestCase):
r = self.client.get(url)
q = PyQuery(r.content)
room = Room.objects.get(meeting=meeting, session_types='regular')
self.assertTrue(q(".room-name:contains(\"{}\")".format(room.name)))
self.assertTrue(q(".room-name:contains(\"{}\")".format(room.capacity)))
timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular'))
self.assertTrue(q("#timeslot{}".format(timeslots[0].pk)))
for s in [s1, s2]:
@ -1031,12 +1054,14 @@ class EditTests(TestCase):
if s.comments:
self.assertIn(s.comments, e.find(".comments").text())
formatted_constraints = e.find(".session-info .formatted-constraints > *")
if s == s1:
self.assertIn(s_other.group.acronym, formatted_constraints.eq(0).html())
self.assertIn(p.name, formatted_constraints.eq(1).html())
elif s == s2:
self.assertIn(p.name, formatted_constraints.eq(0).html())
formatted_constraints1 = q("#session{} .session-info .formatted-constraints > *".format(s1.pk))
self.assertIn(s2.group.acronym, formatted_constraints1.eq(0).html())
self.assertIn(p.name, formatted_constraints1.eq(1).html())
formatted_constraints2 = q("#session{} .session-info .formatted-constraints > *".format(s2.pk))
self.assertIn(p.name, formatted_constraints2.eq(0).html())
self.assertEqual(len(q("#session{}.readonly".format(base_session.pk))), 1)
self.assertTrue(q("em:contains(\"You can't edit this schedule\")"))
@ -1106,7 +1131,7 @@ class EditTests(TestCase):
self.assertEqual(tombstone_event.status_id, 'resched')
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s_tombstone).timeslot, timeslots[1])
self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.tombstone".format(s_tombstone.pk)).html())
self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.readonly".format(s_tombstone.pk)).html())
# unassign
r = self.client.post(url, {
@ -1117,16 +1142,9 @@ class EditTests(TestCase):
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), [])
# try swapping days
timeslots.append(TimeSlot.objects.create(
meeting=meeting, type_id='regular', location=timeslots[0].location,
duration=timeslots[0].duration - datetime.timedelta(minutes=5),
time=timeslots[0].time + datetime.timedelta(days=1),
))
SchedTimeSessAssignment.objects.create(schedule=schedule, session=s1, timeslot=timeslots[1])
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[0])), 1)
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[1])), 1)
SchedTimeSessAssignment.objects.create(schedule=schedule, session=s1, timeslot=timeslots[0])
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1)
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[1])), 1)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), [])
r = self.client.post(url, {
@ -1138,8 +1156,8 @@ class EditTests(TestCase):
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[0])), [])
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), [])
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), [])
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[2])), 1)
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[2])), 1)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2)), [])
# swap back
r = self.client.post(url, {
@ -1149,37 +1167,59 @@ class EditTests(TestCase):
})
self.assertEqual(r.status_code, 302)
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[0])), 1)
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), [])
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), [])
def test_copy_meeting_schedule(self):
def test_new_meeting_schedule(self):
meeting = make_meeting_test_data()
self.client.login(username="secretary", password="secretary+password")
url = urlreverse("ietf.meeting.views.copy_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
# new from scratch
url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# copy
r = self.client.post(url, {
'name': "newtest",
'name': "scratch",
'public': "on",
'notes': "New test",
'visible': "on",
'notes': "New scratch",
'base': meeting.schedule.base_id,
})
self.assertNoFormPostErrors(r)
new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='newtest')
new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='scratch')
self.assertEqual(new_schedule.public, True)
self.assertEqual(new_schedule.visible, True)
self.assertEqual(new_schedule.notes, "New scratch")
self.assertEqual(new_schedule.origin, None)
self.assertEqual(new_schedule.base_id, meeting.schedule.base_id)
# copy
url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.client.post(url, {
'name': "copy",
'public': "on",
'notes': "New copy",
'base': meeting.schedule.base_id,
})
self.assertNoFormPostErrors(r)
new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='copy')
self.assertEqual(new_schedule.public, True)
self.assertEqual(new_schedule.visible, False)
self.assertEqual(new_schedule.notes, "New test")
self.assertEqual(new_schedule.notes, "New copy")
self.assertEqual(new_schedule.origin, meeting.schedule)
self.assertEqual(new_schedule.base_id, meeting.schedule.base_id)
old_assignments = {(a.session_id, a.timeslot_id) for a in SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule)}
for a in SchedTimeSessAssignment.objects.filter(schedule=new_schedule):
self.assertIn((a.session_id, a.timeslot_id), old_assignments)
# FIXME: test extendedfrom is copied correctly
def test_save_agenda_as_and_read_permissions(self):
meeting = make_meeting_test_data()
@ -1390,7 +1430,7 @@ class SessionDetailsTests(TestCase):
class EditScheduleListTests(TestCase):
def setUp(self):
self.mtg = MeetingFactory(type_id='ietf')
ScheduleFactory(meeting=self.mtg,name='Empty-Schedule')
ScheduleFactory(meeting=self.mtg, name='secretary1')
def test_list_schedules(self):
url = urlreverse('ietf.meeting.views.list_schedules',kwargs={'num':self.mtg.number})
@ -1423,8 +1463,8 @@ class EditScheduleListTests(TestCase):
)
# copy
copy_url = urlreverse("ietf.meeting.views.copy_meeting_schedule", kwargs=dict(num=meeting.number, owner=from_schedule.owner_email(), name=from_schedule.name))
r = self.client.post(copy_url, {
new_url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=from_schedule.owner_email(), name=from_schedule.name))
r = self.client.post(new_url, {
'name': "newtest",
'public': "on",
})
@ -1436,22 +1476,20 @@ class EditScheduleListTests(TestCase):
edit_url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=to_schedule.owner_email(), name=to_schedule.name))
# schedule
# schedule session
r = self.client.post(edit_url, {
'action': 'assign',
'timeslot': slot3.pk,
'session': session3.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
# unschedule
# unschedule session
r = self.client.post(edit_url, {
'action': 'unassign',
'session': session1.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
# move
# move session
r = self.client.post(edit_url, {
'action': 'assign',
'timeslot': slot2.pk,
@ -1459,7 +1497,7 @@ class EditScheduleListTests(TestCase):
})
self.assertEqual(json.loads(r.content)['success'], True)
# get differences
# now get differences
r = self.client.get(url, {
'from_schedule': from_schedule.name,
'to_schedule': to_schedule.name,
@ -1584,6 +1622,7 @@ class InterimTests(TestCase):
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
meeting.time_zone = 'America/Los_Angeles'
meeting.save()
url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number})
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)

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/sessions.json$' % settings.URL_REGEXPS, ajax.assignments_json),
url(r'^agenda/%(owner)s/%(schedule_name)s.json$' % settings.URL_REGEXPS, ajax.schedule_infourl),
url(r'^agenda/%(owner)s/%(schedule_name)s/copy/$' % settings.URL_REGEXPS, views.copy_meeting_schedule),
url(r'^agenda/%(owner)s/%(schedule_name)s/new/$' % settings.URL_REGEXPS, views.new_meeting_schedule),
url(r'^agenda/by-room$', views.agenda_by_room),
url(r'^agenda/by-type$', views.agenda_by_type),
url(r'^agenda/by-type/(?P<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/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_schedules', permanent=True)),
url(r'^agendas/diff/$', views.diff_schedules),
url(r'^agenda/new/$', views.new_meeting_schedule),
url(r'^timeslots/edit$', views.edit_timeslots),
url(r'^timeslot/(?P<slot_id>\d+)/edittype$', views.edit_timeslot_type),
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
def session_time_for_sorting(session, use_meeting_date):
official_timeslot = TimeSlot.objects.filter(sessionassignments__session=session, sessionassignments__schedule=session.meeting.schedule).first()
official_timeslot = TimeSlot.objects.filter(sessionassignments__session=session, sessionassignments__schedule__in=[session.meeting.schedule, session.meeting.schedule.base if session.meeting.schedule else None]).first()
if official_timeslot:
return official_timeslot.time
elif use_meeting_date and session.meeting.date:

View file

@ -144,7 +144,7 @@ def materials(request, num=None):
sessions = add_event_info_to_session_qs(Session.objects.filter(
meeting__number=meeting.number,
timeslotassignments__schedule=schedule
timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None]
).distinct().select_related('meeting__schedule', 'group__state', 'group__parent'))
plenaries = sessions.filter(name__icontains='plenary')
@ -297,6 +297,8 @@ def schedule_create(request, num=None, owner=None, name=None):
newschedule = Schedule(name=savedname,
owner=request.user.person,
meeting=meeting,
base=schedule.base,
origin=schedule,
visible=False,
public=False)
@ -354,14 +356,15 @@ def edit_timeslots(request, num=None):
"ts_list":ts_list,
})
class CopyScheduleForm(forms.ModelForm):
class NewScheduleForm(forms.ModelForm):
class Meta:
model = Schedule
fields = ['name', 'visible', 'public', 'notes']
fields = ['name', 'visible', 'public', 'notes', 'base']
def __init__(self, schedule, new_owner, *args, **kwargs):
super(CopyScheduleForm, self).__init__(*args, **kwargs)
def __init__(self, meeting, schedule, new_owner, *args, **kwargs):
super().__init__(*args, **kwargs)
self.meeting = meeting
self.schedule = schedule
self.new_owner = new_owner
@ -370,7 +373,7 @@ class CopyScheduleForm(forms.ModelForm):
name_suggestion = username
counter = 2
existing_names = set(Schedule.objects.filter(meeting=schedule.meeting_id, owner=new_owner).values_list('name', flat=True))
existing_names = set(Schedule.objects.filter(meeting=meeting, owner=new_owner).values_list('name', flat=True))
while name_suggestion in existing_names:
name_suggestion = username + str(counter)
counter += 1
@ -378,55 +381,68 @@ class CopyScheduleForm(forms.ModelForm):
self.fields['name'].initial = name_suggestion
self.fields['name'].label = "Name of new agenda"
self.fields['base'].queryset = self.fields['base'].queryset.filter(meeting=meeting)
if schedule:
self.fields['visible'].initial = schedule.visible
self.fields['public'].initial = schedule.public
self.fields['base'].queryset = self.fields['base'].queryset.exclude(pk=schedule.pk)
self.fields['base'].initial = schedule.base_id
else:
base = Schedule.objects.filter(meeting=meeting, name='base').first()
if base:
self.fields['base'].initial = base.pk
def clean_name(self):
name = self.cleaned_data.get('name')
if name and Schedule.objects.filter(meeting=self.schedule.meeting_id, owner=self.new_owner, name=name):
if name and Schedule.objects.filter(meeting=self.meeting, owner=self.new_owner, name=name):
raise forms.ValidationError("Schedule with this name already exists.")
return name
@role_required('Area Director','Secretariat')
def copy_meeting_schedule(request, num, owner, name):
def new_meeting_schedule(request, num, owner=None, name=None):
meeting = get_meeting(num)
schedule = get_object_or_404(meeting.schedule_set, owner__email__address=owner, name=name)
schedule = get_schedule_by_name(meeting, get_person_by_email(owner), name)
if request.method == 'POST':
form = CopyScheduleForm(schedule, request.user.person, request.POST)
form = NewScheduleForm(meeting, schedule, request.user.person, request.POST)
if form.is_valid():
new_schedule = form.save(commit=False)
new_schedule.meeting = schedule.meeting
new_schedule.meeting = meeting
new_schedule.owner = request.user.person
new_schedule.origin = schedule
new_schedule.save()
# keep a mapping so that extendedfrom references can be chased
old_pk_to_new_pk = {}
extendedfroms = {}
for assignment in schedule.assignments.all():
extendedfrom_id = assignment.extendedfrom_id
if schedule:
# keep a mapping so that extendedfrom references can be chased
old_pk_to_new_pk = {}
extendedfroms = {}
for assignment in schedule.assignments.all():
extendedfrom_id = assignment.extendedfrom_id
# clone by resetting primary key
old_pk = assignment.pk
assignment.pk = None
assignment.schedule = new_schedule
assignment.extendedfrom = None
assignment.save()
# clone by resetting primary key
old_pk = assignment.pk
assignment.pk = None
assignment.schedule = new_schedule
assignment.extendedfrom = None
assignment.save()
old_pk_to_new_pk[old_pk] = assignment.pk
if extendedfrom_id is not None:
extendedfroms[assignment.pk] = extendedfrom_id
old_pk_to_new_pk[old_pk] = assignment.pk
if extendedfrom_id is not None:
extendedfroms[assignment.pk] = extendedfrom_id
for pk, extendedfrom_id in extendedfroms.values():
if extendedfrom_id in old_pk_to_new_pk:
SchedTimeSessAssignment.objects.filter(pk=pk).update(extendedfrom=old_pk_to_new_pk[extendedfrom_id])
for pk, extendedfrom_id in extendedfroms.values():
if extendedfrom_id in old_pk_to_new_pk:
SchedTimeSessAssignment.objects.filter(pk=pk).update(extendedfrom=old_pk_to_new_pk[extendedfrom_id])
# now redirect to this new schedule
return redirect(edit_meeting_schedule, meeting.number, new_schedule.owner_email(), new_schedule.name)
else:
form = CopyScheduleForm(schedule, request.user.person)
form = NewScheduleForm(meeting, schedule, request.user.person)
return render(request, "meeting/copy_meeting_schedule.html", {
return render(request, "meeting/new_meeting_schedule.html", {
'meeting': meeting,
'schedule': schedule,
'form': form,
@ -462,7 +478,15 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
"hide_menu": True
}, status=403, content_type="text/html")
assignments = get_all_assignments_from_schedule(schedule)
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base],
timeslot__location__isnull=False,
session__type='regular',
).order_by('timeslot__time','timeslot__name')
assignments_by_session = defaultdict(list)
for a in assignments:
assignments_by_session[a.session_id].append(a)
rooms = meeting.room_set.filter(session_types__slug='regular').distinct().order_by("capacity")
@ -483,7 +507,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups',
)
timeslots_qs = meeting.timeslot_set.filter(type='regular').prefetch_related('type', 'sessions').order_by('location', 'time', 'name')
timeslots_qs = TimeSlot.objects.filter(meeting=meeting, type='regular').prefetch_related('type').order_by('location', 'time', 'name')
min_duration = min(t.duration for t in timeslots_qs)
max_duration = max(t.duration for t in timeslots_qs)
@ -549,7 +573,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
s.other_sessions = [s_other for s_other in sessions_for_group.get(s.group_id) if s != s_other]
s.is_tombstone = s.current_status in tombstone_states
s.readonly = s.current_status in tombstone_states or any(a.schedule_id != schedule.pk for a in assignments_by_session.get(s.pk, []))
if request.method == 'POST':
@ -622,7 +646,9 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
source_day = swap_days_form.cleaned_data['source_day']
target_day = swap_days_form.cleaned_data['target_day']
swap_meeting_schedule_timeslot_assignments(schedule, [ts for ts in timeslots_qs if ts.time.date() == source_day], [ts for ts in timeslots_qs if ts.time.date() == target_day], target_day - source_day)
source_timeslots = [ts for ts in timeslots_qs if ts.time.date() == source_day]
target_timeslots = [ts for ts in timeslots_qs if ts.time.date() == target_day]
swap_meeting_schedule_timeslot_assignments(schedule, source_timeslots, target_timeslots, target_day - source_day)
return HttpResponseRedirect(request.get_full_path())
@ -670,10 +696,6 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
ts.session_assignments = []
timeslots_by_pk = {ts.pk: ts for ts in timeslots_qs}
assignments_by_session = defaultdict(list)
for a in assignments:
assignments_by_session[a.session_id].append(a)
unassigned_sessions = []
for s in sessions:
assigned = False
@ -798,7 +820,17 @@ def edit_schedule(request, num=None, owner=None, name=None):
})
SchedulePropertiesForm = modelform_factory(Schedule, fields=['name', 'notes', 'visible', 'public'])
class SchedulePropertiesForm(forms.ModelForm):
class Meta:
model = Schedule
fields = ['name', 'notes', 'visible', 'public', 'base']
def __init__(self, meeting, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['base'].queryset = self.fields['base'].queryset.filter(meeting=meeting)
if self.instance.pk is not None:
self.fields['base'].queryset = self.fields['base'].queryset.exclude(pk=self.instance.pk)
@role_required('Area Director','Secretariat')
def edit_schedule_properties(request, num, owner, name):
@ -816,14 +848,14 @@ def edit_schedule_properties(request, num, owner, name):
return HttpResponseForbidden("You may not edit this schedule")
if request.method == 'POST':
form = SchedulePropertiesForm(instance=schedule, data=request.POST)
form = SchedulePropertiesForm(meeting, instance=schedule, data=request.POST)
if form.is_valid():
form.save()
if request.GET.get('next'):
return HttpResponseRedirect(request.GET.get('next'))
return redirect('ietf.meeting.views.edit_schedule', num=num, owner=owner, name=name)
else:
form = SchedulePropertiesForm(instance=schedule)
form = SchedulePropertiesForm(meeting, instance=schedule)
return render(request, "meeting/properties_edit.html", {
"schedule": schedule,
@ -842,7 +874,7 @@ def list_schedules(request, num):
schedules = Schedule.objects.filter(
meeting=meeting
).prefetch_related('owner', 'assignments', 'origin', 'origin__assignments').order_by('owner', '-name', '-public').distinct()
).prefetch_related('owner', 'assignments', 'origin', 'origin__assignments', 'base').order_by('owner', '-name', '-public').distinct()
if not has_role(request.user, 'Secretariat'):
schedules = schedules.filter(Q(visible=True) | Q(owner=request.user.person))
@ -859,7 +891,7 @@ def list_schedules(request, num):
if s.origin:
s.changes_from_origin = len(diff_meeting_schedules(s.origin, s))
if s.pk == meeting.schedule_id:
if s in [meeting.schedule, meeting.schedule.base if meeting.schedule else None]:
official_schedules.append(s)
elif user_is_person(request.user, s.owner):
own_schedules.append(s)
@ -869,13 +901,13 @@ def list_schedules(request, num):
other_private_schedules.append(s)
schedule_groups = [
("Official Agenda", official_schedules),
("Own Draft Agendas", own_schedules),
("Other Draft Agendas", other_public_schedules),
("Other Private Draft Agendas", other_private_schedules),
(official_schedules, False, "Official Agenda"),
(own_schedules, True, "Own Draft Agendas"),
(other_public_schedules, False, "Other Draft Agendas"),
(other_private_schedules, False, "Other Private Draft Agendas"),
]
schedule_groups = [(label, sorted(l, reverse=True, key=lambda s: natural_sort_key(s.name))) for label, l in schedule_groups if l]
schedule_groups = [(sorted(l, reverse=True, key=lambda s: natural_sort_key(s.name)), own, *t) for l, own, *t in schedule_groups if l or own]
return render(request, "meeting/schedule_list.html", {
'meeting': meeting,
@ -967,7 +999,11 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
return render(request, "meeting/no-"+base+ext, {'meeting':meeting }, content_type=mimetype[ext])
updated = meeting.updated()
filtered_assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda'])
filtered_assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base]
).exclude(
timeslot__type__in=['lead', 'offagenda']
)
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
if ext == ".csv":
@ -1095,10 +1131,15 @@ def agenda_by_room(request, num=None, name=None, owner=None):
else:
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base if schedule else None]
).prefetch_related('timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent')
ss_by_day = OrderedDict()
for day in schedule.assignments.dates('timeslot__time','day'):
for day in assignments.dates('timeslot__time','day'):
ss_by_day[day]=[]
for ss in schedule.assignments.order_by('timeslot__location__functional_name','timeslot__location__name','timeslot__time'):
for ss in assignments.order_by('timeslot__location__functional_name','timeslot__location__name','timeslot__time'):
day = ss.timeslot.time.date()
ss_by_day[day].append(ss)
return render(request,"meeting/agenda_by_room.html",{"meeting":meeting,"schedule":schedule,"ss_by_day":ss_by_day})
@ -1111,7 +1152,12 @@ def agenda_by_type(request, num=None, type=None, name=None, owner=None):
else:
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
assignments = schedule.assignments.order_by('session__type__slug','timeslot__time','session__group__acronym')
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base if schedule else None]
).prefetch_related(
'timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent'
).order_by('session__type__slug','timeslot__time','session__group__acronym')
if type:
assignments = assignments.filter(session__type__slug=type)
return render(request,"meeting/agenda_by_type.html",{"meeting":meeting,"schedule":schedule,"assignments":assignments})
@ -1120,7 +1166,11 @@ def agenda_by_type(request, num=None, type=None, name=None, owner=None):
def agenda_by_type_ics(request,num=None,type=None):
meeting = get_meeting(num)
schedule = get_schedule(meeting)
assignments = schedule.assignments.order_by('session__type__slug','timeslot__time')
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base if schedule else None]
).prefetch_related(
'timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent'
).order_by('session__type__slug','timeslot__time')
if type:
assignments = assignments.filter(session__type__slug=type)
updated = meeting.updated()
@ -1240,7 +1290,11 @@ def week_view(request, num=None, name=None, owner=None):
if not schedule:
raise Http404
filtered_assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda'])
filtered_assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base]
).exclude(
timeslot__type__in=['lead','offagenda']
)
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
items = []
@ -1309,7 +1363,11 @@ def room_view(request, num=None, name=None, owner=None):
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
assignments = schedule.assignments.all()
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base if schedule else None]
).prefetch_related(
'timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent'
)
unavailable = meeting.timeslot_set.filter(type__slug='unavail')
if not (assignments.exists() or unavailable.exists()):
return HttpResponse("No sessions/timeslots available yet")
@ -1401,7 +1459,7 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
elif len(item) > 1 and item[0] == '~':
include_types |= set([item[1:]])
assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda'])
assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base]).exclude(timeslot__type__in=['lead','offagenda'])
assignments = preprocess_assignments_for_agenda(assignments, meeting)
if q:
@ -1427,13 +1485,17 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
}, content_type="text/calendar")
@cache_page(15 * 60)
def json_agenda(request, num=None ):
def json_agenda(request, num=None):
meeting = get_meeting(num, type_in=['ietf','interim'])
sessions = []
locations = set()
parent_acronyms = set()
assignments = meeting.schedule.assignments.exclude(session__type__in=['lead','offagenda','break','reg'])
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
).exclude(
session__type__in=['lead','offagenda','break','reg']
)
# Update the assignments with historic information, i.e., valid at the
# time of the meeting
assignments = preprocess_assignments_for_agenda(assignments, meeting, extra_prefetches=[
@ -1609,7 +1671,7 @@ def session_details(request, num, acronym):
session.historic_group.historic_parent = None
session.type_counter = Counter()
ss = session.timeslotassignments.filter(schedule=meeting.schedule).order_by('timeslot__time')
ss = session.timeslotassignments.filter(schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]).order_by('timeslot__time')
if ss:
if meeting.type_id == 'interim' and not (meeting.city or meeting.country):
session.times = [ x.timeslot.utc_start_time() for x in ss ]
@ -2320,15 +2382,23 @@ def make_schedule_official(request, num, owner, name):
schedule.public = True
schedule.visible = True
schedule.save()
if schedule.base and not (schedule.base.public and schedule.base.visible):
schedule.base.public = True
schedule.base.visible = True
schedule.base.save()
meeting.schedule = schedule
meeting.save()
return HttpResponseRedirect(reverse('ietf.meeting.views.list_schedules',kwargs={'num':num}))
if not schedule.public:
messages.warning(request,"This schedule will be made public as it is made official.")
if not schedule.visible:
messages.warning(request,"This schedule will be made visible as it is made official.")
if schedule.base:
if not schedule.base.public:
messages.warning(request,"The base schedule will be made public as it is made official.")
if not schedule.base.visible:
messages.warning(request,"The base schedule will be made visible as it is made official.")
return render(request, "meeting/make_schedule_official.html",
{ 'schedule' : schedule,
@ -2344,12 +2414,14 @@ def delete_schedule(request, num, owner, name):
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
if schedule.name=='Empty-Schedule':
return HttpResponseForbidden('You may not delete the default empty schedule')
# FIXME: we ought to put these checks in a function and only show
# the delete button if the checks pass
if schedule == meeting.schedule:
return HttpResponseForbidden('You may not delete the official schedule for %s'%meeting)
if Schedule.objects.filter(base=schedule).exists():
return HttpResponseForbidden('You may not delete a schedule serving as the base for other schedules')
if not ( has_role(request.user, 'Secretariat') or person.user == request.user ):
return HttpResponseForbidden("You may not delete other user's schedules")
@ -2660,10 +2732,12 @@ def interim_request_details(request, number):
return redirect(interim_pending)
first_session = sessions.first()
assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None])
return render(request, "meeting/interim_request_details.html", {
"meeting": meeting,
"sessions": sessions,
"assignments": assignments,
"group": first_session.group,
"requester": session_requested_by(first_session),
"session_status": current_session_status(first_session),
@ -2783,10 +2857,10 @@ def upcoming_ical(request):
today = datetime.date.today()
# get meetings starting 7 days ago -- we'll filter out sessions in the past further down
meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).order_by('date'))
meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).prefetch_related('schedule').order_by('date'))
assignments = list(SchedTimeSessAssignment.objects.filter(
schedule__meeting__schedule=F('schedule'),
schedule__in=[m.schedule_id for m in meetings] + [m.schedule.base_id for m in meetings if m.schedule],
session__in=[s.pk for m in meetings for s in m.sessions],
timeslot__time__gte=today,
).order_by(
@ -2879,7 +2953,7 @@ def proceedings(request, num=None):
sessions = add_event_info_to_session_qs(
Session.objects.filter(meeting__number=meeting.number)
).filter(
Q(timeslotassignments__schedule=schedule) | Q(current_status='notmeet')
Q(timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None]) | Q(current_status='notmeet')
).select_related().order_by('-current_status')
plenaries = sessions.filter(name__icontains='plenary').exclude(current_status='notmeet')
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu')
@ -3101,7 +3175,7 @@ def edit_timeslot_type(request, num, slot_id):
else:
form = TimeSlotTypeForm(instance=timeslot)
sessions = timeslot.sessions.filter(timeslotassignments__schedule=meeting.schedule)
sessions = timeslot.sessions.filter(timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None])
return render(request, 'meeting/edit_timeslot_type.html', {'timeslot':timeslot,'form':form,'sessions':sessions})

View file

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

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

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

View file

@ -232,9 +232,12 @@ def recording(request, meeting_num):
session.
'''
meeting = get_object_or_404(Meeting, number=meeting_num)
assignments = meeting.schedule.assignments.exclude(session__type__in=('reg','break')).order_by('session__group__acronym')
sessions = [ x.session for x in assignments ]
sessions = Session.objects.filter(
timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
).exclude(
type__in=['reg','break']
).order_by('group__acronym')
if request.method == 'POST':
form = RecordingForm(request.POST,meeting=meeting)
if form.is_valid():

View file

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

View file

@ -33,13 +33,17 @@
<td>{{ assignment.timeslot.location }}</td>
<td>{{ assignment.timeslot.show_location }}</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>
<td>
{% if not assignment.session.type.slug == "break" %}
<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>
{% endif %}
</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>
{% if assignment.schedule_id == schedule.pk %}
<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>
<td>
{% if assignment.session.type.slug != "break" %}
<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>
{% endif %}
</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>
{% endfor %}
</tbody>

View file

@ -52,7 +52,7 @@ def get_session(timeslot, schedule=None):
# todo, doesn't account for shared timeslot
if not schedule:
schedule = timeslot.meeting.schedule
qs = timeslot.sessions.filter(timeslotassignments__schedule=schedule) #.exclude(states__slug='deleted')
qs = timeslot.sessions.filter(timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None]) #.exclude(states__slug='deleted')
if qs:
return qs[0]
else:
@ -66,7 +66,7 @@ def get_timeslot(session, schedule=None):
'''
if not schedule:
schedule = session.meeting.schedule
ss = session.timeslotassignments.filter(schedule=schedule)
ss = session.timeslotassignments.filter(schedule__in=[schedule, schedule.base if schedule else None])
if ss:
return ss[0].timeslot
else:

View file

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

View file

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

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>
<ul class="sessionlist">
{% 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 %}
</ul>
</li>

View file

@ -29,7 +29,7 @@ li.daylistentry { margin-left:2em; font-weight: 400; }
{% block content %}
{% include "meeting/meeting_heading.html" with updated=meeting.updated selected="by-type" title_extra="by Session Type" %}
{% regroup assignments by session.type.slug as type_list %}
{% regroup assignments by session.type_id as type_list %}
<ul class="typelist">
{% for type in type_list %}
<li class="typelistentry {% cycle 'even' 'odd' %}">
@ -41,11 +41,11 @@ li.daylistentry { margin-left:2em; font-weight: 400; }
<h3>{{ day.grouper }}</h3>
<table class="sessiontable">
{% 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.get_hidden_location}}</td>
<td class="type-{{ss.session.type.slug}}">{{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 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=meeting.number acronym=ss.session.group.acronym %}">Materials</a>{% else %}&nbsp;{% endif %}</td>
</tr>
{% endfor %}
</table>

View file

@ -30,7 +30,7 @@
&middot;
{% 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;
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other Agendas</a>
@ -46,7 +46,7 @@
{% if not can_edit %}
&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 %}
</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 %}">
{{ session.scheduling_label }}
</div>

View file

@ -1,10 +1,10 @@
{% load ietf_filters %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW.
{% endif %}The {{ group.name }} ({{ group.acronym }}) {% if group.type.slug == 'wg' and group.state.slug == 'bof' %}BOF{% else %}{{group.type.name}}{% endif %} will hold
{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ meeting.schedule.assignments.first.timeslot.time | date:"H:i" }} to {{ meeting.schedule.assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ meeting.schedule.assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ meeting.schedule.assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}.
{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}.
{% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting.
{% for assignment in meeting.schedule.assignments.all %}Session {{ forloop.counter }}:
{% for assignment in assignments %}Session {{ forloop.counter }}:
{{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.utc_start_time | date:"H:i" }} to {{ assignment.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}
{% endfor %}{% endif %}
{% if meeting.city %}Meeting Location:

View file

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

View file

@ -7,14 +7,14 @@
{% block content %}
{% 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">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-default">Copy agenda</button>
<button type="submit" class="btn btn-primary">Create agenda</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -8,13 +8,13 @@
<script src="{% static 'ietf/bootstrap/js/bootstrap.min.js' %}"></script>
<script type="text/javascript">
{% autoescape off %}
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_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();
{% autoescape off %}
{% for slot in unavailable %}
if (room_names.indexOf("{{slot.get_hidden_location}}") >= 0 )
{
@ -24,7 +24,7 @@
{% for ss in assignments %}
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 %}
{% endautoescape %}
@ -353,6 +353,9 @@
e.style.padding=padding;
e.style.fontFamily="sans-serif";
e.style.fontSize="8pt";
if (items[i].from_base_schedule)
e.style.opacity = 0.5;
e.id=i;
e.onmouseover=resize_func(e,sess_top,room_left,

View file

@ -5,25 +5,34 @@
{% block content %}
{% origin %}
<h1>{% block title %}Possible Meeting Agendas for IETF {{ meeting.number }}{% endblock %}</h1>
{% comment %}
<div>
<p><a href="{% url "ietf.meeting.views.edit_timeslots" meeting.number %}">Edit Timeslots</a></p>
{% comment %}
<div>
<p><a href="{% url "ietf.meeting.views.edit_timeslots" meeting.number %}">Edit Timeslots</a></p>
</div>
{% endcomment %}
{% endcomment %}
<div>
{% for label, schedules in schedule_groups %}
{% for schedules, own, label in schedule_groups %}
<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">
<table class="table table-condensed table-striped">
<tr>
<th class="col-md-2">Name</th>
<th class="col-md-2">Owner</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">Public</th>
</tr>
@ -40,14 +49,19 @@
<td>{{ schedule.owner }}</td>
<td>
{% 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>
{% endif %}
</td>
<td>{{ schedule.notes|linebreaksbr }}</td>
<td>
{% if schedule.visible %}
<div class="label label-success">visible</div>
{% endif %}
</td>
<td>
{% if schedule.base %}
<a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.base.owner_email schedule.base.name %}">{{ schedule.base.name }}</a>
{% endif %}
</td>
<td>{{ schedule.notes|linebreaksbr }}</td>
<td>
{% if schedule.visible %}
<div class="label label-success">visible</div>
{% else %}
<div class="label label-danger">hidden</div>
{% endif %}