From 996fc716eb714dfbf80c60c42027e28e640a7a4e Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 18 Aug 2020 13:24:30 +0000 Subject: [PATCH] Add new page for editing timeslots and misc session, accessible from the possible agendas list for a meeting. The new page incorporates the editing functionality otherwise accessible from /secr/meetings///miscsessions/ /secr/meetings///regularsessions/ /meeting//timeslots/edit in a time vs. room grid that gives a better overview of what's going on each day and allows adding and editing time slots and sessions. Regular sessions must be scheduled in the other schedule editor, but it's possible to add regular timeslots and edit agenda notes on scheduled regular sessions. - Legacy-Id: 18379 --- ietf/meeting/tests_views.py | 155 ++++++++ ietf/meeting/urls.py | 1 + ietf/meeting/views.py | 339 +++++++++++++++++- ietf/static/ietf/css/ietf.css | 132 ++++++- ...dit-meeting-timeslots-and-misc-sessions.js | 156 ++++++++ ...t_meeting_timeslots_and_misc_sessions.html | 92 +++++ .../templates/meeting/edit_timeslot_form.html | 41 +++ ietf/templates/meeting/private_schedule.html | 10 +- ietf/templates/meeting/schedule_list.html | 27 +- 9 files changed, 918 insertions(+), 35 deletions(-) create mode 100644 ietf/static/ietf/js/edit-meeting-timeslots-and-misc-sessions.js create mode 100644 ietf/templates/meeting/edit_meeting_timeslots_and_misc_sessions.html create mode 100644 ietf/templates/meeting/edit_timeslot_form.html 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 %} +

+ +
+ {% for day in day_grid %} +
+
+ {{ day.day|date:"l" }} + {{ day.day|date:"N j, Y" }} +
+ + {% for room, timeslots in day.room_timeslots %} +
+
+ {{ room.name }} + {% if room.capacity %}{{ room.capacity }}{% endif %} +
+ +
+ {% for t in timeslots %} +
+ {% for s in t.assigned_sessions %} + + {% if s.name %} + {{ s.name }} + {% if s.group %} + ({{ s.group.acronym }}) + {% endif %} + {% elif s.group %} + {{ s.group.acronym }} + {% endif %} + + {% empty %} + {% if t.type_id == 'regular' %} + (session) + {% elif t.name %} + {{ t.name }} + {% else %} + {{ t.type.name }} + {% endif %} + {% endfor %} + {{ t.time|date:"G:i" }}-{{ t.end_time|date:"G:i" }} +
+ {% endfor %} +
+
+ {% endfor %} +
+ {% endfor %} +
+ + + +
+ + +
+ {% 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 %} +
+
+ +
+{% endblock %} diff --git a/ietf/templates/meeting/edit_timeslot_form.html b/ietf/templates/meeting/edit_timeslot_form.html new file mode 100644 index 000000000..efe6ca0c8 --- /dev/null +++ b/ietf/templates/meeting/edit_timeslot_form.html @@ -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 %} +
{% csrf_token %} +
+ {% 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 %} +
+ + {% if can_edit %} + + + {% if timeslot %} + + + {% if timeslot.type_id != 'break' and timeslot.can_cancel %} + + {% endif %} + + + {% endif %} + {% endif %} +
+{% elif schedule.base %} +

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 }}.

-

List your meetings.

+

Other agendas for this meeting.

diff --git a/ietf/templates/meeting/schedule_list.html b/ietf/templates/meeting/schedule_list.html index 194b02ad1..c37c22745 100644 --- a/ietf/templates/meeting/schedule_list.html +++ b/ietf/templates/meeting/schedule_list.html @@ -8,12 +8,6 @@

{% block title %}Possible Meeting Agendas for IETF {{ meeting.number }}{% endblock %}

- {% comment %} - - {% endcomment %} -
{% for schedules, own, label in schedule_groups %}
@@ -35,16 +29,12 @@ Notes Visible Public + {% for schedule in schedules %} - {{ schedule.name }} - {% if schedule.can_edit_properties %} - - - - {% endif %} + {{ schedule.name }} {{ schedule.owner }} @@ -55,7 +45,7 @@ {% if schedule.base %} - {{ schedule.base.name }} + {{ schedule.base.name }} {% endif %} {{ schedule.notes|linebreaksbr }} @@ -73,6 +63,17 @@
private
{% endif %} + + {% if schedule.can_edit_properties %} + + + + {% endif %} + + + + + {% endfor %}