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/<meeting>/<schedule>/miscsessions/
/secr/meetings/<meeting>/<schedule>/regularsessions/
/meeting/<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
This commit is contained in:
Ole Laursen 2020-08-18 13:24:30 +00:00
parent c78ffbcd18
commit 996fc716eb
9 changed files with 918 additions and 35 deletions

View file

@ -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()

View file

@ -28,6 +28,7 @@ safe_for_all_meeting_types = [
type_ietf_only_patterns = [
url(r'^agenda/%(owner)s/%(schedule_name)s/edit$' % settings.URL_REGEXPS, views.edit_schedule),
url(r'^agenda/%(owner)s/%(schedule_name)s/edit/$' % settings.URL_REGEXPS, views.edit_meeting_schedule),
url(r'^agenda/%(owner)s/%(schedule_name)s/timeslots/$' % settings.URL_REGEXPS, views.edit_meeting_timeslots_and_misc_sessions),
url(r'^agenda/%(owner)s/%(schedule_name)s/details$' % settings.URL_REGEXPS, views.edit_schedule_properties),
url(r'^agenda/%(owner)s/%(schedule_name)s/delete$' % settings.URL_REGEXPS, views.delete_schedule),
url(r'^agenda/%(owner)s/%(schedule_name)s/make_official$' % settings.URL_REGEXPS, views.make_schedule_official),

View file

@ -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.<br>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.

View file

@ -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;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -8,12 +8,6 @@
<h1>{% block title %}Possible Meeting Agendas for IETF {{ meeting.number }}{% endblock %}</h1>
{% comment %}
<div>
<p><a href="{% url "ietf.meeting.views.edit_timeslots" meeting.number %}">Edit Timeslots</a></p>
</div>
{% endcomment %}
<div>
{% for schedules, own, label in schedule_groups %}
<div class="panel panel-default">
@ -35,16 +29,12 @@
<th class="col-md-3">Notes</th>
<th class="col-md-1">Visible</th>
<th class="col-md-1">Public</th>
<td></td>
</tr>
{% for schedule in schedules %}
<tr>
<td>
<a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.owner_email schedule.name %}">{{ schedule.name }}</a>
{% if schedule.can_edit_properties %}
<a class="edit-schedule-properties" href="{% url "ietf.meeting.views.edit_schedule_properties" meeting.number schedule.owner_email schedule.name %}?next={{ request.get_full_path|urlencode }}">
<i title="Edit agenda properties" class="fa fa-edit"></i>
</a>
{% endif %}
<a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.owner_email schedule.name %}" title="Show regular sessions in agenda">{{ schedule.name }}</a>
</td>
<td>{{ schedule.owner }}</td>
<td>
@ -55,7 +45,7 @@
</td>
<td>
{% if schedule.base %}
<a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.base.owner_email schedule.base.name %}">{{ schedule.base.name }}</a>
<a href="{% url "ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions" meeting.number schedule.base.owner_email schedule.base.name %}">{{ schedule.base.name }}</a>
{% endif %}
</td>
<td>{{ schedule.notes|linebreaksbr }}</td>
@ -73,6 +63,17 @@
<div class="label label-danger">private</div>
{% endif %}
</td>
<td>
{% if schedule.can_edit_properties %}
<a class="edit-schedule-properties" href="{% url "ietf.meeting.views.edit_schedule_properties" meeting.number schedule.owner_email schedule.name %}?next={{ request.get_full_path|urlencode }}">
<i title="Edit agenda properties" class="fa fa-edit"></i>
</a>
{% endif %}
<a href="{% url "ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions" meeting.number schedule.owner_email schedule.name %}">
<i title="Show time slots and misc. sessions for agenda" class="fa fa-calendar"></i>
</a>
</td>
</tr>
{% endfor %}
</table>