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

Edit Timeslots

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