New branch from trunk @ r18382 for meeting improvements, with iola/meeting-improvement-r17835 merged in

- Legacy-Id: 18385
This commit is contained in:
Ole Laursen 2020-08-20 09:36:41 +00:00
commit a39fa7f967
44 changed files with 2235 additions and 441 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

@ -138,8 +138,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

@ -9,7 +9,7 @@ import re
from tempfile import mkstemp
from django.http import HttpRequest, Http404
from django.db.models import Max, Q, Prefetch
from django.db.models import F, Max, Q, Prefetch
from django.conf import settings
from django.core.cache import cache
from django.urls import reverse
@ -150,13 +150,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!
@ -171,13 +164,17 @@ def get_schedule_by_name(meeting, owner, name):
return meeting.schedule_set.filter(name = name).first()
def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefetches=()):
assignments_queryset = assignments_queryset.select_related(
"timeslot", "timeslot__location", "timeslot__type",
).prefetch_related(
assignments_queryset = assignments_queryset.prefetch_related(
'timeslot', 'timeslot__type', 'timeslot__meeting',
'timeslot__location', 'timeslot__location__floorplan', 'timeslot__location__urlresource_set',
Prefetch(
"session",
queryset=add_event_info_to_session_qs(Session.objects.all().prefetch_related(
'group', 'group__charter', 'group__charter__group',
Prefetch('materials',
queryset=Document.objects.exclude(states__type=F("type"), states__slug='deleted').order_by('sessionpresentation__order').prefetch_related('states'),
to_attr='prefetched_active_materials'
)
))
),
*extra_prefetches
@ -218,10 +215,21 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
parents = Group.objects.filter(pk__in=parent_id_set)
parent_replacements = find_history_replacements_active_at(parents, meeting_time)
timeslot_by_session_pk = {a.session_id: a.timeslot for a in assignments}
for a in assignments:
if a.session and a.session.historic_group and a.session.historic_group.parent_id:
a.session.historic_group.historic_parent = parent_replacements.get(a.session.historic_group.parent_id)
if a.session.current_status == 'resched':
a.session.rescheduled_to = timeslot_by_session_pk.get(a.session.tombstone_for_id)
for d in a.session.prefetched_active_materials:
# make sure these are precomputed with the meeting instead
# of having to look it up
d.get_href(meeting=meeting)
d.get_versionless_href(meeting=meeting)
return assignments
def read_session_file(type, num, doc):
@ -423,6 +431,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,19 @@
# Copyright The IETF Trust 2020, All Rights Reserved
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('meeting', '0030_allow_empty_joint_with_sessions'),
]
operations = [
migrations.AddField(
model_name='session',
name='tombstone_for',
field=models.ForeignKey(blank=True, help_text='This session is the tombstone for a session that was rescheduled', null=True, on_delete=django.db.models.deletion.CASCADE, to='meeting.Session'),
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 2.0.13 on 2020-07-01 02:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('meeting', '0031_session_tombstone_for'),
]
operations = [
migrations.AddField(
model_name='schedule',
name='notes',
field=models.TextField(blank=True),
),
migrations.AlterField(
model_name='schedule',
name='public',
field=models.BooleanField(default=True, help_text='Allow others to see this agenda.'),
),
migrations.AlterField(
model_name='schedule',
name='visible',
field=models.BooleanField(default=True, help_text='Show in the list of possible agendas for the meeting.', verbose_name='Show in agenda list'),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 2.0.13 on 2020-08-04 06:22
from django.db import migrations
import django.db.models.deletion
import ietf.utils.models
class Migration(migrations.Migration):
dependencies = [
('meeting', '0032_schedule_notes'),
]
operations = [
migrations.AddField(
model_name='schedule',
name='origin',
field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='meeting.Schedule'),
),
]

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', '0033_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

@ -297,7 +297,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)
@ -397,14 +399,14 @@ class Room(models.Model):
return self.functional_name
# audio stream support
def audio_stream_url(self):
urlresource = self.urlresource_set.filter(name_id='audiostream').first()
return urlresource.url if urlresource else None
urlresources = [ur for ur in self.urlresource_set.all() if ur.name_id == 'audiostream']
return urlresources[0].url if urlresources else None
def video_stream_url(self):
urlresource = self.urlresource_set.filter(name_id__in=['meetecho', ]).first()
return urlresource.url if urlresource else None
urlresources = [ur for ur in self.urlresource_set.all() if ur.name_id in ['meetecho']]
return urlresources[0].url if urlresources else None
def webex_url(self):
urlresource = self.urlresource_set.filter(name_id__in=['webex', ]).first()
return urlresource.url if urlresource else None
urlresources = [ur for ur in self.urlresource_set.all() if ur.name_id in ['webex']]
return urlresources[0].url if urlresources else None
#
class Meta:
ordering = ["-id"]
@ -456,7 +458,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
@ -628,10 +630,14 @@ class Schedule(models.Model):
meeting = ForeignKey(Meeting, null=True, related_name='schedule_set')
name = models.CharField(max_length=16, blank=False, help_text="Letters, numbers and -:_ allowed.", validators=[RegexValidator(r'^[A-Za-z0-9-:_]*$')])
owner = ForeignKey(Person)
visible = models.BooleanField(default=True, help_text="Make this agenda available to those who know about it.")
public = models.BooleanField(default=True, help_text="Make this agenda publically available.")
visible = models.BooleanField("Show in agenda list", default=True, help_text="Show in the list of possible agendas for the meeting.")
public = models.BooleanField(default=True, help_text="Allow others to see this agenda.")
badness = models.IntegerField(null=True, blank=True)
# considering copiedFrom = ForeignKey('Schedule', blank=True, null=True)
notes = models.TextField(blank=True)
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)
@ -658,20 +664,6 @@ class Schedule(models.Model):
else:
return "noemail"
@property
def visible_token(self):
if self.visible:
return "visible"
else:
return "hidden"
@property
def public_token(self):
if self.public:
return "public"
else:
return "private"
@property
def is_official(self):
return (self.meeting.schedule == self)
@ -943,6 +935,8 @@ class Session(models.Model):
modified = models.DateTimeField(auto_now=True)
remote_instructions = models.CharField(blank=True,max_length=1024)
tombstone_for = models.ForeignKey('Session', blank=True, null=True, help_text="This session is the tombstone for a session that was rescheduled", on_delete=models.CASCADE)
materials = models.ManyToManyField(Document, through=SessionPresentation, blank=True)
resources = models.ManyToManyField(ResourceAssociation, blank=True)
@ -1084,7 +1078,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)
@ -1117,11 +1111,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

@ -78,8 +78,9 @@ def make_meeting_test_data(meeting=None, create_interims=False):
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')
@ -148,7 +149,7 @@ def make_meeting_test_data(meeting=None, create_interims=False):
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"),
@ -156,7 +157,7 @@ def make_meeting_test_data(meeting=None, create_interims=False):
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

@ -4,7 +4,6 @@
import time
import datetime
from pyquery import PyQuery
from unittest import skipIf
import django
@ -15,9 +14,11 @@ import debug # pyflakes:ignore
from ietf.doc.factories import DocumentFactory
from ietf.group import colors
from ietf.person.models import Person
from ietf.meeting.factories import SessionFactory
from ietf.meeting.test_data import make_meeting_test_data
from ietf.meeting.models import Schedule, SchedTimeSessAssignment, Session, Room, TimeSlot, Constraint, ConstraintName
from ietf.meeting.models import SchedulingEvent, SessionStatusName
from ietf.utils.test_runner import IetfLiveServerTestCase
from ietf.utils.pipe import pipe
from ietf import settings
@ -99,6 +100,14 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
time=max(slot1.end_time(), slot2.end_time()) + datetime.timedelta(minutes=10),
)
slot4 = TimeSlot.objects.create(
meeting=meeting,
type_id='regular',
location=room1,
duration=datetime.timedelta(hours=2),
time=slot1.time + datetime.timedelta(days=1),
)
s1, s2 = Session.objects.filter(meeting=meeting, type='regular')
s2.requested_duration = slot2.duration + datetime.timedelta(minutes=10)
s2.save()
@ -106,6 +115,12 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
s2b = Session.objects.create(meeting=meeting, group=s2.group, attendees=10, requested_duration=datetime.timedelta(minutes=60), type_id='regular')
SchedulingEvent.objects.create(
session=s2b,
status=SessionStatusName.objects.get(slug='appr'),
by=Person.objects.get(name='(System)'),
)
Constraint.objects.create(
meeting=meeting,
source=s1.group,
@ -117,8 +132,9 @@ 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)
q = PyQuery(self.driver.page_source)
self.assertEqual(len(q('.session')), 3)
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
s2_element = self.driver.find_element_by_css_selector('#session{}'.format(s2.pk))
@ -231,6 +247,14 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click()
self.assertTrue(not self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed())
# swap days
self.driver.find_element_by_css_selector(".day [data-target=\"#swap-days-modal\"][data-dayid=\"{}\"]".format(slot4.time.date().isoformat())).click()
self.assertTrue(self.driver.find_element_by_css_selector("#swap-days-modal").is_displayed())
self.driver.find_element_by_css_selector("#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(slot1.time.date().isoformat())).click()
self.driver.find_element_by_css_selector("#swap-days-modal button[type=\"submit\"]").click()
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{} #session{}'.format(slot4.pk, s1.pk)))
@skipIf(skip_selenium, skip_message)
@skipIf(django.VERSION[0]==2, "Skipping test with race conditions under Django 2")

View file

@ -126,6 +126,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"))
@ -151,6 +153,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')]))
@ -190,6 +193,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)
@ -214,6 +218,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(
@ -672,16 +677,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')
@ -1072,7 +1083,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
@ -1080,11 +1105,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 = TimeSlot.objects.filter(meeting=meeting, type='regular')
self.assertTrue(q("#timeslot{}".format(timeslots[0].pk)))
for s in [s1, s2]:
@ -1118,12 +1141,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\")"))
@ -1136,10 +1161,14 @@ class EditTests(TestCase):
self.assertEqual(r.status_code, 403)
# turn us into owner
meeting.schedule.owner = Person.objects.get(user__username="secretary")
meeting.schedule.save()
schedule = meeting.schedule
schedule.owner = Person.objects.get(user__username="secretary")
schedule.save()
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
meeting.schedule = None
meeting.save()
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=schedule.owner_email(), name=schedule.name))
r = self.client.get(url)
q = PyQuery(r.content)
self.assertTrue(not q("em:contains(\"You can't edit this schedule\")"))
@ -1152,51 +1181,287 @@ class EditTests(TestCase):
'timeslot': timeslots[0].pk,
'session': s1.pk,
})
self.assertEqual(r.content, b"OK")
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=meeting.schedule, session=s1).timeslot, timeslots[0])
self.assertEqual(json.loads(r.content)['success'], True)
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0])
# move assignment
# move assignment on unofficial schedule
r = self.client.post(url, {
'action': 'assign',
'timeslot': timeslots[1].pk,
'session': s1.pk,
})
self.assertEqual(r.content, b"OK")
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=meeting.schedule, session=s1).timeslot, timeslots[1])
self.assertEqual(json.loads(r.content)['success'], True)
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[1])
# move assignment on official schedule, leaving tombstone
meeting.schedule = schedule
meeting.save()
SchedulingEvent.objects.create(
session=s1,
status=SessionStatusName.objects.get(slug='sched'),
by=Person.objects.get(name='(System)')
)
r = self.client.post(url, {
'action': 'assign',
'timeslot': timeslots[0].pk,
'session': s1.pk,
})
json_content = json.loads(r.content)
self.assertEqual(json_content['success'], True)
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0])
sessions_for_group = Session.objects.filter(group=s1.group, meeting=meeting)
self.assertEqual(len(sessions_for_group), 2)
s_tombstone = [s for s in sessions_for_group if s != s1][0]
self.assertEqual(s_tombstone.tombstone_for, s1)
tombstone_event = SchedulingEvent.objects.get(session=s_tombstone)
self.assertEqual(tombstone_event.status_id, 'resched')
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s_tombstone).timeslot, timeslots[1])
self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.readonly".format(s_tombstone.pk)).html())
# unassign
r = self.client.post(url, {
'action': 'unassign',
'session': s1.pk,
})
self.assertEqual(r.content, b"OK")
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule, session=s1)), [])
self.assertEqual(json.loads(r.content)['success'], True)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), [])
# try swapping days
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])), [])
def test_copy_meeting_schedule(self):
r = self.client.post(url, {
'action': 'swapdays',
'source_day': timeslots[0].time.date().isoformat(),
'target_day': timeslots[2].time.date().isoformat(),
})
self.assertEqual(r.status_code, 302)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[0])), [])
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[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, {
'action': 'swapdays',
'source_day': timeslots[2].time.date().isoformat(),
'target_day': timeslots[0].time.date().isoformat(),
})
self.assertEqual(r.status_code, 302)
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_edit_meeting_timeslots_and_misc_sessions(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))
# check we have the grid and everything set up as a baseline -
# the Javascript tests check that the Javascript can work with
# it
url = urlreverse("ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions", kwargs=dict(num=meeting.number, owner=meeting.schedule.base.owner_email(), name=meeting.schedule.base.name))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
breakfast_room = Room.objects.get(meeting=meeting, name="Breakfast Room")
break_room = Room.objects.get(meeting=meeting, name="Break Area")
reg_room = Room.objects.get(meeting=meeting, name="Registration Area")
for i in range(meeting.days):
self.assertTrue(q("[data-day=\"{}\"]".format((meeting.date + datetime.timedelta(days=i)).isoformat())))
self.assertTrue(q(".room-label:contains(\"{}\")".format(breakfast_room.name)))
self.assertTrue(q(".room-label:contains(\"{}\")".format(break_room.name)))
self.assertTrue(q(".room-label:contains(\"{}\")".format(reg_room.name)))
break_slot = TimeSlot.objects.get(location=break_room, type='break')
room_row = q(".room-row[data-day=\"{}\"][data-room=\"{}\"]".format(break_slot.time.date().isoformat(), break_slot.location_id))
self.assertTrue(room_row)
self.assertTrue(room_row.find("#timeslot{}".format(break_slot.pk)))
self.assertTrue(q(".timeslot-form"))
# add timeslot
ietf_group = Group.objects.get(acronym='ietf')
# copy
r = self.client.post(url, {
'name': "newtest",
'public': "on",
'day': meeting.date,
'time': '08:30',
'duration': '1:30',
'location': break_room.pk,
'show_location': 'on',
'type': 'other',
'group': ietf_group.pk,
'name': "IETF Testing",
'short': "ietf-testing",
'scroll': 1234,
'action': 'add-timeslot',
})
self.assertNoFormPostErrors(r)
self.assertIn("#scroll=1234", r['Location'])
test_timeslot = TimeSlot.objects.get(meeting=meeting, name="IETF Testing")
self.assertEqual(test_timeslot.time, datetime.datetime.combine(meeting.date, datetime.time(8, 30)))
self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1, minutes=30))
self.assertEqual(test_timeslot.location_id, break_room.pk)
self.assertEqual(test_timeslot.show_location, True)
self.assertEqual(test_timeslot.type_id, 'other')
test_session = Session.objects.get(meeting=meeting, timeslotassignments__timeslot=test_timeslot)
self.assertEqual(test_session.short, 'ietf-testing')
self.assertEqual(test_session.group, ietf_group)
self.assertTrue(SchedulingEvent.objects.filter(session=test_session, status='sched'))
# edit timeslot
r = self.client.get(url, {
'timeslot': test_timeslot.pk,
'action': 'edit-timeslot',
})
self.assertEqual(r.status_code, 200)
edit_form_html = json.loads(r.content)['form']
q = PyQuery(edit_form_html)
self.assertEqual(q("[name=name]").val(), test_timeslot.name)
self.assertEqual(q("[name=location]").val(), str(test_timeslot.location_id))
self.assertEqual(q("[name=timeslot]").val(), str(test_timeslot.pk))
self.assertEqual(q("[name=type]").val(), str(test_timeslot.type_id))
self.assertEqual(q("[name=group]").val(), str(ietf_group.pk))
iab_group = Group.objects.get(acronym='iab')
r = self.client.post(url, {
'timeslot': test_timeslot.pk,
'day': meeting.date,
'time': '09:30',
'duration': '1:00',
'location': breakfast_room.pk,
'type': 'other',
'group': iab_group.pk,
'name': "IETF Testing 2",
'short': "ietf-testing2",
'action': 'edit-timeslot',
})
self.assertNoFormPostErrors(r)
test_timeslot.refresh_from_db()
self.assertEqual(test_timeslot.time, datetime.datetime.combine(meeting.date, datetime.time(9, 30)))
self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1))
self.assertEqual(test_timeslot.location_id, breakfast_room.pk)
self.assertEqual(test_timeslot.show_location, False)
self.assertEqual(test_timeslot.type_id, 'other')
test_session.refresh_from_db()
self.assertEqual(test_session.short, 'ietf-testing2')
self.assertEqual(test_session.group, iab_group)
# cancel timeslot
r = self.client.post(url, {
'timeslot': test_timeslot.pk,
'action': 'cancel-timeslot',
})
self.assertNoFormPostErrors(r)
new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='newtest')
event = SchedulingEvent.objects.filter(
session__timeslotassignments__timeslot=test_timeslot
).order_by('-id').first()
self.assertEqual(event.status_id, 'canceled')
# delete timeslot
test_presentation = Document.objects.create(name='slides-test', type_id='slides')
SessionPresentation.objects.create(
document=test_presentation,
rev='1',
session=test_session
)
r = self.client.post(url, {
'timeslot': test_timeslot.pk,
'action': 'delete-timeslot',
})
self.assertNoFormPostErrors(r)
self.assertEqual(list(TimeSlot.objects.filter(pk=test_timeslot.pk)), [])
self.assertEqual(list(Session.objects.filter(pk=test_session.pk)), [])
self.assertEqual(test_presentation.get_state_slug(), 'deleted')
# set agenda note
assignment = SchedTimeSessAssignment.objects.filter(session__group__acronym='mars', schedule=meeting.schedule).first()
url = urlreverse("ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
r = self.client.post(url, {
'timeslot': assignment.timeslot_id,
'day': assignment.timeslot.time.date().isoformat(),
'time': assignment.timeslot.time.time().isoformat(),
'duration': assignment.timeslot.duration,
'location': assignment.timeslot.location_id,
'type': assignment.timeslot.type_id,
'name': assignment.timeslot.name,
'agenda_note': "New Test Note",
'action': 'edit-timeslot',
})
self.assertNoFormPostErrors(r)
assignment.session.refresh_from_db()
self.assertEqual(assignment.session.agenda_note, "New Test Note")
def test_new_meeting_schedule(self):
meeting = make_meeting_test_data()
self.client.login(username="secretary", password="secretary+password")
# 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)
r = self.client.post(url, {
'name': "scratch",
'public': "on",
'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='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 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()
@ -1407,7 +1672,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})
@ -1415,6 +1680,75 @@ class EditScheduleListTests(TestCase):
r = self.client.get(url)
self.assertTrue(r.status_code, 200)
def test_diff_schedules(self):
meeting = make_meeting_test_data()
url = urlreverse('ietf.meeting.views.diff_schedules',kwargs={'num':meeting.number})
login_testing_unauthorized(self,"secretary", url)
r = self.client.get(url)
self.assertTrue(r.status_code, 200)
from_schedule = Schedule.objects.get(meeting=meeting, name="test-unofficial-schedule")
session1 = Session.objects.filter(meeting=meeting, group__acronym='mars').first()
session2 = Session.objects.filter(meeting=meeting, group__acronym='ames').first()
session3 = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym='mars'),
attendees=10, requested_duration=datetime.timedelta(minutes=70),
type_id='regular')
SchedulingEvent.objects.create(session=session3, status_id='schedw', by=Person.objects.first())
slot2 = TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('-time').first()
slot3 = TimeSlot.objects.create(
meeting=meeting, type_id='regular', location=slot2.location,
duration=datetime.timedelta(minutes=60),
time=slot2.time + datetime.timedelta(minutes=60),
)
# copy
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",
})
self.assertNoFormPostErrors(r)
to_schedule = Schedule.objects.get(meeting=meeting, name='newtest')
# make some changes
edit_url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=to_schedule.owner_email(), name=to_schedule.name))
# 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 session
r = self.client.post(edit_url, {
'action': 'unassign',
'session': session1.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
# move session
r = self.client.post(edit_url, {
'action': 'assign',
'timeslot': slot2.pk,
'session': session2.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
# now get differences
r = self.client.get(url, {
'from_schedule': from_schedule.name,
'to_schedule': to_schedule.name,
})
self.assertTrue(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q(".schedule-diffs tr")), 3)
def test_delete_schedule(self):
url = urlreverse('ietf.meeting.views.delete_schedule',
kwargs={'num':self.mtg.number,
@ -1530,6 +1864,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

@ -28,6 +28,7 @@ safe_for_all_meeting_types = [
type_ietf_only_patterns = [
url(r'^agenda/%(owner)s/%(schedule_name)s/edit$' % settings.URL_REGEXPS, views.edit_schedule),
url(r'^agenda/%(owner)s/%(schedule_name)s/edit/$' % settings.URL_REGEXPS, views.edit_meeting_schedule),
url(r'^agenda/%(owner)s/%(schedule_name)s/timeslots/$' % settings.URL_REGEXPS, views.edit_meeting_timeslots_and_misc_sessions),
url(r'^agenda/%(owner)s/%(schedule_name)s/details$' % settings.URL_REGEXPS, views.edit_schedule_properties),
url(r'^agenda/%(owner)s/%(schedule_name)s/delete$' % settings.URL_REGEXPS, views.delete_schedule),
url(r'^agenda/%(owner)s/%(schedule_name)s/make_official$' % settings.URL_REGEXPS, views.make_schedule_official),
@ -41,13 +42,15 @@ 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),
url(r'^agenda/by-type/(?P<type>[a-z]+)/ics$', views.agenda_by_type_ics),
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

@ -19,7 +19,7 @@ from django.utils.safestring import mark_safe
import debug # pyflakes:ignore
from ietf.dbtemplate.models import DBTemplate
from ietf.meeting.models import Session, Meeting, SchedulingEvent, TimeSlot, Constraint
from ietf.meeting.models import Session, Meeting, SchedulingEvent, TimeSlot, Constraint, SchedTimeSessAssignment
from ietf.group.models import Group, Role
from ietf.group.utils import can_manage_materials
from ietf.name.models import SessionStatusName, ConstraintName
@ -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:
@ -442,3 +442,121 @@ def preprocess_constraints_for_meeting_schedule_editor(meeting, sessions):
formatted_constraints_for_sessions[s.pk][joint_with_groups_constraint_name] = [g.acronym for g in joint_groups]
return constraints_for_sessions, formatted_constraints_for_sessions, constraint_names
def diff_meeting_schedules(from_schedule, to_schedule):
"""Compute the difference between the two meeting schedules as a list
describing the set of actions that will turn the schedule of from into
the schedule of to, like:
[
{'change': 'schedule', 'session': session_id, 'to': timeslot_id},
{'change': 'move', 'session': session_id, 'from': timeslot_id, 'to': timeslot_id2},
{'change': 'unschedule', 'session': session_id, 'from': timeslot_id},
]
Uses .assignments.all() so that it can be prefetched.
"""
diffs = []
from_session_timeslots = {
a.session_id: a.timeslot_id
for a in from_schedule.assignments.all()
}
session_ids_in_to = set()
for a in to_schedule.assignments.all():
session_ids_in_to.add(a.session_id)
from_timeslot_id = from_session_timeslots.get(a.session_id)
if from_timeslot_id is None:
diffs.append({'change': 'schedule', 'session': a.session_id, 'to': a.timeslot_id})
elif a.timeslot_id != from_timeslot_id:
diffs.append({'change': 'move', 'session': a.session_id, 'from': from_timeslot_id, 'to': a.timeslot_id})
for from_session_id, from_timeslot_id in from_session_timeslots.items():
if from_session_id not in session_ids_in_to:
diffs.append({'change': 'unschedule', 'session': from_session_id, 'from': from_timeslot_id})
return diffs
def prefetch_schedule_diff_objects(diffs):
session_ids = set()
timeslot_ids = set()
for d in diffs:
session_ids.add(d['session'])
if d['change'] == 'schedule':
timeslot_ids.add(d['to'])
elif d['change'] == 'move':
timeslot_ids.add(d['from'])
timeslot_ids.add(d['to'])
elif d['change'] == 'unschedule':
timeslot_ids.add(d['from'])
session_lookup = {s.pk: s for s in Session.objects.filter(pk__in=session_ids)}
timeslot_lookup = {t.pk: t for t in TimeSlot.objects.filter(pk__in=timeslot_ids).prefetch_related('location')}
res = []
for d in diffs:
d_objs = {
'change': d['change'],
'session': session_lookup.get(d['session'])
}
if d['change'] == 'schedule':
d_objs['to'] = timeslot_lookup.get(d['to'])
elif d['change'] == 'move':
d_objs['from'] = timeslot_lookup.get(d['from'])
d_objs['to'] = timeslot_lookup.get(d['to'])
elif d['change'] == 'unschedule':
d_objs['from'] = timeslot_lookup.get(d['from'])
res.append(d_objs)
return res
def swap_meeting_schedule_timeslot_assignments(schedule, source_timeslots, target_timeslots, source_target_offset):
"""Swap the assignments of the two meeting schedule timeslots in one
go, automatically matching them up based on the specified offset,
e.g. timedelta(days=1). For timeslots where no suitable swap match
is found, the sessions are unassigned. Doesn't take tombstones into
account."""
assignments_by_timeslot = defaultdict(list)
for a in SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot__in=source_timeslots + target_timeslots):
assignments_by_timeslot[a.timeslot_id].append(a)
timeslots_to_match_up = [(source_timeslots, target_timeslots, source_target_offset), (target_timeslots, source_timeslots, -source_target_offset)]
for lhs_timeslots, rhs_timeslots, lhs_offset in timeslots_to_match_up:
timeslots_by_location = defaultdict(list)
for rts in rhs_timeslots:
timeslots_by_location[rts.location_id].append(rts)
for lts in lhs_timeslots:
lts_assignments = assignments_by_timeslot.pop(lts.pk, [])
if not lts_assignments:
continue
swapped = False
most_overlapping_rts, max_overlap = max([
(rts, max(datetime.timedelta(0), min(lts.end_time() + lhs_offset, rts.end_time()) - max(lts.time + lhs_offset, rts.time)))
for rts in timeslots_by_location.get(lts.location_id, [])
] + [(None, datetime.timedelta(0))], key=lambda t: t[1])
if max_overlap > datetime.timedelta(minutes=5):
for a in lts_assignments:
a.timeslot = most_overlapping_rts
a.modified = datetime.datetime.now()
a.save()
swapped = True
if not swapped:
for a in lts_assignments:
a.delete()

File diff suppressed because it is too large Load diff

View file

@ -12077,6 +12077,16 @@
"model": "name.sessionstatusname",
"pk": "schedw"
},
{
"fields": {
"desc": "",
"name": "Rescheduled",
"order": 0,
"used": true
},
"model": "name.sessionstatusname",
"pk": "resched"
},
{
"fields": {
"desc": "",

View file

@ -0,0 +1,24 @@
# Copyright The IETF Trust 2020, All Rights Reserved
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('name', '0017_update_constraintname_order_and_label'),
]
def add_rescheduled_session_status_name(apps, schema_editor):
SessionStatusName = apps.get_model('name', 'SessionStatusName')
SessionStatusName.objects.get_or_create(
slug='resched',
name="Rescheduled",
)
def noop(apps, schema_editor):
pass
operations = [
migrations.RunPython(add_rescheduled_session_status_name, noop, elidable=True),
]

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(),
@ -341,27 +343,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
@ -295,7 +302,7 @@ def blue_sheet_generate(request, meeting_id):
if request.method == "POST":
groups = Group.objects.filter(
type__in=['wg','rg','ag','rag','program'],
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')
@ -379,7 +386,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':
@ -574,7 +581,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)
@ -655,24 +662,28 @@ 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':
if 'cancel' in request.POST:
pk = request.POST.get('pk')
session = get_object_or_404(sessions, pk=pk)
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='canceled'),
by=request.user.person,
)
messages.success(request, 'Session cancelled')
if session.current_status not in ['canceled', 'resched']:
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='canceled'),
by=request.user.person,
)
messages.success(request, 'Session cancelled')
return redirect('ietf.secr.meetings.views.regular_sessions', meeting_id=meeting_id, schedule_name=schedule_name)
status_names = {n.slug: n.name for n in SessionStatusName.objects.all()}
for s in sessions:
s.current_status_name = status_names.get(s.current_status, s.current_status)
s.can_cancel = s.current_status not in ['canceled', 'resched']
return render(request, 'meetings/sessions.html', {
'meeting': meeting,

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

@ -221,9 +221,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

@ -36,11 +36,13 @@
<td>{{ session.current_status_name }}</td>
<td><a href="{% url 'ietf.secr.meetings.views.regular_session_edit' meeting_id=meeting.number schedule_name=schedule.name session_id=session.id %}">Edit</a></td>
<td>
<form method="post">
{% csrf_token %}
<input type="hidden" name="pk" value="{{ session.pk }}">
<input type="submit" name="cancel" value="Cancel">
</form>
{% if session.can_cancel %}
<form method="post">
{% csrf_token %}
<input type="hidden" name="pk" value="{{ session.pk }}">
<input type="submit" name="cancel" value="Cancel">
</form>
{% endif %}
</td>
</tr>
{% endfor %}

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

@ -1003,6 +1003,12 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
border-top: 1px solid #ddd;
}
/* === List Meeting Schedules ====================================== */
.from-base-schedule {
opacity: 0.7;
}
/* === Edit Meeting Schedule ====================================== */
.edit-meeting-schedule .edit-grid {
@ -1034,6 +1040,18 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
height: 3em;
}
.edit-meeting-schedule .edit-grid .day-label .swap-days {
cursor: pointer;
}
.edit-meeting-schedule .edit-grid .day-label .swap-days:hover {
color: #666;
}
.edit-meeting-schedule #swap-days-modal .modal-body label {
display: block;
}
.edit-meeting-schedule .edit-grid .day-flow {
margin-left: 8em;
display: flex;
@ -1118,6 +1136,11 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
cursor: default;
}
.edit-meeting-schedule .session.readonly {
cursor: default;
background-color: #ddd;
}
.edit-meeting-schedule .session.selected .session-label {
font-weight: bold;
}
@ -1289,3 +1312,130 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
cursor: default;
background-color: #eee;
}
/* === Edit Meeting Timeslots and Misc Sessions =================== */
.edit-meeting-timeslots-and-misc-sessions .day {
margin-bottom: 1em;
}
.edit-meeting-timeslots-and-misc-sessions .day-label {
text-align: center;
font-size: 20px;
margin-bottom: 0.4em;
}
.edit-meeting-timeslots-and-misc-sessions .room-row {
border-bottom: 1px solid #ccc;
height: 20px;
display: flex;
cursor: pointer;
}
.edit-meeting-timeslots-and-misc-sessions .room-label {
width: 12em;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.edit-meeting-timeslots-and-misc-sessions .timeline {
position: relative;
flex-grow: 1;
}
.edit-meeting-timeslots-and-misc-sessions .timeline.hover {
background: radial-gradient(#999 1px, transparent 1px);
background-size: 20px 20px;
}
.edit-meeting-timeslots-and-misc-sessions .timeline.selected.hover,
.edit-meeting-timeslots-and-misc-sessions .timeline.selected {
background: radial-gradient(#999 2px, transparent 2px);
background-size: 20px 20px;
}
.edit-meeting-timeslots-and-misc-sessions .timeslot {
position: absolute;
overflow: hidden;
background-color: #f0f0f0;
opacity: 0.8;
height: 19px;
top: 0px;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
padding-left: 0.2em;
border-left: 1px solid #999;
border-right: 1px solid #999;
}
.edit-meeting-timeslots-and-misc-sessions .timeslot:hover {
background-color: #ccc;
}
.edit-meeting-timeslots-and-misc-sessions .timeslot.selected {
background-color: #bbb;
}
.edit-meeting-timeslots-and-misc-sessions .timeslot .session.cancelled {
color: #a00;
}
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel {
position: sticky;
bottom: 0;
left: 0;
width: 100%;
border-top: 0.2em solid #ccc;
padding-top: 0.2em;
margin-bottom: 2em;
background-color: #fff;
opacity: 0.95;
}
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel form {
display: flex;
align-items: flex-start;
}
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel form button {
margin: 0 0.5em;
}
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form {
display: flex;
flex-wrap: wrap;
align-items: baseline;
}
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form .form-group {
margin-right: 1em;
margin-bottom: 0.5em;
}
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form label {
display: inline-block;
margin-right: 0.5em;
}
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form .form-control {
display: inline-block;
width: auto;
}
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form [name=time],
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form [name=duration] {
width: 6em;
}
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form [name=name] {
width: 25em;
}
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form [name=short] {
width: 10em;
}

View file

@ -1,14 +1,14 @@
jQuery(document).ready(function () {
let content = jQuery(".edit-meeting-schedule");
function failHandler(xhr, textStatus, error) {
let errorText = error;
function reportServerError(xhr, textStatus, error) {
let errorText = error || textStatus;
if (xhr && xhr.responseText)
errorText += "\n\n" + xhr.responseText;
alert("Error: " + errorText);
}
let sessions = content.find(".session");
let sessions = content.find(".session").not(".readonly");
let timeslots = content.find(".timeslot");
let days = content.find(".day-flow .day");
@ -120,7 +120,7 @@ jQuery(document).ready(function () {
});
if (ietfData.can_edit) {
if (!content.find(".edit-grid").hasClass("read-only")) {
// dragging
sessions.on("dragstart", function (event) {
event.originalEvent.dataTransfer.setData("text/plain", this.id);
@ -130,7 +130,6 @@ jQuery(document).ready(function () {
});
sessions.on("dragend", function () {
jQuery(this).removeClass("dragging");
});
sessions.prop('draggable', true);
@ -161,31 +160,47 @@ jQuery(document).ready(function () {
});
dropElements.on('drop', function (event) {
jQuery(this).parent().removeClass("dropping");
let dropElement = jQuery(this);
let sessionId = event.originalEvent.dataTransfer.getData("text/plain");
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session") {
dropElement.parent().removeClass("dropping");
return;
}
let sessionElement = sessions.filter("#" + sessionId);
if (sessionElement.length == 0)
if (sessionElement.length == 0) {
dropElement.parent().removeClass("dropping");
return;
}
event.preventDefault(); // prevent opening as link
if (sessionElement.parent().is(this))
let dragParent = sessionElement.parent();
if (dragParent.is(this)) {
dropElement.parent().removeClass("dropping");
return;
}
let dropElement = jQuery(this);
let dropParent = dropElement.parent();
function failHandler(xhr, textStatus, error) {
dropElement.parent().removeClass("dropping");
reportServerError(xhr, textStatus, error);
}
function done(response) {
if (response != "OK") {
failHandler(null, null, response);
dropElement.parent().removeClass("dropping");
if (!response.success) {
reportServerError(null, null, response);
return;
}
dropElement.append(sessionElement); // move element
if (response.tombstone)
dragParent.append(response.tombstone);
updateCurrentSchedulingHints();
if (dropParent.hasClass("unassigned-sessions"))
sortUnassigned();
@ -193,7 +208,7 @@ jQuery(document).ready(function () {
if (dropParent.hasClass("unassigned-sessions")) {
jQuery.ajax({
url: ietfData.urls.assign,
url: window.location.href,
method: "post",
timeout: 5 * 1000,
data: {
@ -204,7 +219,7 @@ jQuery(document).ready(function () {
}
else {
jQuery.ajax({
url: ietfData.urls.assign,
url: window.location.href,
method: "post",
data: {
action: "assign",
@ -215,6 +230,32 @@ jQuery(document).ready(function () {
}).fail(failHandler).done(done);
}
});
// swap days
content.find(".swap-days").on("click", function () {
let originDay = this.dataset.dayid;
let modal = content.find("#swap-days-modal");
let radios = modal.find(".modal-body label");
radios.removeClass("text-muted");
radios.find("input[name=target_day]").prop("disabled", false).prop("checked", false);
let originRadio = radios.find("input[name=target_day][value=" + originDay + "]");
originRadio.parent().addClass("text-muted");
originRadio.prop("disabled", true);
modal.find(".modal-title .day").text(jQuery.trim(originRadio.parent().text()));
modal.find("input[name=source_day]").val(originDay);
updateSwapDaysSubmitButton();
});
function updateSwapDaysSubmitButton() {
content.find("#swap-days-modal button[type=submit]").prop("disabled", content.find("#swap-days-modal input[name=target_day]:checked").length == 0);
}
content.find("#swap-days-modal input[name=target_day]").on("change", function () {
updateSwapDaysSubmitButton();
});
}
// hints for the current schedule

View file

@ -0,0 +1,156 @@
jQuery(document).ready(function () {
function reportServerError(xhr, textStatus, error) {
let errorText = error || textStatus;
if (xhr && xhr.responseText)
errorText += "\n\n" + xhr.responseText;
alert("Error: " + errorText);
}
let content = jQuery(".edit-meeting-timeslots-and-misc-sessions");
if (content.data('scroll'))
jQuery(document).scrollTop(+content.data('scroll'));
else {
let scrollFragment = "#scroll=";
if (window.location.hash.slice(0, scrollFragment.length) == scrollFragment && !isNaN(+window.location.hash.slice(scrollFragment.length))) {
jQuery(document).scrollTop(+window.location.hash.slice(scrollFragment.length));
history.replaceState(null, document.title, window.location.pathname + window.location.search);
}
}
function reportServerError(xhr, textStatus, error) {
let errorText = error || textStatus;
if (xhr && xhr.responseText)
errorText += "\n\n" + xhr.responseText;
alert("Error: " + errorText);
}
let timeslots = content.find(".timeslot");
timeslots.each(function () {
jQuery(this).tooltip({title: jQuery(this).text()});
});
content.find(".day-grid").on("click", cancelCurrentActivity);
let schedulingPanel = content.find(".scheduling-panel");
function cancelCurrentActivity() {
content.find(".selected").removeClass("selected");
schedulingPanel.hide();
schedulingPanel.find(".panel-content").children().remove();
// if we came from a failed POST, that's no longer relevant so overwrite history
history.replaceState(null, document.title, window.location.pathname + window.location.search);
}
if (!content.hasClass("read-only")) {
// we handle the hover effect in Javascript because we don't want
// it to show in case the timeslot itself is hovered
content.find(".room-label,.timeline").on("mouseover", function () {
jQuery(this).closest(".day").find(".timeline.hover").removeClass("hover");
jQuery(this).closest(".room-row").find(".timeline").addClass("hover");
}).on("mouseleave", function (){
jQuery(this).closest(".day").find(".timeline.hover").removeClass("hover");
});
content.find(".timeline .timeslot").on("mouseover", function (e) {
e.stopPropagation();
jQuery(this).closest(".day").find(".timeline.hover").removeClass("hover");
}).on("mouseleave", function (e) {
jQuery(this).closest(".day").find(".timeline.hover").removeClass("hover");
});
content.find(".room-row").on("click", function (e) {
e.stopPropagation();
cancelCurrentActivity();
jQuery(this).find(".timeline").addClass("selected");
schedulingPanel.find(".panel-content").append(content.find(".add-timeslot-template").html());
schedulingPanel.find("[name=day]").val(this.dataset.day);
schedulingPanel.find("[name=location]").val(this.dataset.room);
schedulingPanel.find("[name=type]").trigger("change");
schedulingPanel.show();
schedulingPanel.find("[name=time]").focus();
});
}
content.find(".timeline .timeslot").on("click", function (e) {
e.stopPropagation();
let element = jQuery(this);
element.addClass("selected");
jQuery.ajax({
url: window.location.href,
method: "get",
timeout: 5 * 1000,
data: {
action: "edit-timeslot",
timeslot: this.id.slice("timeslot".length)
}
}).fail(reportServerError).done(function (response) {
if (!response.form) {
reportServerError(null, null, response);
return;
}
cancelCurrentActivity();
element.addClass("selected");
schedulingPanel.find(".panel-content").append(response.form);
schedulingPanel.find(".timeslot-form [name=type]").trigger("change");
schedulingPanel.find(".timeslot-form").show();
schedulingPanel.show();
});
});
content.on("change click", ".timeslot-form [name=type]", function () {
let form = jQuery(this).closest("form");
let hide = {};
form.find("[name=group],[name=short],[name=\"agenda_note\"]").prop('disabled', false).closest(".form-group").show();
if (this.value == "break") {
form.find("[name=short]").closest(".form-group").hide();
}
else if (this.value == "plenary") {
let group = form.find("[name=group]");
group.val(group.data('ietf'));
}
else if (this.value == "regular") {
form.find("[name=short]").closest(".form-group").hide();
}
if (this.value != "regular")
form.find("[name=\"agenda_note\"]").closest(".form-group").hide();
if (['break', 'reg', 'reserved', 'unavail', 'regular'].indexOf(this.value) != -1) {
let group = form.find("[name=group]");
group.prop('disabled', true);
group.closest(".form-group").hide();
}
});
content.on("submit", ".timeslot-form", function () {
let form = jQuery(this).closest("form");
form.find("[name=scroll]").remove();
form.append("<input type=hidden name=scroll value=" + jQuery(document).scrollTop() + ">");
});
content.on("click", "button[type=submit][name=action][value=\"delete-timeslot\"],button[type=submit][name=action][value=\"cancel-timeslot\"]", function (e) {
let msg = this.value == "delete-timeslot" ? "Delete this time slot?" : "Cancel the session in this time slot?";
if (!confirm(msg)) {
e.preventDefault();
}
});
schedulingPanel.find(".close").on("click", function () {
cancelCurrentActivity();
});
schedulingPanel.find('.timeslot-form [name=type]').trigger("change");
});

View file

@ -235,7 +235,7 @@
<span class="hidden-xs">
{% if item.timeslot.type.slug == 'other' %}
{% if item.session.agenda or item.session.remote_instructions or item.session.agenda_note %}
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
{% else %}
{% for slide in item.session.slides %}
<a href="{{slide.get_href}}">{{ slide.title|clean_whitespace }}</a>
@ -324,6 +324,20 @@
<span class="label label-danger pull-right">CANCELLED</span>
{% endif %}
{% if item.session.current_status == 'resched' %}
<span class="label label-danger pull-right">
RESCHEDULED
{% if item.session.rescheduled_to %}
TO
{% if "-utc" in request.path %}
{{ item.session.rescheduled_to.utc_start_time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.utc_end_time|date:"G:i" }}
{% else %}
{{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}
{% endif %}
{% endif %}
</span>
{% endif %}
{% if item.session.agenda_note|first_url|conference_url %}
<br><a href={{item.session.agenda_note|first_url}}>{{item.session.agenda_note|slice:":23"}}</a>
{% elif item.session.agenda_note %}
@ -333,7 +347,7 @@
</td>
<td class="text-nowrap text-right">
<span class="hidden-xs">
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
</span>
</td>
</tr>

View file

@ -22,7 +22,7 @@
{% endif %}{% if item.timeslot.type_id == 'regular' %}{% if item.session.historic_group %}{% ifchanged %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% endif %}
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% elif item.session.current_status == 'resched' %} *** RESCHEDULED{% if item.session.rescheduled_to %} TO {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}{% endif %} ***{% endif %}
{% endif %}{% endif %}{% if item.timeslot.type.slug == "break" %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.break_area and item.timeslot.show_location %} - {{ schedule.meeting.break_area }}{% endif %}{% endif %}{% if item.timeslot.type.slug == "other" %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }} - {{ item.timeslot.location.name }}{% endif %}{% endfor %}

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

@ -0,0 +1,40 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2020, All Rights Reserved #}
{% load origin %}
{% load ietf_filters %}
{% load bootstrap3 %}
{% block content %}
{% origin %}
<h1>{% block title %}Differences between Meeting Agendas for IETF {{ meeting.number }}{% endblock %}</h1>
<form method="get">
{% bootstrap_form form %}
<button type="submit">Show differences</button>
</form>
{% if diffs != None %}
<h2>Differences from {{ from_schedule.name }} ({{ from_schedule.owner }}) to {{ to_schedule.name }} ({{ to_schedule.owner }}) </h2>
{% if diffs %}
<table class="table table-condensed schedule-diffs">
{% for d in diffs %}
<tr>
<td>
{% if d.change == 'schedule' %}
Scheduled <b>{{ d.session.session_label }}</b> to <b>{{ d.to.time|date:"l G:i" }} at {{ d.to.location.name }}</b>
{% elif d.change == 'move' %}
Moved <b>{{ d.session.session_label }}</b> from {{ d.from.time|date:"l G:i" }} at {{ d.from.location.name }} to <b>{{ d.to.time|date:"l G:i" }} at {{ d.to.location.name }}</b>
{% elif d.change == 'unschedule' %}
Unscheduled <b>{{ d.session.session_label }}</b> from {{ d.from.time|date:"l G:i" }} at {{ d.from.location.name }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
No differences in scheduled sessions found!
{% endif %}
{% endif %}
{% endblock content %}

View file

@ -12,12 +12,9 @@
{% endfor %}
{% endblock morecss %}
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting schedule{% endblock %}
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %}
{% block js %}
<script type='text/javascript'>
var ietfData = {{ js_data|safe }};
</script>
<script type="text/javascript" src="{% static 'ietf/js/edit-meeting-schedule.js' %}"></script>
{% endblock js %}
@ -27,14 +24,20 @@
<div class="edit-meeting-schedule">
<p class="pull-right">
<a href="{% url "ietf.meeting.views.copy_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">Copy schedule</a>
{% if can_edit_properties %}
<a href="{% url "ietf.meeting.views.edit_schedule_properties" schedule.meeting.number schedule.owner_email schedule.name %}">Edit properties</a>
&middot;
{% endif %}
<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 %}">All schedules for meeting</a>
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other Agendas</a>
</p>
<p>
Schedule name: {{ schedule.name }}
Agenda name: {{ schedule.name }}
&middot;
@ -43,7 +46,7 @@
{% if not can_edit %}
&middot;
<em>You can't edit this schedule. Take a copy first.</em>
<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>
@ -74,7 +77,7 @@
{% for day in days %}
<div class="day">
<div class="day-label">
<strong>{{ day.day|date:"l" }}</strong><br>
<strong>{{ day.day|date:"l" }}</strong> <i class="fa fa-exchange swap-days" data-dayid="{{ day.day.isoformat }}" data-toggle="modal" data-target="#swap-days-modal"></i><br>
{{ day.day|date:"N j, Y" }}
</div>
@ -88,9 +91,9 @@
</div>
<div class="drop-target">
{% for assignment, session in t.session_assignments %}
{% include "meeting/edit_meeting_schedule_session.html" %}
{% endfor %}
{% for assignment, session in t.session_assignments %}
{% include "meeting/edit_meeting_schedule_session.html" %}
{% endfor %}
</div>
</div>
{% endfor %}
@ -167,5 +170,34 @@
</div>
</div>
<div id="swap-days-modal" class="modal" role="dialog" aria-labelledby="swap-days-modal-title">
<div class="modal-dialog modal-lg" role="document">
<form class="modal-content" method="post">{% csrf_token %}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="modal-title" id="swap-days-modal-title">Swap <span class="day"></span> with</h4>
</div>
<input type="hidden" name="source_day" value="">
<div class="modal-body">
{% for day in days %}
<label>
<input type="radio" name="target_day" value="{{ day.day.isoformat }}"> {{ day.day|date:"l, N j, Y" }}
</label>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" name="action" value="swapdays" class="btn btn-primary">Swap days</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

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 %}" 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

@ -0,0 +1,92 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
{% load origin %}
{% load staticfiles %}
{% load ietf_filters %}
{% load bootstrap3 %}
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %}
{% block js %}
<script type="text/javascript" src="{% static 'ietf/js/edit-meeting-timeslots-and-misc-sessions.js' %}"></script>
{% endblock js %}
{% block content %}
{% origin %}
<div class="edit-meeting-timeslots-and-misc-sessions {% if not can_edit %}read-only{% endif %}" {% if scroll %}data-scroll="{{ scroll }}"{% endif %}>
<p class="pull-right">
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other Agendas</a>
</p>
<p>
Meeting time slots and misc. sessions for agenda: {{ schedule.name }} {% if not can_edit %}<em>(you do not have permission to edit time slots)</em>{% endif %}
</p>
<div class="day-grid">
{% for day in day_grid %}
<div class="day">
<div class="day-label">
<strong>{{ day.day|date:"l" }}</strong>
{{ day.day|date:"N j, Y" }}
</div>
{% for room, timeslots in day.room_timeslots %}
<div class="room-row" data-room="{{ room.pk }}" data-day="{{ day.day.isoformat }}">
<div class="room-label" title="{{ room.name }}">
<strong>{{ room.name }}</strong>
{% if room.capacity %}{{ room.capacity }}{% endif %}
</div>
<div class="timeline">
{% for t in timeslots %}
<div id="timeslot{{ t.pk }}" class="timeslot" style="left: {{ t.left_offset|floatformat }}%; width: {{ t.layout_width|floatformat }}%;">
{% for s in t.assigned_sessions %}
<span class="session {% if s.current_status == 'canceled' or s.current_status == 'resched' %}cancelled{% endif %}">
{% if s.name %}
{{ s.name }}
{% if s.group %}
({{ s.group.acronym }})
{% endif %}
{% elif s.group %}
{{ s.group.acronym }}
{% endif %}
</span>
{% empty %}
{% if t.type_id == 'regular' %}
(session)
{% elif t.name %}
{{ t.name }}
{% else %}
{{ t.type.name }}
{% endif %}
{% endfor %}
<span class="time-label">{{ t.time|date:"G:i" }}-{{ t.end_time|date:"G:i" }}</span>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="add-timeslot-template" style="display:none">
{% include "meeting/edit_timeslot_form.html" with timeslot_form_action='add' timeslot_form=empty_timeslot_form %}
</div>
<div class="scheduling-panel" style="{% if not edit_timeslot_form and not add_timeslot_form %}display:none{% endif %}">
<i class="close fa fa-times pull-right"></i>
<div class="panel-content">
{% if edit_timeslot_form %}
{% include "meeting/edit_timeslot_form.html" with timeslot_form_action='edit' timeslot_form=edit_timeslot_form %}
{% elif add_timeslot_form %}
{% include "meeting/edit_timeslot_form.html" with timeslot_form_action='add' timeslot_form=add_timeslot_form %}
{% endif %}
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,41 @@
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% if not timeslot_form.active_assignment or timeslot_form.active_assignment.schedule_id == schedule.pk %}
<form class="timeslot-form" method="post">{% csrf_token %}
<div class="flowing-form">
{% bootstrap_field timeslot_form.day %}
{% bootstrap_field timeslot_form.time %}
{% bootstrap_field timeslot_form.duration %}
{% bootstrap_field timeslot_form.location %}
{% bootstrap_field timeslot_form.show_location %}
{% bootstrap_field timeslot_form.type %}
{% bootstrap_field timeslot_form.group %}
{% bootstrap_field timeslot_form.name %}
{% bootstrap_field timeslot_form.short %}
{% if 'agenda_note' in timeslot_form.fields %}
{% bootstrap_field timeslot_form.agenda_note %}
{% endif %}
</div>
{% if can_edit %}
<button type="submit" class="btn btn-primary" name="action" value="{{ timeslot_form_action }}-timeslot">
{% if timeslot_form_action == 'add' %}Add time slot{% else %}Save{% endif %} slot
</button>
{% if timeslot %}
<input type="hidden" name="timeslot" value="{{ timeslot.pk }}">
{% if timeslot.type_id != 'break' and timeslot.can_cancel %}
<button type="submit" class="btn btn-danger" name="action" value="cancel-timeslot" title="Cancel session">Cancel session</button>
{% endif %}
<button type="submit" class="btn btn-danger" name="action" value="delete-timeslot" title="Delete time slot">Delete</button>
{% endif %}
{% endif %}
</form>
{% elif schedule.base %}
<p class="text-center">You cannot edit this session here - it is set up in the <a href="{% url 'ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions' meeting.number schedule.base.owner_email schedule.base.name %}">base schedule</a></p>
{% endif %}

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

@ -367,6 +367,9 @@ promiselist.push(ss_promise);
<div class="agenda_save_box">
<div id="agenda_title"><b>Agenda name: </b><span>{{schedule.name}}</span></div>
{% if can_edit_properties %}
<div><b>Properties</b> <a href="{% url "ietf.meeting.views.edit_schedule_properties" schedule.meeting.number schedule.owner_email schedule.name %}">Edit</a></div>
{% endif %}
<div id="agenda_saveas">
<form action="{{saveasurl}}" method="post">{% csrf_token %}
{{ saveas.as_p }}

View file

@ -1,20 +1,20 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
{% load origin %}
{% load static %}
{% load staticfiles %}
{% load ietf_filters %}
{% load bootstrap3 %}
{% block content %}
{% origin %}
<h1>{% block title %}Copy schedule {{ schedule.name }}{% endblock %}</h1>
<h1>{% block title %}{% if schedule %}Copy agenda {{ schedule.name }} to new agenda{% else %}New agenda{% endif %}{% endblock %}</h1>
<form class="form-horizontal" method="post">
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-default">Copy schedule</button>
<button type="submit" class="btn btn-primary">Create agenda</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -1,21 +1,15 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
{% load origin %}
{% block title %}IETF {{ meeting.number }} Meeting Agenda: {{ schedule.owner }} / {{ schedule.name }}{% endblock %}
{% block morecss %}
{% for area in area_list %}
.{{ area.upcase_acronym}}-scheme, .meeting_event th.{{ area.upcase_acronym}}-scheme, #{{ area.upcase_acronym }}-groups, #selector-{{ area.upcase_acronym }} { color:{{ area.fg_color }}; background-color: {{ area.bg_color }} }
{% endfor %}
{% endblock morecss %}
{% block content %}
{% origin %}
<div id="read_only">
<p>You do not have access this agenda. It belongs to {{ schedule.owner }}.</p>
<p><a href="{% url "ietf.meeting.views.list_schedules" meeting.number %}">List your meetings</a>.</p>
<p><a href="{% url "ietf.meeting.views.list_schedules" meeting.number %}">Other agendas for this meeting</a>.</p>
<div class="wrapper custom_text_stuff"></div>
</div>

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

@ -1,45 +1,81 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
{% load origin %}
{% load static %}
{% load ietf_filters %}
{% block title %}IETF {{ meeting.number }} Meeting Agenda List{% endblock %}
{% block content %}
{% origin %}
<h1>IETF {{meeting.number}} Agenda List</h1>
{% comment %}
<div>
<p><a href="{% url "ietf.meeting.views.edit_timeslots" meeting.number %}">Edit Timeslots</a></p>
</div>
{% endcomment %}
<h1>{% block title %}Possible Meeting Agendas for IETF {{ meeting.number }}{% endblock %}</h1>
<div>
{% regroup schedules by is_official as classed_schedules %}
{% for class in classed_schedules %}
{% for schedules, own, label in schedule_groups %}
<div class="panel panel-default">
<div class="panel-heading">{{class.grouper|yesno:"Official,Unofficial"}} Schedule{{class.list|length|pluralize}}</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-4">Name</th>
<th class="col-md-4">Owner</th>
<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-1">Base</th>
<th class="col-md-3">Notes</th>
<th class="col-md-1">Visible</th>
<th class="col-md-1">Public</th>
<th class="col-md-1"></th>
<td></td>
</tr>
{% for schedule in schedules %}
<tr>
<td>
<a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.owner_email schedule.name %}" title="Show regular sessions in agenda">{{ schedule.name }}</a>
</td>
<td>{{ schedule.owner }}</td>
<td>
{% if schedule.origin %}
<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>
{% if schedule.base %}
<a href="{% url "ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions" 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 %}
</td>
<td>
{% if schedule.public %}
<div class="label label-success">public</div>
{% else %}
<div class="label label-danger">private</div>
{% endif %}
</td>
<td>
{% if schedule.can_edit_properties %}
<a class="edit-schedule-properties" href="{% url "ietf.meeting.views.edit_schedule_properties" meeting.number schedule.owner_email schedule.name %}?next={{ request.get_full_path|urlencode }}">
<i title="Edit agenda properties" class="fa fa-edit"></i>
</a>
{% endif %}
<a href="{% url "ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions" meeting.number schedule.owner_email schedule.name %}">
<i title="Show time slots and misc. sessions for agenda" class="fa fa-calendar"></i>
</a>
</td>
</tr>
{% for schedule in class.list %}
<tr>
<td><a href="{% url "ietf.meeting.views.edit_schedule" schedule.meeting.number schedule.owner_email schedule.name %}">
{{ schedule.name }}</a></td>
<td>{{ schedule.owner }}</td>
<td>{{ schedule.visible_token }}</td>
<td>{{ schedule.public_token }}</td>
<td><a class="btn btn-default" href="{% url "ietf.meeting.views.edit_schedule_properties" schedule.meeting.number schedule.owner_email schedule.name %}">
EDIT</a></td>
</tr>
{% endfor %}
</table>
</div>