diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index ab1d4cf55..39a753e53 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -1171,6 +1171,161 @@ class EditTests(TestCase):
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")
+
+ # 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)
+ 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')
+
+ r = self.client.post(url, {
+ '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)
+
+ 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()
diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py
index 4dcdc5ff6..670558b95 100644
--- a/ietf/meeting/urls.py
+++ b/ietf/meeting/urls.py
@@ -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),
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 21f0d39df..9bbe17072 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -54,7 +54,8 @@ from ietf.person.models import Person
from ietf.ietfauth.utils import role_required, has_role, user_is_person
from ietf.mailtrigger.utils import gather_address_lists
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
-from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment
+from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment, Room, TimeSlotTypeName
+from ietf.meeting.forms import CustomDurationField
from ietf.meeting.helpers import get_areas, get_person_by_email, get_schedule_by_name
from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list
from ietf.meeting.helpers import get_all_assignments_from_schedule
@@ -470,13 +471,12 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
if request.method == 'POST':
return HttpResponseForbidden("Can't view this schedule")
- # FIXME: check this
- return render(request, "meeting/private_schedule.html",
- {"schedule":schedule,
- "meeting": meeting,
- "meeting_base_url": request.build_absolute_uri(meeting.base_url()),
- "hide_menu": True
- }, status=403, content_type="text/html")
+ return render(request, "meeting/private_schedule.html", {
+ "schedule":schedule,
+ "meeting": meeting,
+ "meeting_base_url": request.build_absolute_uri(meeting.base_url()),
+ "hide_menu": True
+ }, status=403, content_type="text/html")
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base],
@@ -495,7 +495,6 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
sessions = add_event_info_to_session_qs(
Session.objects.filter(
meeting=meeting,
- # Restrict graphical scheduling to regular meeting requests (Sessions) for now
type='regular',
).order_by('pk'),
requested_time=True,
@@ -746,6 +745,328 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
})
+class RoomNameModelChoiceField(forms.ModelChoiceField):
+ def label_from_instance(self, obj):
+ return obj.name
+
+class TimeSlotForm(forms.Form):
+ day = forms.TypedChoiceField(coerce=lambda t: datetime.datetime.strptime(t, "%Y-%m-%d").date())
+ time = forms.TimeField()
+ duration = CustomDurationField() # this is just to make 1:30 turn into 1.5 hours instead of 1.5 minutes
+ location = RoomNameModelChoiceField(queryset=Room.objects.all(), required=False, empty_label="(No location)")
+ show_location = forms.BooleanField(initial=True, required=False)
+ type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(used=True), empty_label=None, required=False)
+ name = forms.CharField(help_text='Name that appears on the agenda', required=False)
+ short = forms.CharField(max_length=32,label='Short name', help_text='Abbreviated session name used for material file names', required=False)
+ group = forms.ModelChoiceField(queryset=Group.objects.filter(type__in=['ietf', 'team'], state='active'),
+ help_text='''Select a group to associate with this session.
For example: Tutorials = Education, Code Sprint = Tools Team''',
+ required=False)
+ agenda_note = forms.CharField(required=False)
+
+ def __init__(self, meeting, schedule, *args, timeslot=None, **kwargs):
+ super().__init__(*args,**kwargs)
+
+ self.fields["time"].widget.attrs["placeholder"] = "HH:MM"
+ self.fields["duration"].widget.attrs["placeholder"] = "HH:MM"
+ self.fields["duration"].initial = ""
+
+ self.fields["day"].choices = [
+ ((meeting.date + datetime.timedelta(days=i)).isoformat(), (meeting.date + datetime.timedelta(days=i)).strftime("%a %b %d"))
+ for i in range(meeting.days)
+ ]
+
+ self.fields['location'].queryset = self.fields['location'].queryset.filter(meeting=meeting)
+
+ self.fields['group'].widget.attrs['data-ietf'] = Group.objects.get(acronym='ietf').pk
+
+ self.active_assignment = None
+
+ if timeslot:
+ self.initial = {
+ 'day': timeslot.time.date(),
+ 'time': timeslot.time.time(),
+ 'duration': timeslot.duration,
+ 'location': timeslot.location_id,
+ 'show_location': timeslot.show_location,
+ 'type': timeslot.type_id,
+ 'name': timeslot.name,
+ }
+
+ assignments = sorted(SchedTimeSessAssignment.objects.filter(
+ timeslot=timeslot,
+ schedule__in=[schedule, schedule.base if schedule else None]
+ ).select_related('session', 'session__group'), key=lambda a: 0 if a.schedule_id == schedule.pk else 1)
+
+ if assignments:
+ self.active_assignment = assignments[0]
+
+ self.initial['short'] = self.active_assignment.session.short
+ self.initial['group'] = self.active_assignment.session.group_id
+
+ if not self.active_assignment or timeslot.type_id != 'regular':
+ del self.fields['agenda_note'] # at the moment, the UI only shows this field for regular sessions
+
+ self.timeslot = timeslot
+
+ def clean(self):
+ group = self.cleaned_data.get('group')
+ ts_type = self.cleaned_data.get('type')
+ short = self.cleaned_data.get('short')
+
+ if ts_type:
+ if ts_type.slug in ['break', 'reg', 'reserved', 'unavail', 'regular']:
+ if ts_type.slug != 'regular':
+ self.cleaned_data['group'] = self.fields['group'].queryset.get(acronym='secretariat')
+ else:
+ if not group:
+ self.add_error('group', 'When scheduling this type of time slot, a group must be associated')
+ if not short:
+ self.add_error('short', 'When scheduling this type of time slot, a short name is required')
+
+ if self.timeslot and self.timeslot.type_id == 'regular' and self.active_assignment and ts_type.pk != self.timeslot.type_id:
+ self.add_error('type', "Can't change type on time slots for regular sessions when a session has been assigned")
+
+ if self.active_assignment and self.active_assignment.session.group != self.cleaned_data.get('group') and self.active_assignment.session.materials.exists() and self.timeslot.type_id != 'regular':
+ self.add_error('group', "Can't change group after materials have been uploaded")
+
+
+@role_required('Area Director', 'Secretariat')
+def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name=None):
+ meeting = get_meeting(num)
+ if name is None:
+ schedule = meeting.schedule
+ else:
+ schedule = get_schedule_by_name(meeting, get_person_by_email(owner), name)
+
+ if schedule is None:
+ raise Http404("No meeting information for meeting %s owner %s schedule %s available" % (num, owner, name))
+
+ rooms = list(Room.objects.filter(meeting=meeting).prefetch_related('session_types').order_by('-capacity', 'name'))
+ rooms.append(Room(name="(No location)"))
+
+ timeslot_qs = TimeSlot.objects.filter(meeting=meeting).prefetch_related('type').order_by('time')
+
+ can_edit = has_role(request.user, 'Secretariat')
+
+ if request.method == 'GET' and request.GET.get('action') == "edit-timeslot":
+ timeslot_pk = request.GET.get('timeslot')
+ if not timeslot_pk or not timeslot_pk.isdecimal():
+ raise Http404
+ timeslot = get_object_or_404(timeslot_qs, pk=timeslot_pk)
+
+ assigned_session = add_event_info_to_session_qs(Session.objects.filter(
+ timeslotassignments__schedule__in=[schedule, schedule.base],
+ timeslotassignments__timeslot=timeslot,
+ )).first()
+
+ timeslot.can_cancel = not assigned_session or assigned_session.current_status not in ['canceled', 'canceled', 'resched']
+
+ return JsonResponse({
+ 'form': render_to_string("meeting/edit_timeslot_form.html", {
+ 'timeslot_form_action': 'edit',
+ 'timeslot_form': TimeSlotForm(meeting, schedule, timeslot=timeslot),
+ 'timeslot': timeslot,
+ 'schedule': schedule,
+ 'meeting': meeting,
+ 'can_edit': can_edit,
+ }, request=request)
+ })
+
+ scroll = request.POST.get('scroll')
+
+ def redirect_with_scroll():
+ url = request.get_full_path()
+ if scroll and scroll.isdecimal():
+ url += "#scroll={}".format(scroll)
+ return HttpResponseRedirect(url)
+
+ add_timeslot_form = None
+ if request.method == 'POST' and request.POST.get('action') == 'add-timeslot' and can_edit:
+ add_timeslot_form = TimeSlotForm(meeting, schedule, request.POST)
+ if add_timeslot_form.is_valid():
+ c = add_timeslot_form.cleaned_data
+
+ timeslot, created = TimeSlot.objects.get_or_create(
+ meeting=meeting,
+ type=c['type'],
+ name=c['name'],
+ time=datetime.datetime.combine(c['day'], c['time']),
+ duration=c['duration'],
+ location=c['location'],
+ show_location=c['show_location'],
+ )
+
+ if timeslot.type_id != 'regular':
+ if not created:
+ Session.objects.filter(timeslotassignments__timeslot=timeslot).delete()
+
+ session = Session.objects.create(
+ meeting=meeting,
+ name=c['name'],
+ short=c['short'],
+ group=c['group'],
+ type=c['type'],
+ agenda_note=c.get('agenda_note') or "",
+ )
+
+ SchedulingEvent.objects.create(
+ session=session,
+ status=SessionStatusName.objects.get(slug='sched'),
+ by=request.user.person,
+ )
+
+ SchedTimeSessAssignment.objects.create(
+ timeslot=timeslot,
+ session=session,
+ schedule=schedule
+ )
+
+ return redirect_with_scroll()
+
+ edit_timeslot_form = None
+ if request.method == 'POST' and request.POST.get('action') == 'edit-timeslot' and can_edit:
+ timeslot_pk = request.POST.get('timeslot')
+ if not timeslot_pk or not timeslot_pk.isdecimal():
+ raise Http404
+
+ timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk)
+
+ edit_timeslot_form = TimeSlotForm(meeting, schedule, request.POST, timeslot=timeslot)
+ if edit_timeslot_form.is_valid() and edit_timeslot_form.active_assignment.schedule_id == schedule.pk:
+
+ c = edit_timeslot_form.cleaned_data
+
+ timeslot.type = c['type']
+ timeslot.name = c['name']
+ timeslot.time = datetime.datetime.combine(c['day'], c['time'])
+ timeslot.duration = c['duration']
+ timeslot.location = c['location']
+ timeslot.show_location = c['show_location']
+ timeslot.save()
+
+ session = Session.objects.filter(
+ timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None],
+ timeslotassignments__timeslot=timeslot,
+ ).select_related('group').first()
+
+ if session:
+ if timeslot.type_id != 'regular':
+ session.name = c['name']
+ session.short = c['short']
+ session.group = c['group']
+ session.type = c['type']
+ session.agenda_note = c.get('agenda_note') or ""
+ session.save()
+
+ return redirect_with_scroll()
+
+ if request.method == 'POST' and request.POST.get('action') == 'cancel-timeslot' and can_edit:
+ timeslot_pk = request.POST.get('timeslot')
+ if not timeslot_pk or not timeslot_pk.isdecimal():
+ raise Http404
+
+ timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk)
+ if timeslot.type_id != 'break':
+ sessions = add_event_info_to_session_qs(
+ Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=timeslot),
+ ).exclude(current_status__in=['canceled', 'resched'])
+ for session in sessions:
+ SchedulingEvent.objects.create(
+ session=session,
+ status=SessionStatusName.objects.get(slug='canceled'),
+ by=request.user.person,
+ )
+
+ return redirect_with_scroll()
+
+ if request.method == 'POST' and request.POST.get('action') == 'delete-timeslot' and can_edit:
+ timeslot_pk = request.POST.get('timeslot')
+ if not timeslot_pk or not timeslot_pk.isdecimal():
+ raise Http404
+
+ timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk)
+
+ if timeslot.type_id != 'regular':
+ for session in Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=timeslot):
+ for doc in session.materials.all():
+ doc.set_state(State.objects.get(type=doc.type_id, slug='deleted'))
+ e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='deleted')
+ e.desc = "Deleted meeting session"
+ e.save()
+
+ session.delete()
+
+ timeslot.delete()
+
+ return redirect_with_scroll()
+
+ sessions_by_pk = {
+ s.pk: s for s in
+ add_event_info_to_session_qs(
+ Session.objects.filter(
+ meeting=meeting,
+ ).order_by('pk'),
+ requested_time=True,
+ requested_by=True,
+ ).filter(
+ current_status__in=['appr', 'schedw', 'scheda', 'sched', 'canceled', 'canceledpa', 'resched']
+ ).prefetch_related(
+ 'group', 'group', 'group__type',
+ )
+ }
+
+ assignments_by_timeslot = defaultdict(list)
+ for a in SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base]):
+ assignments_by_timeslot[a.timeslot_id].append(a)
+
+ days = [meeting.date + datetime.timedelta(days=i) for i in range(meeting.days)]
+
+ timeslots_by_day_and_room = defaultdict(list)
+ for t in timeslot_qs:
+ timeslots_by_day_and_room[(t.time.date(), t.location_id)].append(t)
+
+ min_time = min([t.time.time() for t in timeslot_qs] + [datetime.time(8)])
+ max_time = max([t.end_time().time() for t in timeslot_qs] + [datetime.time(22)])
+ min_max_delta = datetime.datetime.combine(meeting.date, max_time) - datetime.datetime.combine(meeting.date, min_time)
+
+ day_grid = []
+ for d in days:
+ room_timeslots = []
+ for r in rooms:
+ ts = []
+ for t in timeslots_by_day_and_room.get((d, r.pk), []):
+ if t.type_id == 'regular' and not any(t.slug == 'regular' for t in r.session_types.all()):
+ continue
+ t.assigned_sessions = []
+ for a in assignments_by_timeslot.get(t.pk, []):
+ s = sessions_by_pk.get(a.session_id)
+ if s:
+ t.assigned_sessions.append(s)
+
+ t.left_offset = 100.0 * (t.time - datetime.datetime.combine(t.time.date(), min_time)) / min_max_delta
+ t.layout_width = min(100.0 * t.duration / min_max_delta, 100 - t.left_offset)
+ ts.append(t)
+
+ room_timeslots.append((r, ts))
+
+ day_grid.append({
+ 'day': d,
+ 'room_timeslots': room_timeslots
+ })
+
+ return render(request, "meeting/edit_meeting_timeslots_and_misc_sessions.html", {
+ 'meeting': meeting,
+ 'schedule': schedule,
+ 'can_edit': can_edit,
+ 'day_grid': day_grid,
+ 'empty_timeslot_form': TimeSlotForm(meeting, schedule),
+ 'add_timeslot_form': add_timeslot_form,
+ 'edit_timeslot_form': edit_timeslot_form,
+ 'scroll': scroll,
+ 'hide_menu': True,
+ })
+
+
##############################################################################
#@role_required('Area Director','Secretariat')
# disable the above security for now, check it below.
diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css
index 8870f923c..112c3a585 100644
--- a/ietf/static/ietf/css/ietf.css
+++ b/ietf/static/ietf/css/ietf.css
@@ -1005,11 +1005,6 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
/* === List Meeting Schedules ====================================== */
-.table a.edit-schedule-properties {
- display: inline-block;
- margin-left: 0.2em;
-}
-
.from-base-schedule {
opacity: 0.7;
}
@@ -1317,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;
+}
diff --git a/ietf/static/ietf/js/edit-meeting-timeslots-and-misc-sessions.js b/ietf/static/ietf/js/edit-meeting-timeslots-and-misc-sessions.js
new file mode 100644
index 000000000..b247e9e29
--- /dev/null
+++ b/ietf/static/ietf/js/edit-meeting-timeslots-and-misc-sessions.js
@@ -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("");
+ });
+
+ 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");
+});
diff --git a/ietf/templates/meeting/edit_meeting_timeslots_and_misc_sessions.html b/ietf/templates/meeting/edit_meeting_timeslots_and_misc_sessions.html
new file mode 100644
index 000000000..a83b31bd9
--- /dev/null
+++ b/ietf/templates/meeting/edit_meeting_timeslots_and_misc_sessions.html
@@ -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 %}
+
+{% endblock js %}
+
+
+{% block content %}
+ {% origin %}
+
+ Other Agendas +
+ ++ Meeting time slots and misc. sessions for agenda: {{ schedule.name }} {% if not can_edit %}(you do not have permission to edit time slots){% endif %} +
+ +You cannot edit this session here - it is set up in the base schedule
+{% endif %} diff --git a/ietf/templates/meeting/private_schedule.html b/ietf/templates/meeting/private_schedule.html index de5362546..9aa647659 100644 --- a/ietf/templates/meeting/private_schedule.html +++ b/ietf/templates/meeting/private_schedule.html @@ -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 %}You do not have access this agenda. It belongs to {{ schedule.owner }}.
- +