diff --git a/ietf/bin/create-break-sessions b/ietf/bin/create-break-sessions index 8ee8f1f2c..52ce044d8 100755 --- a/ietf/bin/create-break-sessions +++ b/ietf/bin/create-break-sessions @@ -33,11 +33,11 @@ for meeting in Meeting.objects.filter(type="ietf").order_by("date"): for schedule in meeting.schedule_set.all(): print " Checking for missing Break and Reg sessions in %s" % schedule for timeslot in meeting.timeslot_set.all(): - if timeslot.type_id == 'break': - assignment, created = ScheduleTimeslotSSessionAssignment.objects.get_or_create(timeslot=timeslot, session=brk, schedule=schedule) + if timeslot.type_id == 'break' and not (schedule.base and SchedTimeSessAssignment.objects.filter(timeslot=timeslot, session=brk, schedule=schedule.base).exists()): + assignment, created = SchedTimeSessAssignment.objects.get_or_create(timeslot=timeslot, session=brk, schedule=schedule) if created: print " Added %s break assignment" % timeslot - if timeslot.type_id == 'reg': - assignment, created = ScheduleTimeslotSSessionAssignment.objects.get_or_create(timeslot=timeslot, session=reg, schedule=schedule) + if timeslot.type_id == 'reg' and not (schedule.base and SchedTimeSessAssignment.objects.filter(timeslot=timeslot, session=reg, schedule=schedule.base).exists()): + assignment, created = SchedTimeSessAssignment.objects.get_or_create(timeslot=timeslot, session=reg, schedule=schedule) if created: print " Added %s registration assignment" % timeslot diff --git a/ietf/meeting/admin.py b/ietf/meeting/admin.py index f8477ce2f..59ac39b10 100644 --- a/ietf/meeting/admin.py +++ b/ietf/meeting/admin.py @@ -126,8 +126,8 @@ admin.site.register(SchedulingEvent, SchedulingEventAdmin) class ScheduleAdmin(admin.ModelAdmin): list_display = ["name", "meeting", "owner", "visible", "public", "badness"] - list_filter = ["meeting", ] - raw_id_fields = ["meeting", "owner", ] + list_filter = ["meeting"] + raw_id_fields = ["meeting", "owner", "origin", "base"] search_fields = ["meeting__number", "name", "owner__name"] ordering = ["-meeting", "name"] diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 18644f46e..b401cc9e5 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -143,13 +143,6 @@ def get_schedule(meeting, name=None): schedule = get_object_or_404(meeting.schedule_set, name=name) return schedule -def get_schedule_by_id(meeting, schedid): - if schedid is None: - schedule = meeting.schedule - else: - schedule = get_object_or_404(meeting.schedule_set, id=int(schedid)) - return schedule - # seems this belongs in ietf/person/utils.py? def get_person_by_email(email): # email == None may actually match people who haven't set an email! @@ -428,6 +421,11 @@ def get_announcement_initial(meeting, is_change=False): type = group.type.slug.upper() if group.type.slug == 'wg' and group.state.slug == 'bof': type = 'BOF' + + assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None] + ).order_by('timeslot__time') + initial['subject'] = '{name} ({acronym}) {type} {desc} Meeting: {date}{change}'.format( name=group.name, acronym=group.acronym, diff --git a/ietf/meeting/management/commands/create_dummy_meeting.py b/ietf/meeting/management/commands/create_dummy_meeting.py index bab20b847..375f9a9a9 100644 --- a/ietf/meeting/management/commands/create_dummy_meeting.py +++ b/ietf/meeting/management/commands/create_dummy_meeting.py @@ -85,8 +85,11 @@ class Command(BaseCommand): date=datetime.date(2019, 11, 16), days=7, ) - schedule = Schedule.objects.create(meeting=m, name='Empty-Schedule', owner_id=1, - visible=True, public=True) + base_schedule = Schedule.objects.create(meeting=m, name='base', owner_id=1, + visible=True, public=True) + + schedule = Schedule.objects.create(meeting=m, name='first1', owner_id=1, + visible=True, public=True, base=base_schedule) m.schedule = schedule m.save() diff --git a/ietf/meeting/migrations/0032_add_schedule_base.py b/ietf/meeting/migrations/0032_add_schedule_base.py new file mode 100644 index 000000000..2c41ebe68 --- /dev/null +++ b/ietf/meeting/migrations/0032_add_schedule_base.py @@ -0,0 +1,25 @@ +# Generated by Django 2.0.13 on 2020-08-07 09:30 + +from django.db import migrations +import django.db.models.deletion +import ietf.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0031_add_session_origin'), + ] + + operations = [ + migrations.AddField( + model_name='schedule', + name='base', + field=ietf.utils.models.ForeignKey(blank=True, help_text='Sessions scheduled in the base show up in this schedule.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='derivedschedule_set', to='meeting.Schedule'), + ), + migrations.AlterField( + model_name='schedule', + name='origin', + field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='meeting.Schedule'), + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 44dd3a27c..24f3832ea 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -291,7 +291,9 @@ class Meeting(models.Model): min_time = datetime.datetime(1970, 1, 1, 0, 0, 0) # should be Meeting.modified, but we don't have that timeslots_updated = self.timeslot_set.aggregate(Max('modified'))["modified__max"] or min_time sessions_updated = self.session_set.aggregate(Max('modified'))["modified__max"] or min_time - assignments_updated = (self.schedule.assignments.aggregate(Max('modified'))["modified__max"] or min_time) if self.schedule else min_time + assignments_updated = min_time + if self.schedule: + assignments_updated = SchedTimeSessAssignment.objects.filter(schedule__in=[self.schedule, self.schedule.base if self.schedule else None]).aggregate(Max('modified'))["modified__max"] or min_time ts = max(timeslots_updated, sessions_updated, assignments_updated) tz = pytz.timezone(settings.PRODUCTION_TIMEZONE) ts = tz.localize(ts) @@ -450,7 +452,7 @@ class TimeSlot(models.Model): @property def session(self): if not hasattr(self, "_session_cache"): - self._session_cache = self.sessions.filter(timeslotassignments__schedule=self.meeting.schedule).first() + self._session_cache = self.sessions.filter(timeslotassignments__schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting else None]).first() return self._session_cache @property @@ -626,7 +628,10 @@ class Schedule(models.Model): public = models.BooleanField(default=True, help_text="Allow others to see this agenda.") badness = models.IntegerField(null=True, blank=True) notes = models.TextField(blank=True) - origin = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL) + origin = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL, related_name="+") + base = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL, + help_text="Sessions scheduled in the base schedule show up in this schedule too.", related_name="derivedschedule_set", + limit_choices_to={'base': None}) # prevent the inheritance from being more than one layer deep (no recursion) def __str__(self): return u"%s:%s(%s)" % (self.meeting, self.name, self.owner) @@ -1047,7 +1052,7 @@ class Session(models.Model): ss0name = "(%s)" % SessionStatusName.objects.get(slug=status_id).name else: ss0name = "(unscheduled)" - ss = self.timeslotassignments.filter(schedule=self.meeting.schedule).order_by('timeslot__time') + ss = self.timeslotassignments.filter(schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting.schedule else None]).order_by('timeslot__time') if ss: ss0name = ','.join(x.timeslot.time.strftime("%a-%H%M") for x in ss) return "%s: %s %s %s" % (self.meeting, self.group.acronym, self.name, ss0name) @@ -1080,11 +1085,8 @@ class Session(models.Model): def reverse_constraints(self): return Constraint.objects.filter(target=self.group, meeting=self.meeting).order_by('name__name') - def timeslotassignment_for_schedule(self, schedule): - return self.timeslotassignments.filter(schedule=schedule).first() - def official_timeslotassignment(self): - return self.timeslotassignment_for_schedule(self.meeting.schedule) + return self.timeslotassignments.filter(schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting.schedule else None]).first() def constraints_dict(self, host_scheme): constraint_list = [] diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index f15bc3dfe..a9b8535c3 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -76,8 +76,9 @@ def make_meeting_test_data(meeting=None): if not meeting: meeting = Meeting.objects.get(number="72", type="ietf") - schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-schedule", visible=True, public=True) - unofficial_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-unofficial-schedule", visible=True, public=True) + base_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="base", visible=True, public=True) + schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-schedule", visible=True, public=True, base=base_schedule) + unofficial_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-unofficial-schedule", visible=True, public=True, base=base_schedule) # test room pname = RoomResourceName.objects.create(name='projector',slug='proj') @@ -146,7 +147,7 @@ def make_meeting_test_data(meeting=None): requested_duration=datetime.timedelta(minutes=480), type_id="reg") SchedulingEvent.objects.create(session=reg_session, status_id='schedw', by=system_person) - SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=schedule) + SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=base_schedule) # Break break_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"), @@ -154,7 +155,7 @@ def make_meeting_test_data(meeting=None): requested_duration=datetime.timedelta(minutes=30), type_id="break") SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person) - SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=schedule) + SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=base_schedule) meeting.schedule = schedule meeting.save() diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index 879dd1d73..09e5f449a 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -133,6 +133,8 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase): url = self.absreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email())) self.driver.get(url) + WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.edit-meeting-schedule'))) + self.assertEqual(len(self.driver.find_elements_by_css_selector('.session')), 3) # select - show session info diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 51493ecc8..ab1d4cf55 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -127,6 +127,8 @@ class MeetingTests(TestCase): future_meeting = Meeting.objects.create(date=datetime.date(future_year, 7, 22), number=future_num, type_id='ietf', city="Panama City", country="PA", time_zone='America/Panama') + registration_text = "Registration" + # utc time_interval = "%s-%s" % (slot.utc_start_time().strftime("%H:%M").lstrip("0"), (slot.utc_start_time() + slot.duration).strftime("%H:%M").lstrip("0")) @@ -152,6 +154,7 @@ class MeetingTests(TestCase): self.assertIn(session.group.parent.acronym.upper(), agenda_content) self.assertIn(slot.location.name, agenda_content) self.assertIn(time_interval, agenda_content) + self.assertIn(registration_text, agenda_content) # Make sure there's a frame for the agenda and it points to the right place self.assertTrue(any([session.materials.get(type='agenda').get_href() in x.attrib["data-src"] for x in q('tr div.modal-body div.frame')])) @@ -191,6 +194,7 @@ class MeetingTests(TestCase): self.assertContains(r, session.group.name) self.assertContains(r, session.group.parent.acronym.upper()) self.assertContains(r, slot.location.name) + self.assertContains(r, registration_text) self.assertContains(r, session.materials.get(type='agenda').uploaded_filename) self.assertContains(r, session.materials.filter(type='slides').exclude(states__type__slug='slides',states__slug='deleted').first().uploaded_filename) @@ -215,6 +219,7 @@ class MeetingTests(TestCase): self.assertNotContains(r, 'CANCELLED') self.assertContains(r, session.group.acronym) self.assertContains(r, slot.location.name) + self.assertContains(r, registration_text) # week view with a cancelled session SchedulingEvent.objects.create( @@ -585,16 +590,22 @@ class MeetingTests(TestCase): self.client.login(username='secretary',password='secretary+password') response = self.client.get(url) self.assertEqual(response.status_code,200) + + new_base = Schedule.objects.create(name="newbase", owner=schedule.owner, meeting=schedule.meeting) response = self.client.post(url, { 'name':schedule.name, 'visible':True, 'public':True, + 'notes': "New Notes", + 'base': new_base.pk, } ) - self.assertEqual(response.status_code,302) - schedule = Schedule.objects.get(pk=schedule.pk) + self.assertNoFormPostErrors(response) + schedule.refresh_from_db() self.assertTrue(schedule.visible) self.assertTrue(schedule.public) + self.assertEqual(schedule.notes, "New Notes") + self.assertEqual(schedule.base_id, new_base.pk) def test_agenda_by_type_ics(self): session=SessionFactory(meeting__type_id='ietf',type_id='lead') @@ -985,7 +996,21 @@ class EditTests(TestCase): person=p, name=ConstraintName.objects.get(slug="bethere"), ) - + + room = Room.objects.get(meeting=meeting, session_types='regular') + base_timeslot = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, + duration=datetime.timedelta(minutes=50), + time=datetime.datetime.combine(meeting.date + datetime.timedelta(days=2), datetime.time(9, 30))) + + timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('time')) + + base_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="irg"), + attendees=20, requested_duration=datetime.timedelta(minutes=30), + type_id='regular') + SchedulingEvent.objects.create(session=base_session, status_id='schedw', by=Person.objects.get(user__username='secretary')) + SchedTimeSessAssignment.objects.create(timeslot=base_timeslot, session=base_session, schedule=meeting.schedule.base) + + # check we have the grid and everything set up as a baseline - # the Javascript tests check that the Javascript can work with # it @@ -993,11 +1018,9 @@ class EditTests(TestCase): r = self.client.get(url) q = PyQuery(r.content) - room = Room.objects.get(meeting=meeting, session_types='regular') self.assertTrue(q(".room-name:contains(\"{}\")".format(room.name))) self.assertTrue(q(".room-name:contains(\"{}\")".format(room.capacity))) - timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular')) self.assertTrue(q("#timeslot{}".format(timeslots[0].pk))) for s in [s1, s2]: @@ -1031,12 +1054,14 @@ class EditTests(TestCase): if s.comments: self.assertIn(s.comments, e.find(".comments").text()) - formatted_constraints = e.find(".session-info .formatted-constraints > *") - if s == s1: - self.assertIn(s_other.group.acronym, formatted_constraints.eq(0).html()) - self.assertIn(p.name, formatted_constraints.eq(1).html()) - elif s == s2: - self.assertIn(p.name, formatted_constraints.eq(0).html()) + formatted_constraints1 = q("#session{} .session-info .formatted-constraints > *".format(s1.pk)) + self.assertIn(s2.group.acronym, formatted_constraints1.eq(0).html()) + self.assertIn(p.name, formatted_constraints1.eq(1).html()) + + formatted_constraints2 = q("#session{} .session-info .formatted-constraints > *".format(s2.pk)) + self.assertIn(p.name, formatted_constraints2.eq(0).html()) + + self.assertEqual(len(q("#session{}.readonly".format(base_session.pk))), 1) self.assertTrue(q("em:contains(\"You can't edit this schedule\")")) @@ -1106,7 +1131,7 @@ class EditTests(TestCase): self.assertEqual(tombstone_event.status_id, 'resched') self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s_tombstone).timeslot, timeslots[1]) - self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.tombstone".format(s_tombstone.pk)).html()) + self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.readonly".format(s_tombstone.pk)).html()) # unassign r = self.client.post(url, { @@ -1117,16 +1142,9 @@ class EditTests(TestCase): self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), []) # try swapping days - timeslots.append(TimeSlot.objects.create( - meeting=meeting, type_id='regular', location=timeslots[0].location, - duration=timeslots[0].duration - datetime.timedelta(minutes=5), - time=timeslots[0].time + datetime.timedelta(days=1), - )) - - SchedTimeSessAssignment.objects.create(schedule=schedule, session=s1, timeslot=timeslots[1]) - - self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[0])), 1) - self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[1])), 1) + SchedTimeSessAssignment.objects.create(schedule=schedule, session=s1, timeslot=timeslots[0]) + self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1) + self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[1])), 1) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), []) r = self.client.post(url, { @@ -1138,8 +1156,8 @@ class EditTests(TestCase): self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[0])), []) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), []) - self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), []) - self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[2])), 1) + self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[2])), 1) + self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2)), []) # swap back r = self.client.post(url, { @@ -1149,37 +1167,59 @@ class EditTests(TestCase): }) self.assertEqual(r.status_code, 302) - self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[0])), 1) + self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), []) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), []) - def test_copy_meeting_schedule(self): + def test_new_meeting_schedule(self): meeting = make_meeting_test_data() self.client.login(username="secretary", password="secretary+password") - url = urlreverse("ietf.meeting.views.copy_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name)) + # new from scratch + url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertEqual(r.status_code, 200) - # copy r = self.client.post(url, { - 'name': "newtest", + 'name': "scratch", 'public': "on", - 'notes': "New test", + 'visible': "on", + 'notes': "New scratch", + 'base': meeting.schedule.base_id, }) self.assertNoFormPostErrors(r) - new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='newtest') + new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='scratch') + self.assertEqual(new_schedule.public, True) + self.assertEqual(new_schedule.visible, True) + self.assertEqual(new_schedule.notes, "New scratch") + self.assertEqual(new_schedule.origin, None) + self.assertEqual(new_schedule.base_id, meeting.schedule.base_id) + + # copy + url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name)) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + + r = self.client.post(url, { + 'name': "copy", + 'public': "on", + 'notes': "New copy", + 'base': meeting.schedule.base_id, + }) + self.assertNoFormPostErrors(r) + + new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='copy') self.assertEqual(new_schedule.public, True) self.assertEqual(new_schedule.visible, False) - self.assertEqual(new_schedule.notes, "New test") + self.assertEqual(new_schedule.notes, "New copy") self.assertEqual(new_schedule.origin, meeting.schedule) + self.assertEqual(new_schedule.base_id, meeting.schedule.base_id) old_assignments = {(a.session_id, a.timeslot_id) for a in SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule)} for a in SchedTimeSessAssignment.objects.filter(schedule=new_schedule): self.assertIn((a.session_id, a.timeslot_id), old_assignments) - # FIXME: test extendedfrom is copied correctly def test_save_agenda_as_and_read_permissions(self): meeting = make_meeting_test_data() @@ -1390,7 +1430,7 @@ class SessionDetailsTests(TestCase): class EditScheduleListTests(TestCase): def setUp(self): self.mtg = MeetingFactory(type_id='ietf') - ScheduleFactory(meeting=self.mtg,name='Empty-Schedule') + ScheduleFactory(meeting=self.mtg, name='secretary1') def test_list_schedules(self): url = urlreverse('ietf.meeting.views.list_schedules',kwargs={'num':self.mtg.number}) @@ -1423,8 +1463,8 @@ class EditScheduleListTests(TestCase): ) # copy - copy_url = urlreverse("ietf.meeting.views.copy_meeting_schedule", kwargs=dict(num=meeting.number, owner=from_schedule.owner_email(), name=from_schedule.name)) - r = self.client.post(copy_url, { + new_url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=from_schedule.owner_email(), name=from_schedule.name)) + r = self.client.post(new_url, { 'name': "newtest", 'public': "on", }) @@ -1436,22 +1476,20 @@ class EditScheduleListTests(TestCase): edit_url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=to_schedule.owner_email(), name=to_schedule.name)) - # schedule + # schedule session r = self.client.post(edit_url, { 'action': 'assign', 'timeslot': slot3.pk, 'session': session3.pk, }) self.assertEqual(json.loads(r.content)['success'], True) - - # unschedule + # unschedule session r = self.client.post(edit_url, { 'action': 'unassign', 'session': session1.pk, }) self.assertEqual(json.loads(r.content)['success'], True) - - # move + # move session r = self.client.post(edit_url, { 'action': 'assign', 'timeslot': slot2.pk, @@ -1459,7 +1497,7 @@ class EditScheduleListTests(TestCase): }) self.assertEqual(json.loads(r.content)['success'], True) - # get differences + # now get differences r = self.client.get(url, { 'from_schedule': from_schedule.name, 'to_schedule': to_schedule.name, @@ -1584,6 +1622,7 @@ class InterimTests(TestCase): meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting meeting.time_zone = 'America/Los_Angeles' meeting.save() + url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number}) login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index 8a38f5655..4dcdc5ff6 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -41,7 +41,7 @@ type_ietf_only_patterns = [ url(r'^agenda/%(owner)s/%(schedule_name)s/session/(?P\d+).json$' % settings.URL_REGEXPS, ajax.assignment_json), url(r'^agenda/%(owner)s/%(schedule_name)s/sessions.json$' % settings.URL_REGEXPS, ajax.assignments_json), url(r'^agenda/%(owner)s/%(schedule_name)s.json$' % settings.URL_REGEXPS, ajax.schedule_infourl), - url(r'^agenda/%(owner)s/%(schedule_name)s/copy/$' % settings.URL_REGEXPS, views.copy_meeting_schedule), + url(r'^agenda/%(owner)s/%(schedule_name)s/new/$' % settings.URL_REGEXPS, views.new_meeting_schedule), url(r'^agenda/by-room$', views.agenda_by_room), url(r'^agenda/by-type$', views.agenda_by_type), url(r'^agenda/by-type/(?P[a-z]+)$', views.agenda_by_type), @@ -49,6 +49,7 @@ type_ietf_only_patterns = [ url(r'^agendas/list$', views.list_schedules), url(r'^agendas/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_schedules', permanent=True)), url(r'^agendas/diff/$', views.diff_schedules), + url(r'^agenda/new/$', views.new_meeting_schedule), url(r'^timeslots/edit$', views.edit_timeslots), url(r'^timeslot/(?P\d+)/edittype$', views.edit_timeslot_type), url(r'^rooms$', ajax.timeslot_roomsurl), diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index a3888fd0e..d54eed3d5 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -28,7 +28,7 @@ from ietf.person.models import Person, Email from ietf.secr.proceedings.proc_utils import import_audio_files def session_time_for_sorting(session, use_meeting_date): - official_timeslot = TimeSlot.objects.filter(sessionassignments__session=session, sessionassignments__schedule=session.meeting.schedule).first() + official_timeslot = TimeSlot.objects.filter(sessionassignments__session=session, sessionassignments__schedule__in=[session.meeting.schedule, session.meeting.schedule.base if session.meeting.schedule else None]).first() if official_timeslot: return official_timeslot.time elif use_meeting_date and session.meeting.date: diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index dfb02c6e1..21f0d39df 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -144,7 +144,7 @@ def materials(request, num=None): sessions = add_event_info_to_session_qs(Session.objects.filter( meeting__number=meeting.number, - timeslotassignments__schedule=schedule + timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None] ).distinct().select_related('meeting__schedule', 'group__state', 'group__parent')) plenaries = sessions.filter(name__icontains='plenary') @@ -297,6 +297,8 @@ def schedule_create(request, num=None, owner=None, name=None): newschedule = Schedule(name=savedname, owner=request.user.person, meeting=meeting, + base=schedule.base, + origin=schedule, visible=False, public=False) @@ -354,14 +356,15 @@ def edit_timeslots(request, num=None): "ts_list":ts_list, }) -class CopyScheduleForm(forms.ModelForm): +class NewScheduleForm(forms.ModelForm): class Meta: model = Schedule - fields = ['name', 'visible', 'public', 'notes'] + fields = ['name', 'visible', 'public', 'notes', 'base'] - def __init__(self, schedule, new_owner, *args, **kwargs): - super(CopyScheduleForm, self).__init__(*args, **kwargs) + def __init__(self, meeting, schedule, new_owner, *args, **kwargs): + super().__init__(*args, **kwargs) + self.meeting = meeting self.schedule = schedule self.new_owner = new_owner @@ -370,7 +373,7 @@ class CopyScheduleForm(forms.ModelForm): name_suggestion = username counter = 2 - existing_names = set(Schedule.objects.filter(meeting=schedule.meeting_id, owner=new_owner).values_list('name', flat=True)) + existing_names = set(Schedule.objects.filter(meeting=meeting, owner=new_owner).values_list('name', flat=True)) while name_suggestion in existing_names: name_suggestion = username + str(counter) counter += 1 @@ -378,55 +381,68 @@ class CopyScheduleForm(forms.ModelForm): self.fields['name'].initial = name_suggestion self.fields['name'].label = "Name of new agenda" + self.fields['base'].queryset = self.fields['base'].queryset.filter(meeting=meeting) + + if schedule: + self.fields['visible'].initial = schedule.visible + self.fields['public'].initial = schedule.public + self.fields['base'].queryset = self.fields['base'].queryset.exclude(pk=schedule.pk) + self.fields['base'].initial = schedule.base_id + else: + base = Schedule.objects.filter(meeting=meeting, name='base').first() + if base: + self.fields['base'].initial = base.pk + def clean_name(self): name = self.cleaned_data.get('name') - if name and Schedule.objects.filter(meeting=self.schedule.meeting_id, owner=self.new_owner, name=name): + if name and Schedule.objects.filter(meeting=self.meeting, owner=self.new_owner, name=name): raise forms.ValidationError("Schedule with this name already exists.") return name @role_required('Area Director','Secretariat') -def copy_meeting_schedule(request, num, owner, name): +def new_meeting_schedule(request, num, owner=None, name=None): meeting = get_meeting(num) - schedule = get_object_or_404(meeting.schedule_set, owner__email__address=owner, name=name) + schedule = get_schedule_by_name(meeting, get_person_by_email(owner), name) if request.method == 'POST': - form = CopyScheduleForm(schedule, request.user.person, request.POST) + form = NewScheduleForm(meeting, schedule, request.user.person, request.POST) if form.is_valid(): new_schedule = form.save(commit=False) - new_schedule.meeting = schedule.meeting + new_schedule.meeting = meeting new_schedule.owner = request.user.person new_schedule.origin = schedule new_schedule.save() - # keep a mapping so that extendedfrom references can be chased - old_pk_to_new_pk = {} - extendedfroms = {} - for assignment in schedule.assignments.all(): - extendedfrom_id = assignment.extendedfrom_id + if schedule: + # keep a mapping so that extendedfrom references can be chased + old_pk_to_new_pk = {} + extendedfroms = {} + for assignment in schedule.assignments.all(): + extendedfrom_id = assignment.extendedfrom_id - # clone by resetting primary key - old_pk = assignment.pk - assignment.pk = None - assignment.schedule = new_schedule - assignment.extendedfrom = None - assignment.save() + # clone by resetting primary key + old_pk = assignment.pk + assignment.pk = None + assignment.schedule = new_schedule + assignment.extendedfrom = None + assignment.save() - old_pk_to_new_pk[old_pk] = assignment.pk - if extendedfrom_id is not None: - extendedfroms[assignment.pk] = extendedfrom_id + old_pk_to_new_pk[old_pk] = assignment.pk + if extendedfrom_id is not None: + extendedfroms[assignment.pk] = extendedfrom_id - for pk, extendedfrom_id in extendedfroms.values(): - if extendedfrom_id in old_pk_to_new_pk: - SchedTimeSessAssignment.objects.filter(pk=pk).update(extendedfrom=old_pk_to_new_pk[extendedfrom_id]) + for pk, extendedfrom_id in extendedfroms.values(): + if extendedfrom_id in old_pk_to_new_pk: + SchedTimeSessAssignment.objects.filter(pk=pk).update(extendedfrom=old_pk_to_new_pk[extendedfrom_id]) # now redirect to this new schedule return redirect(edit_meeting_schedule, meeting.number, new_schedule.owner_email(), new_schedule.name) else: - form = CopyScheduleForm(schedule, request.user.person) + form = NewScheduleForm(meeting, schedule, request.user.person) - return render(request, "meeting/copy_meeting_schedule.html", { + return render(request, "meeting/new_meeting_schedule.html", { 'meeting': meeting, 'schedule': schedule, 'form': form, @@ -462,7 +478,15 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): "hide_menu": True }, status=403, content_type="text/html") - assignments = get_all_assignments_from_schedule(schedule) + assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=[schedule, schedule.base], + timeslot__location__isnull=False, + session__type='regular', + ).order_by('timeslot__time','timeslot__name') + + assignments_by_session = defaultdict(list) + for a in assignments: + assignments_by_session[a.session_id].append(a) rooms = meeting.room_set.filter(session_types__slug='regular').distinct().order_by("capacity") @@ -483,7 +507,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): 'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups', ) - timeslots_qs = meeting.timeslot_set.filter(type='regular').prefetch_related('type', 'sessions').order_by('location', 'time', 'name') + timeslots_qs = TimeSlot.objects.filter(meeting=meeting, type='regular').prefetch_related('type').order_by('location', 'time', 'name') min_duration = min(t.duration for t in timeslots_qs) max_duration = max(t.duration for t in timeslots_qs) @@ -549,7 +573,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): s.other_sessions = [s_other for s_other in sessions_for_group.get(s.group_id) if s != s_other] - s.is_tombstone = s.current_status in tombstone_states + s.readonly = s.current_status in tombstone_states or any(a.schedule_id != schedule.pk for a in assignments_by_session.get(s.pk, [])) if request.method == 'POST': @@ -622,7 +646,9 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): source_day = swap_days_form.cleaned_data['source_day'] target_day = swap_days_form.cleaned_data['target_day'] - swap_meeting_schedule_timeslot_assignments(schedule, [ts for ts in timeslots_qs if ts.time.date() == source_day], [ts for ts in timeslots_qs if ts.time.date() == target_day], target_day - source_day) + source_timeslots = [ts for ts in timeslots_qs if ts.time.date() == source_day] + target_timeslots = [ts for ts in timeslots_qs if ts.time.date() == target_day] + swap_meeting_schedule_timeslot_assignments(schedule, source_timeslots, target_timeslots, target_day - source_day) return HttpResponseRedirect(request.get_full_path()) @@ -670,10 +696,6 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): ts.session_assignments = [] timeslots_by_pk = {ts.pk: ts for ts in timeslots_qs} - assignments_by_session = defaultdict(list) - for a in assignments: - assignments_by_session[a.session_id].append(a) - unassigned_sessions = [] for s in sessions: assigned = False @@ -798,7 +820,17 @@ def edit_schedule(request, num=None, owner=None, name=None): }) -SchedulePropertiesForm = modelform_factory(Schedule, fields=['name', 'notes', 'visible', 'public']) +class SchedulePropertiesForm(forms.ModelForm): + class Meta: + model = Schedule + fields = ['name', 'notes', 'visible', 'public', 'base'] + + def __init__(self, meeting, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields['base'].queryset = self.fields['base'].queryset.filter(meeting=meeting) + if self.instance.pk is not None: + self.fields['base'].queryset = self.fields['base'].queryset.exclude(pk=self.instance.pk) @role_required('Area Director','Secretariat') def edit_schedule_properties(request, num, owner, name): @@ -816,14 +848,14 @@ def edit_schedule_properties(request, num, owner, name): return HttpResponseForbidden("You may not edit this schedule") if request.method == 'POST': - form = SchedulePropertiesForm(instance=schedule, data=request.POST) + form = SchedulePropertiesForm(meeting, instance=schedule, data=request.POST) if form.is_valid(): form.save() if request.GET.get('next'): return HttpResponseRedirect(request.GET.get('next')) return redirect('ietf.meeting.views.edit_schedule', num=num, owner=owner, name=name) else: - form = SchedulePropertiesForm(instance=schedule) + form = SchedulePropertiesForm(meeting, instance=schedule) return render(request, "meeting/properties_edit.html", { "schedule": schedule, @@ -842,7 +874,7 @@ def list_schedules(request, num): schedules = Schedule.objects.filter( meeting=meeting - ).prefetch_related('owner', 'assignments', 'origin', 'origin__assignments').order_by('owner', '-name', '-public').distinct() + ).prefetch_related('owner', 'assignments', 'origin', 'origin__assignments', 'base').order_by('owner', '-name', '-public').distinct() if not has_role(request.user, 'Secretariat'): schedules = schedules.filter(Q(visible=True) | Q(owner=request.user.person)) @@ -859,7 +891,7 @@ def list_schedules(request, num): if s.origin: s.changes_from_origin = len(diff_meeting_schedules(s.origin, s)) - if s.pk == meeting.schedule_id: + if s in [meeting.schedule, meeting.schedule.base if meeting.schedule else None]: official_schedules.append(s) elif user_is_person(request.user, s.owner): own_schedules.append(s) @@ -869,13 +901,13 @@ def list_schedules(request, num): other_private_schedules.append(s) schedule_groups = [ - ("Official Agenda", official_schedules), - ("Own Draft Agendas", own_schedules), - ("Other Draft Agendas", other_public_schedules), - ("Other Private Draft Agendas", other_private_schedules), + (official_schedules, False, "Official Agenda"), + (own_schedules, True, "Own Draft Agendas"), + (other_public_schedules, False, "Other Draft Agendas"), + (other_private_schedules, False, "Other Private Draft Agendas"), ] - schedule_groups = [(label, sorted(l, reverse=True, key=lambda s: natural_sort_key(s.name))) for label, l in schedule_groups if l] + schedule_groups = [(sorted(l, reverse=True, key=lambda s: natural_sort_key(s.name)), own, *t) for l, own, *t in schedule_groups if l or own] return render(request, "meeting/schedule_list.html", { 'meeting': meeting, @@ -967,7 +999,11 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc="" return render(request, "meeting/no-"+base+ext, {'meeting':meeting }, content_type=mimetype[ext]) updated = meeting.updated() - filtered_assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda']) + filtered_assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=[schedule, schedule.base] + ).exclude( + timeslot__type__in=['lead', 'offagenda'] + ) filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting) if ext == ".csv": @@ -1095,10 +1131,15 @@ def agenda_by_room(request, num=None, name=None, owner=None): else: person = get_person_by_email(owner) schedule = get_schedule_by_name(meeting, person, name) + + assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=[schedule, schedule.base if schedule else None] + ).prefetch_related('timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent') + ss_by_day = OrderedDict() - for day in schedule.assignments.dates('timeslot__time','day'): + for day in assignments.dates('timeslot__time','day'): ss_by_day[day]=[] - for ss in schedule.assignments.order_by('timeslot__location__functional_name','timeslot__location__name','timeslot__time'): + for ss in assignments.order_by('timeslot__location__functional_name','timeslot__location__name','timeslot__time'): day = ss.timeslot.time.date() ss_by_day[day].append(ss) return render(request,"meeting/agenda_by_room.html",{"meeting":meeting,"schedule":schedule,"ss_by_day":ss_by_day}) @@ -1111,7 +1152,12 @@ def agenda_by_type(request, num=None, type=None, name=None, owner=None): else: person = get_person_by_email(owner) schedule = get_schedule_by_name(meeting, person, name) - assignments = schedule.assignments.order_by('session__type__slug','timeslot__time','session__group__acronym') + assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=[schedule, schedule.base if schedule else None] + ).prefetch_related( + 'timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent' + ).order_by('session__type__slug','timeslot__time','session__group__acronym') + if type: assignments = assignments.filter(session__type__slug=type) return render(request,"meeting/agenda_by_type.html",{"meeting":meeting,"schedule":schedule,"assignments":assignments}) @@ -1120,7 +1166,11 @@ def agenda_by_type(request, num=None, type=None, name=None, owner=None): def agenda_by_type_ics(request,num=None,type=None): meeting = get_meeting(num) schedule = get_schedule(meeting) - assignments = schedule.assignments.order_by('session__type__slug','timeslot__time') + assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=[schedule, schedule.base if schedule else None] + ).prefetch_related( + 'timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent' + ).order_by('session__type__slug','timeslot__time') if type: assignments = assignments.filter(session__type__slug=type) updated = meeting.updated() @@ -1240,7 +1290,11 @@ def week_view(request, num=None, name=None, owner=None): if not schedule: raise Http404 - filtered_assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda']) + filtered_assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=[schedule, schedule.base] + ).exclude( + timeslot__type__in=['lead','offagenda'] + ) filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting) items = [] @@ -1309,7 +1363,11 @@ def room_view(request, num=None, name=None, owner=None): person = get_person_by_email(owner) schedule = get_schedule_by_name(meeting, person, name) - assignments = schedule.assignments.all() + assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=[schedule, schedule.base if schedule else None] + ).prefetch_related( + 'timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent' + ) unavailable = meeting.timeslot_set.filter(type__slug='unavail') if not (assignments.exists() or unavailable.exists()): return HttpResponse("No sessions/timeslots available yet") @@ -1401,7 +1459,7 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None): elif len(item) > 1 and item[0] == '~': include_types |= set([item[1:]]) - assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda']) + assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base]).exclude(timeslot__type__in=['lead','offagenda']) assignments = preprocess_assignments_for_agenda(assignments, meeting) if q: @@ -1427,13 +1485,17 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None): }, content_type="text/calendar") @cache_page(15 * 60) -def json_agenda(request, num=None ): +def json_agenda(request, num=None): meeting = get_meeting(num, type_in=['ietf','interim']) sessions = [] locations = set() parent_acronyms = set() - assignments = meeting.schedule.assignments.exclude(session__type__in=['lead','offagenda','break','reg']) + assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None] + ).exclude( + session__type__in=['lead','offagenda','break','reg'] + ) # Update the assignments with historic information, i.e., valid at the # time of the meeting assignments = preprocess_assignments_for_agenda(assignments, meeting, extra_prefetches=[ @@ -1609,7 +1671,7 @@ def session_details(request, num, acronym): session.historic_group.historic_parent = None session.type_counter = Counter() - ss = session.timeslotassignments.filter(schedule=meeting.schedule).order_by('timeslot__time') + ss = session.timeslotassignments.filter(schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]).order_by('timeslot__time') if ss: if meeting.type_id == 'interim' and not (meeting.city or meeting.country): session.times = [ x.timeslot.utc_start_time() for x in ss ] @@ -2320,15 +2382,23 @@ def make_schedule_official(request, num, owner, name): schedule.public = True schedule.visible = True schedule.save() + if schedule.base and not (schedule.base.public and schedule.base.visible): + schedule.base.public = True + schedule.base.visible = True + schedule.base.save() meeting.schedule = schedule meeting.save() return HttpResponseRedirect(reverse('ietf.meeting.views.list_schedules',kwargs={'num':num})) if not schedule.public: messages.warning(request,"This schedule will be made public as it is made official.") - if not schedule.visible: messages.warning(request,"This schedule will be made visible as it is made official.") + if schedule.base: + if not schedule.base.public: + messages.warning(request,"The base schedule will be made public as it is made official.") + if not schedule.base.visible: + messages.warning(request,"The base schedule will be made visible as it is made official.") return render(request, "meeting/make_schedule_official.html", { 'schedule' : schedule, @@ -2344,12 +2414,14 @@ def delete_schedule(request, num, owner, name): person = get_person_by_email(owner) schedule = get_schedule_by_name(meeting, person, name) - if schedule.name=='Empty-Schedule': - return HttpResponseForbidden('You may not delete the default empty schedule') - + # FIXME: we ought to put these checks in a function and only show + # the delete button if the checks pass if schedule == meeting.schedule: return HttpResponseForbidden('You may not delete the official schedule for %s'%meeting) + if Schedule.objects.filter(base=schedule).exists(): + return HttpResponseForbidden('You may not delete a schedule serving as the base for other schedules') + if not ( has_role(request.user, 'Secretariat') or person.user == request.user ): return HttpResponseForbidden("You may not delete other user's schedules") @@ -2660,10 +2732,12 @@ def interim_request_details(request, number): return redirect(interim_pending) first_session = sessions.first() + assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]) return render(request, "meeting/interim_request_details.html", { "meeting": meeting, "sessions": sessions, + "assignments": assignments, "group": first_session.group, "requester": session_requested_by(first_session), "session_status": current_session_status(first_session), @@ -2783,10 +2857,10 @@ def upcoming_ical(request): today = datetime.date.today() # get meetings starting 7 days ago -- we'll filter out sessions in the past further down - meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).order_by('date')) + meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).prefetch_related('schedule').order_by('date')) assignments = list(SchedTimeSessAssignment.objects.filter( - schedule__meeting__schedule=F('schedule'), + schedule__in=[m.schedule_id for m in meetings] + [m.schedule.base_id for m in meetings if m.schedule], session__in=[s.pk for m in meetings for s in m.sessions], timeslot__time__gte=today, ).order_by( @@ -2879,7 +2953,7 @@ def proceedings(request, num=None): sessions = add_event_info_to_session_qs( Session.objects.filter(meeting__number=meeting.number) ).filter( - Q(timeslotassignments__schedule=schedule) | Q(current_status='notmeet') + Q(timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None]) | Q(current_status='notmeet') ).select_related().order_by('-current_status') plenaries = sessions.filter(name__icontains='plenary').exclude(current_status='notmeet') ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu') @@ -3101,7 +3175,7 @@ def edit_timeslot_type(request, num, slot_id): else: form = TimeSlotTypeForm(instance=timeslot) - sessions = timeslot.sessions.filter(timeslotassignments__schedule=meeting.schedule) + sessions = timeslot.sessions.filter(timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]) return render(request, 'meeting/edit_timeslot_type.html', {'timeslot':timeslot,'form':form,'sessions':sessions}) diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py index 40d2ac9f4..526488a9e 100644 --- a/ietf/secr/meetings/tests.py +++ b/ietf/secr/meetings/tests.py @@ -65,7 +65,7 @@ class SecrMeetingTestCase(TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) q = PyQuery(response.content) - self.assertEqual(len(q('#id_schedule_selector option')),3) + self.assertEqual({option.get('value') for option in q('#id_schedule_selector option:not([value=""])')}, {'base', 'test-schedule', 'test-unofficial-schedule'}) def test_add_meeting(self): "Add Meeting" @@ -92,6 +92,9 @@ class SecrMeetingTestCase(TestCase): new_meeting = Meeting.objects.get(number=number) self.assertTrue(new_meeting.schedule) + self.assertEqual(new_meeting.schedule.name, 'secretary1') + self.assertTrue(new_meeting.schedule.base) + self.assertEqual(new_meeting.schedule.base.name, 'base') self.assertEqual(new_meeting.attendees, None) def test_edit_meeting(self): @@ -197,8 +200,7 @@ class SecrMeetingTestCase(TestCase): # test delete # first unschedule sessions so we can delete - SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule).delete() - SchedTimeSessAssignment.objects.filter(schedule=meeting.unofficial_schedule).delete() + SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base, meeting.unofficial_schedule]).delete() self.client.login(username="secretary", password="secretary+password") post_dict = { 'room-TOTAL_FORMS': q('input[name="room-TOTAL_FORMS"]').val(), @@ -339,27 +341,29 @@ class SecrMeetingTestCase(TestCase): def test_meetings_misc_session_delete(self): meeting = make_meeting_test_data() - slot = meeting.schedule.assignments.filter(timeslot__type='reg').first().timeslot - url = reverse('ietf.secr.meetings.views.misc_session_delete', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id}) - target = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name}) + schedule = meeting.schedule.base + slot = schedule.assignments.filter(timeslot__type='reg').first().timeslot + url = reverse('ietf.secr.meetings.views.misc_session_delete', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name,'slot_id':slot.id}) + target = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) response = self.client.post(url, {'post':'yes'}) self.assertRedirects(response, target) - self.assertFalse(meeting.schedule.assignments.filter(timeslot=slot)) + self.assertFalse(schedule.assignments.filter(timeslot=slot)) def test_meetings_misc_session_cancel(self): meeting = make_meeting_test_data() - slot = meeting.schedule.assignments.filter(timeslot__type='reg').first().timeslot - url = reverse('ietf.secr.meetings.views.misc_session_cancel', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id}) - redirect_url = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name}) + schedule = meeting.schedule.base + slot = schedule.assignments.filter(timeslot__type='reg').first().timeslot + url = reverse('ietf.secr.meetings.views.misc_session_cancel', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name,'slot_id':slot.id}) + redirect_url = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) response = self.client.post(url, {'post':'yes'}) self.assertRedirects(response, redirect_url) - session = slot.sessionassignments.filter(schedule=meeting.schedule).first().session + session = slot.sessionassignments.filter(schedule=schedule).first().session self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'canceled') def test_meetings_regular_session_edit(self): diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index 8e79efdb6..13c7c33f6 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -22,7 +22,6 @@ from ietf.meeting.utils import add_event_info_to_session_qs from ietf.meeting.utils import only_sessions_that_can_meet from ietf.name.models import SessionStatusName from ietf.group.models import Group, GroupEvent -from ietf.person.models import Person from ietf.secr.meetings.blue_sheets import create_blue_sheets from ietf.secr.meetings.forms import ( BaseMeetingRoomFormSet, MeetingModelForm, MeetingSelectForm, MeetingRoomForm, MiscSessionForm, TimeSlotForm, RegularSessionEditForm, @@ -85,21 +84,19 @@ def check_misc_sessions(meeting,schedule): Ensure misc session timeslots exist and have appropriate SchedTimeSessAssignment objects for the specified schedule. ''' + # FIXME: this is a legacy function: delete it once base schedules are rolled out + + if Schedule.objects.filter(meeting=meeting, base__isnull=False).exists(): + return + slots = TimeSlot.objects.filter(meeting=meeting,type__in=('break','reg','other','plenary','lead','offagenda')) plenary = slots.filter(type='plenary').first() if plenary: assignments = plenary.sessionassignments.all() if not assignments.filter(schedule=schedule): source = assignments.first().schedule - copy_assignments(slots,source,schedule) - -def copy_assignments(slots,source,target): - ''' - Copy SchedTimeSessAssignment objects from source schedule to target schedule. Slots is - a queryset of slots - ''' - for ss in SchedTimeSessAssignment.objects.filter(schedule=source,timeslot__in=slots): - SchedTimeSessAssignment.objects.create(schedule=target,session=ss.session,timeslot=ss.timeslot) + for ss in SchedTimeSessAssignment.objects.filter(schedule=source,timeslot__in=slots): + SchedTimeSessAssignment.objects.create(schedule=schedule,session=ss.session,timeslot=ss.timeslot) def get_last_meeting(meeting): last_number = int(meeting.number) - 1 @@ -221,13 +218,23 @@ def add(request): if form.is_valid(): meeting = form.save() + base_schedule = Schedule.objects.create( + meeting=meeting, + name='base', + owner=request.user.person, + visible=True, + public=True + ) + schedule = Schedule.objects.create(meeting = meeting, - name = 'Empty-Schedule', - owner = Person.objects.get(name='(System)'), + name = "{}1".format(request.user.username), + owner = request.user.person, visible = True, - public = True) + public = True, + base = base_schedule, + ) meeting.schedule = schedule - + # we want to carry session request lock status over from previous meeting previous_meeting = get_meeting( int(meeting.number) - 1 ) meeting.session_request_lock_message = previous_meeting.session_request_lock_message @@ -296,7 +303,7 @@ def blue_sheet_generate(request, meeting_id): # TODO: Why aren't 'ag' in here as well? groups = Group.objects.filter( type__in=['wg','rg'], - session__timeslotassignments__schedule=meeting.schedule).order_by('acronym') + session__timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]).order_by('acronym') create_blue_sheets(meeting, groups) messages.success(request, 'Blue Sheets generated') @@ -380,7 +387,7 @@ def misc_sessions(request, meeting_id, schedule_name): check_misc_sessions(meeting,schedule) misc_session_types = ['break','reg','other','plenary','lead'] - assignments = schedule.assignments.filter(timeslot__type__in=misc_session_types) + assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base], timeslot__type__in=misc_session_types) assignments = assignments.order_by('-timeslot__type__name','timeslot__time') if request.method == 'POST': @@ -571,7 +578,7 @@ def notifications(request, meeting_id): meeting = get_object_or_404(Meeting, number=meeting_id) last_notice = GroupEvent.objects.filter(type='sent_notification').first() groups = set() - for ss in meeting.schedule.assignments.filter(timeslot__type='regular'): + for ss in SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None], timeslot__type='regular'): last_notice = ss.session.group.latest_event(type='sent_notification') if last_notice and ss.modified > last_notice.time: groups.add(ss.session.group) @@ -652,7 +659,7 @@ def regular_sessions(request, meeting_id, schedule_name): schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name) sessions = add_event_info_to_session_qs( - only_sessions_that_can_meet(schedule.meeting.session_set) + only_sessions_that_can_meet(meeting.session_set) ).order_by('group__acronym') if request.method == 'POST': diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py index 9e49c7ac8..d6dfce910 100644 --- a/ietf/secr/proceedings/proc_utils.py +++ b/ietf/secr/proceedings/proc_utils.py @@ -32,11 +32,10 @@ VIDEO_TITLE_RE = re.compile(r'IETF(?P[\d]+)-(?P.*)-(?P\d{8}) def _get_session(number,name,date,time): '''Lookup session using data from video title''' meeting = Meeting.objects.get(number=number) - schedule = meeting.schedule timeslot_time = datetime.datetime.strptime(date + time,'%Y%m%d%H%M') try: assignment = SchedTimeSessAssignment.objects.get( - schedule = schedule, + schedule__in = [meeting.schedule, meeting.schedule.base], session__group__acronym = name.lower(), timeslot__time = timeslot_time, ) @@ -108,7 +107,7 @@ def get_timeslot_for_filename(filename): meeting=meeting, location__name=room_mapping[match.groupdict()['room']], time=time, - sessionassignments__schedule=meeting.schedule, + sessionassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None], ).distinct() uncancelled_slots = [t for t in slots if not add_event_info_to_session_qs(t.sessions.all()).filter(current_status='canceled').exists()] return uncancelled_slots[0] diff --git a/ietf/secr/proceedings/views.py b/ietf/secr/proceedings/views.py index 916118217..c1fa06cb5 100644 --- a/ietf/secr/proceedings/views.py +++ b/ietf/secr/proceedings/views.py @@ -232,9 +232,12 @@ def recording(request, meeting_num): session. ''' meeting = get_object_or_404(Meeting, number=meeting_num) - assignments = meeting.schedule.assignments.exclude(session__type__in=('reg','break')).order_by('session__group__acronym') - sessions = [ x.session for x in assignments ] - + sessions = Session.objects.filter( + timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None] + ).exclude( + type__in=['reg','break'] + ).order_by('group__acronym') + if request.method == 'POST': form = RecordingForm(request.POST,meeting=meeting) if form.is_valid(): diff --git a/ietf/secr/static/secr/css/custom.css b/ietf/secr/static/secr/css/custom.css index 94cbf48cb..c3cc4078a 100644 --- a/ietf/secr/static/secr/css/custom.css +++ b/ietf/secr/static/secr/css/custom.css @@ -461,6 +461,11 @@ input.draft-file-input { Meeting Tool ========================================================================== */ +#misc-sessions .from-base-schedule { + text-align: centeR; + opacity: 0.7; +} + #misc-session-edit-form input[type="text"] { width: 30em; } diff --git a/ietf/secr/templates/meetings/misc_sessions.html b/ietf/secr/templates/meetings/misc_sessions.html index cb2545ae8..c946c338e 100644 --- a/ietf/secr/templates/meetings/misc_sessions.html +++ b/ietf/secr/templates/meetings/misc_sessions.html @@ -33,13 +33,17 @@ {{ assignment.timeslot.location }} {{ assignment.timeslot.show_location }} {{ assignment.timeslot.type }} - Edit - - {% if not assignment.session.type.slug == "break" %} - Cancel - {% endif %} - - Delete + {% if assignment.schedule_id == schedule.pk %} + Edit + + {% if assignment.session.type.slug != "break" %} + Cancel + {% endif %} + + Delete + {% else %} + (from base schedule) + {% endif %} {% endfor %} diff --git a/ietf/secr/utils/meeting.py b/ietf/secr/utils/meeting.py index d3e41831e..63c4a6dcc 100644 --- a/ietf/secr/utils/meeting.py +++ b/ietf/secr/utils/meeting.py @@ -52,7 +52,7 @@ def get_session(timeslot, schedule=None): # todo, doesn't account for shared timeslot if not schedule: schedule = timeslot.meeting.schedule - qs = timeslot.sessions.filter(timeslotassignments__schedule=schedule) #.exclude(states__slug='deleted') + qs = timeslot.sessions.filter(timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None]) #.exclude(states__slug='deleted') if qs: return qs[0] else: @@ -66,7 +66,7 @@ def get_timeslot(session, schedule=None): ''' if not schedule: schedule = session.meeting.schedule - ss = session.timeslotassignments.filter(schedule=schedule) + ss = session.timeslotassignments.filter(schedule__in=[schedule, schedule.base if schedule else None]) if ss: return ss[0].timeslot else: diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css index 8bde6d099..8870f923c 100644 --- a/ietf/static/ietf/css/ietf.css +++ b/ietf/static/ietf/css/ietf.css @@ -1010,6 +1010,9 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { margin-left: 0.2em; } +.from-base-schedule { + opacity: 0.7; +} /* === Edit Meeting Schedule ====================================== */ @@ -1138,7 +1141,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { cursor: default; } -.edit-meeting-schedule .session.tombstone { +.edit-meeting-schedule .session.readonly { cursor: default; background-color: #ddd; } diff --git a/ietf/static/ietf/js/edit-meeting-schedule.js b/ietf/static/ietf/js/edit-meeting-schedule.js index 82f531d5c..75f307199 100644 --- a/ietf/static/ietf/js/edit-meeting-schedule.js +++ b/ietf/static/ietf/js/edit-meeting-schedule.js @@ -8,7 +8,7 @@ jQuery(document).ready(function () { alert("Error: " + errorText); } - let sessions = content.find(".session").not(".tombstone"); + let sessions = content.find(".session").not(".readonly"); let timeslots = content.find(".timeslot"); let days = content.find(".day-flow .day"); diff --git a/ietf/templates/meeting/agenda_by_room.html b/ietf/templates/meeting/agenda_by_room.html index 5d67c8ec2..2e0a68bd7 100644 --- a/ietf/templates/meeting/agenda_by_room.html +++ b/ietf/templates/meeting/agenda_by_room.html @@ -29,7 +29,7 @@ ul.sessionlist { list-style:none; padding-left:2em; margin-bottom:10px;}
  • {{room.grouper|default:"Location Unavailable"}}

      {% for ss in room.list %} -
    • {{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}
    • +
    • {{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}
    • {% endfor %}
  • diff --git a/ietf/templates/meeting/agenda_by_type.html b/ietf/templates/meeting/agenda_by_type.html index afe1eb462..84b4258ac 100644 --- a/ietf/templates/meeting/agenda_by_type.html +++ b/ietf/templates/meeting/agenda_by_type.html @@ -29,7 +29,7 @@ li.daylistentry { margin-left:2em; font-weight: 400; } {% block content %} {% include "meeting/meeting_heading.html" with updated=meeting.updated selected="by-type" title_extra="by Session Type" %} -{% regroup assignments by session.type.slug as type_list %} +{% regroup assignments by session.type_id as type_list %}
      {% for type in type_list %}
    • @@ -41,11 +41,11 @@ li.daylistentry { margin-left:2em; font-weight: 400; }

      {{ day.grouper }}

      {% for ss in day.list %} - + - - + + {% endfor %}
      {{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.timeslot.get_hidden_location}}{{ss.session.short_name}} {% if ss.session.type_id == 'regular' or ss.session.type_id == 'plenary' or ss.session.type_id == 'other' %} Materials{% else %} {% endif %}{{ss.session.short_name}} {% if ss.session.type_id == 'regular' or ss.session.type_id == 'plenary' or ss.session.type_id == 'other' %} Materials{% else %} {% endif %}
      diff --git a/ietf/templates/meeting/edit_meeting_schedule.html b/ietf/templates/meeting/edit_meeting_schedule.html index 71321a193..d32eb2654 100644 --- a/ietf/templates/meeting/edit_meeting_schedule.html +++ b/ietf/templates/meeting/edit_meeting_schedule.html @@ -30,7 +30,7 @@ · {% endif %} - Copy agenda + New agenda · Other Agendas @@ -46,7 +46,7 @@ {% if not can_edit %} · - You can't edit this schedule. Take a copy first. + You can't edit this schedule. Make a new agenda from this. {% endif %}

      diff --git a/ietf/templates/meeting/edit_meeting_schedule_session.html b/ietf/templates/meeting/edit_meeting_schedule_session.html index 01f6cce1e..a5a5d5950 100644 --- a/ietf/templates/meeting/edit_meeting_schedule_session.html +++ b/ietf/templates/meeting/edit_meeting_schedule_session.html @@ -1,4 +1,4 @@ -
      +
      {{ session.scheduling_label }}
      diff --git a/ietf/templates/meeting/interim_announcement.txt b/ietf/templates/meeting/interim_announcement.txt index be9608d66..d832b7be7 100644 --- a/ietf/templates/meeting/interim_announcement.txt +++ b/ietf/templates/meeting/interim_announcement.txt @@ -1,10 +1,10 @@ {% load ietf_filters %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW. {% endif %}The {{ group.name }} ({{ group.acronym }}) {% if group.type.slug == 'wg' and group.state.slug == 'bof' %}BOF{% else %}{{group.type.name}}{% endif %} will hold -{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ meeting.schedule.assignments.first.timeslot.time | date:"H:i" }} to {{ meeting.schedule.assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ meeting.schedule.assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ meeting.schedule.assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}. +{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}. {% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting. -{% for assignment in meeting.schedule.assignments.all %}Session {{ forloop.counter }}: +{% for assignment in assignments %}Session {{ forloop.counter }}: {{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.utc_start_time | date:"H:i" }} to {{ assignment.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %} {% endfor %}{% endif %} {% if meeting.city %}Meeting Location: diff --git a/ietf/templates/meeting/interim_request_details.html b/ietf/templates/meeting/interim_request_details.html index 9d3aaf0ac..66a3d8886 100644 --- a/ietf/templates/meeting/interim_request_details.html +++ b/ietf/templates/meeting/interim_request_details.html @@ -26,7 +26,7 @@
      {{ meeting.country }}
      Timezone
      {{ meeting.time_zone }}
      - {% for assignment in meeting.schedule.assignments.all %} + {% for assignment in assignments %}
      Date
      {{ assignment.timeslot.time|date:"Y-m-d" }} diff --git a/ietf/templates/meeting/copy_meeting_schedule.html b/ietf/templates/meeting/new_meeting_schedule.html similarity index 62% rename from ietf/templates/meeting/copy_meeting_schedule.html rename to ietf/templates/meeting/new_meeting_schedule.html index 3b28fcc66..c9ace8cb4 100644 --- a/ietf/templates/meeting/copy_meeting_schedule.html +++ b/ietf/templates/meeting/new_meeting_schedule.html @@ -7,14 +7,14 @@ {% block content %} {% origin %} -

      {% block title %}Copy agenda {{ schedule.name }}{% endblock %}

      +

      {% block title %}{% if schedule %}Copy agenda {{ schedule.name }} to new agenda{% else %}New agenda{% endif %}{% endblock %}

      {% csrf_token %} {% bootstrap_form form %} {% buttons %} - + {% endbuttons %}
      {% endblock %} diff --git a/ietf/templates/meeting/room-view.html b/ietf/templates/meeting/room-view.html index ea577d5c8..c1fb43d8c 100644 --- a/ietf/templates/meeting/room-view.html +++ b/ietf/templates/meeting/room-view.html @@ -8,13 +8,13 @@