Add new view for editing meeting schedules. This is a preliminary
basic version with timeslots in a fixed grid and drag and drop for assigning and unassigning. Compared to the existing JS based view, it is missing session details (attendences etc.), conflicts and other warnings, toggling of sessions in areas, area coloring, extending to next timeslot and probably more. Add new auxiliary view to copy a schedule for the new schedule editor. - Legacy-Id: 17389
This commit is contained in:
parent
de99911e38
commit
393ee64bec
|
@ -34,7 +34,7 @@ from ietf.meeting.helpers import can_approve_interim_request, can_view_interim_r
|
|||
from ietf.meeting.helpers import send_interim_approval_request
|
||||
from ietf.meeting.helpers import send_interim_cancellation_notice
|
||||
from ietf.meeting.helpers import send_interim_minutes_reminder, populate_important_dates, update_important_dates
|
||||
from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission, SchedulingEvent
|
||||
from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission, SchedulingEvent, Room
|
||||
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting
|
||||
from ietf.meeting.utils import finalize, condition_slide_order
|
||||
from ietf.meeting.utils import add_event_info_to_session_qs
|
||||
|
@ -911,10 +911,104 @@ class EditTests(TestCase):
|
|||
|
||||
def test_edit_schedule(self):
|
||||
meeting = make_meeting_test_data()
|
||||
|
||||
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
r = self.client.get(urlreverse("ietf.meeting.views.edit_schedule", kwargs=dict(num=meeting.number)))
|
||||
self.assertContains(r, "load_assignments")
|
||||
|
||||
def test_edit_meeting_schedule(self):
|
||||
meeting = make_meeting_test_data()
|
||||
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
|
||||
# check we have the grid and everything set up
|
||||
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number))
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
|
||||
room = Room.objects.get(meeting=meeting, session_types='regular')
|
||||
self.assertTrue(q("th:contains(\"{}\")".format(room.name)))
|
||||
self.assertTrue(q("th:contains(\"{}\")".format(room.capacity)))
|
||||
|
||||
timeslots = TimeSlot.objects.filter(meeting=meeting, type='regular')
|
||||
self.assertTrue(q("td:contains(\"{}\")".format(timeslots[0].time.strftime("%H:%M"))))
|
||||
self.assertTrue(q("td.timeslot[data-timeslot=\"{}\"]".format(timeslots[0].pk)))
|
||||
|
||||
sessions = Session.objects.filter(meeting=meeting, type='regular')
|
||||
for s in sessions:
|
||||
self.assertIn(s.group.acronym, q("#session{}".format(s.pk)).text())
|
||||
|
||||
self.assertIn("You can't edit this schedule", r.content)
|
||||
|
||||
# can't change anything
|
||||
r = self.client.post(url, {
|
||||
'action': 'assign',
|
||||
'timeslot': timeslots[0].pk,
|
||||
'session': sessions[0].pk,
|
||||
})
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
# turn us into owner
|
||||
meeting.schedule.owner = Person.objects.get(user__username="secretary")
|
||||
meeting.schedule.save()
|
||||
|
||||
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
|
||||
r = self.client.get(url)
|
||||
self.assertNotIn("You can't edit this schedule", r.content)
|
||||
|
||||
SchedTimeSessAssignment.objects.filter(session=sessions[0]).delete()
|
||||
|
||||
# assign
|
||||
r = self.client.post(url, {
|
||||
'action': 'assign',
|
||||
'timeslot': timeslots[0].pk,
|
||||
'session': sessions[0].pk,
|
||||
})
|
||||
self.assertEqual(r.content, "OK")
|
||||
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=meeting.schedule, session=sessions[0]).timeslot, timeslots[0])
|
||||
|
||||
# move assignment
|
||||
r = self.client.post(url, {
|
||||
'action': 'assign',
|
||||
'timeslot': timeslots[1].pk,
|
||||
'session': sessions[0].pk,
|
||||
})
|
||||
self.assertEqual(r.content, "OK")
|
||||
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=meeting.schedule, session=sessions[0]).timeslot, timeslots[1])
|
||||
|
||||
# unassign
|
||||
r = self.client.post(url, {
|
||||
'action': 'unassign',
|
||||
'session': sessions[0].pk,
|
||||
})
|
||||
self.assertEqual(r.content, "OK")
|
||||
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule, session=sessions[0])), [])
|
||||
|
||||
|
||||
def test_copy_meeting_schedule(self):
|
||||
meeting = make_meeting_test_data()
|
||||
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
|
||||
url = urlreverse("ietf.meeting.views.copy_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# copy
|
||||
r = self.client.post(url, {
|
||||
'name': "newtest",
|
||||
'public': "on",
|
||||
})
|
||||
self.assertNoFormPostErrors(r)
|
||||
|
||||
new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='newtest')
|
||||
self.assertEqual(new_schedule.public, True)
|
||||
self.assertEqual(new_schedule.visible, False)
|
||||
|
||||
old_assignments = {(a.session_id, a.timeslot_id) for a in SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule)}
|
||||
for a in SchedTimeSessAssignment.objects.filter(schedule=new_schedule):
|
||||
self.assertIn((a.session_id, a.timeslot_id), old_assignments)
|
||||
# FIXME: test extendedfrom is copied correctly
|
||||
|
||||
def test_save_agenda_as_and_read_permissions(self):
|
||||
meeting = make_meeting_test_data()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright The IETF Trust 2007-2019, All Rights Reserved
|
||||
# Copyright The IETF Trust 2007-2020, All Rights Reserved
|
||||
|
||||
from django.conf.urls import include
|
||||
from django.views.generic import RedirectView
|
||||
|
@ -27,6 +27,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/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),
|
||||
|
@ -40,6 +41,7 @@ type_ietf_only_patterns = [
|
|||
url(r'^agenda/%(owner)s/%(schedule_name)s/session/(?P<assignment_id>\d+).json$' % settings.URL_REGEXPS, ajax.assignment_json),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/sessions.json$' % settings.URL_REGEXPS, ajax.assignments_json),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s.json$' % settings.URL_REGEXPS, ajax.schedule_infourl),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/copy/$' % settings.URL_REGEXPS, views.copy_meeting_schedule),
|
||||
url(r'^agenda/by-room$', views.agenda_by_room),
|
||||
url(r'^agenda/by-type$', views.agenda_by_type),
|
||||
url(r'^agenda/by-type/(?P<type>[a-z]+)$', views.agenda_by_type),
|
||||
|
@ -76,6 +78,7 @@ type_ietf_only_patterns_id_optional = [
|
|||
url(r'^agenda(?P<ext>.txt)$', views.agenda),
|
||||
url(r'^agenda(?P<ext>.csv)$', views.agenda),
|
||||
url(r'^agenda/edit$', views.edit_schedule),
|
||||
url(r'^agenda/edit/$', views.edit_meeting_schedule),
|
||||
url(r'^requests$', views.meeting_requests),
|
||||
url(r'^agenda/agenda\.ics$', views.ical_agenda),
|
||||
url(r'^agenda\.ics$', views.ical_agenda),
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
import csv
|
||||
import datetime
|
||||
import glob
|
||||
|
@ -42,6 +43,7 @@ from django.template.loader import render_to_string
|
|||
from django.utils.functional import curry
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.utils.text import slugify
|
||||
from django.utils.html import escape
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
|
@ -349,6 +351,266 @@ def edit_timeslots(request, num=None):
|
|||
"ts_list":ts_list,
|
||||
})
|
||||
|
||||
class CopyScheduleForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Schedule
|
||||
fields = ['name', 'visible', 'public']
|
||||
|
||||
def __init__(self, schedule, new_owner, *args, **kwargs):
|
||||
super(CopyScheduleForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.schedule = schedule
|
||||
self.new_owner = new_owner
|
||||
|
||||
username = new_owner.user.username
|
||||
|
||||
name_suggestion = username
|
||||
counter = 2
|
||||
|
||||
existing_names = set(Schedule.objects.filter(meeting=schedule.meeting_id, owner=new_owner).values_list('name', flat=True))
|
||||
while name_suggestion in existing_names:
|
||||
name_suggestion = username + str(counter)
|
||||
counter += 1
|
||||
|
||||
self.fields['name'].initial = name_suggestion
|
||||
self.fields['name'].label = "Name of new schedule"
|
||||
|
||||
def clean_name(self):
|
||||
name = self.cleaned_data.get('name')
|
||||
if name and Schedule.objects.filter(meeting=self.schedule.meeting_id, owner=self.new_owner, name=name):
|
||||
raise forms.ValidationError("Schedule with this name already exists.")
|
||||
return name
|
||||
|
||||
@role_required('Area Director','Secretariat')
|
||||
def copy_meeting_schedule(request, num, owner, name):
|
||||
meeting = get_meeting(num)
|
||||
schedule = get_object_or_404(meeting.schedule_set, owner__email__address=owner, name=name)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CopyScheduleForm(schedule, request.user.person, request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
new_schedule = form.save(commit=False)
|
||||
new_schedule.meeting = schedule.meeting
|
||||
new_schedule.owner = request.user.person
|
||||
new_schedule.save()
|
||||
|
||||
# keep a mapping so that extendedfrom references can be chased
|
||||
old_pk_to_new_pk = {}
|
||||
extendedfroms = {}
|
||||
for assignment in schedule.assignments.all():
|
||||
extendedfrom_id = assignment.extendedfrom_id
|
||||
|
||||
# clone by resetting primary key
|
||||
old_pk = assignment.pk
|
||||
assignment.pk = None
|
||||
assignment.schedule = new_schedule
|
||||
assignment.extendedfrom = None
|
||||
assignment.save()
|
||||
|
||||
old_pk_to_new_pk[old_pk] = assignment.pk
|
||||
if extendedfrom_id is not None:
|
||||
extendedfroms[assignment.pk] = extendedfrom_id
|
||||
|
||||
for pk, extendedfrom_id in extendedfroms.values():
|
||||
if extendedfrom_id in old_pk_to_new_pk:
|
||||
SchedTimeSessAssignment.objects.filter(pk=pk).update(extendedfrom=old_pk_to_new_pk[extendedfrom_id])
|
||||
|
||||
# now redirect to this new schedule
|
||||
return redirect(edit_meeting_schedule, meeting.number, new_schedule.owner_email(), new_schedule.name)
|
||||
|
||||
else:
|
||||
form = CopyScheduleForm(schedule, request.user.person)
|
||||
|
||||
return render(request, "meeting/copy_meeting_schedule.html", {
|
||||
'meeting': meeting,
|
||||
'schedule': schedule,
|
||||
'form': form,
|
||||
})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def edit_meeting_schedule(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))
|
||||
|
||||
can_see, can_edit, secretariat = schedule_permissions(meeting, schedule, request.user)
|
||||
|
||||
if not can_see:
|
||||
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")
|
||||
|
||||
assignments = get_all_assignments_from_schedule(schedule)
|
||||
|
||||
# FIXME
|
||||
#areas = get_areas()
|
||||
#ads = find_ads_for_meeting(meeting)
|
||||
|
||||
css_ems_per_hour = 1.5
|
||||
|
||||
rooms = meeting.room_set.filter(session_types__slug='regular').distinct().order_by("capacity")
|
||||
timeslots_qs = meeting.timeslot_set.filter(type='regular').prefetch_related('type', 'sessions').order_by('time', 'location', 'name')
|
||||
|
||||
sessions = add_event_info_to_session_qs(
|
||||
Session.objects.filter(
|
||||
meeting=meeting,
|
||||
# Restrict graphical scheduling to regular meeting requests (Sessions) for now
|
||||
type='regular',
|
||||
),
|
||||
requested_time=True,
|
||||
requested_by=True,
|
||||
).exclude(current_status__in=['notmeet', 'disappr', 'deleted', 'apprw']).prefetch_related(
|
||||
'resources', 'group', 'group__parent',
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
if not can_edit:
|
||||
return HttpResponseForbidden("Can't edit this schedule")
|
||||
|
||||
action = request.POST.get('action')
|
||||
|
||||
if action == 'assign' and request.POST.get('session', '').isdigit() and request.POST.get('timeslot', '').isdigit():
|
||||
session = get_object_or_404(sessions, pk=request.POST['session'])
|
||||
timeslot = get_object_or_404(timeslots_qs, pk=request.POST['timeslot'])
|
||||
|
||||
existing_assignments = SchedTimeSessAssignment.objects.filter(session=session, schedule=schedule)
|
||||
if existing_assignments:
|
||||
existing_assignments.update(timeslot=timeslot, modified=datetime.datetime.now())
|
||||
else:
|
||||
SchedTimeSessAssignment.objects.create(
|
||||
session=session,
|
||||
schedule=schedule,
|
||||
timeslot=timeslot,
|
||||
)
|
||||
|
||||
return HttpResponse("OK")
|
||||
|
||||
elif action == 'unassign' and request.POST.get('session', '').isdigit():
|
||||
session = get_object_or_404(sessions, pk=request.POST['session'])
|
||||
SchedTimeSessAssignment.objects.filter(session=session, schedule=schedule).delete()
|
||||
|
||||
return HttpResponse("OK")
|
||||
|
||||
return HttpResponse("Invalid parameters", status_code=400)
|
||||
|
||||
assignments_by_session = defaultdict(list)
|
||||
for a in assignments:
|
||||
assignments_by_session[a.session_id].append(a)
|
||||
|
||||
# prepare timeslot matrix
|
||||
times = {} # start time -> end time
|
||||
timeslots = {}
|
||||
timeslots_by_pk = {}
|
||||
for ts in timeslots_qs:
|
||||
ts_end_time = ts.end_time()
|
||||
if ts_end_time < times.get(ts.time, datetime.datetime.max):
|
||||
times[ts.time] = ts_end_time
|
||||
|
||||
timeslots[(ts.location_id, ts.time)] = ts
|
||||
timeslots_by_pk[ts.pk] = ts
|
||||
ts.session_assignments = []
|
||||
|
||||
timeslot_matrix = [
|
||||
(start_time, end_time, (end_time - start_time).seconds / 60.0 / 60.0 * css_ems_per_hour, [(r, timeslots.get((r.pk, start_time))) for r in rooms])
|
||||
for start_time, end_time in sorted(times.items())
|
||||
]
|
||||
|
||||
# prepare sessions
|
||||
unassigned_sessions = []
|
||||
session_data = []
|
||||
for s in sessions:
|
||||
d = {
|
||||
'id': s.pk,
|
||||
}
|
||||
|
||||
if s.requested_by:
|
||||
d['requested_by'] = s.requested_by
|
||||
if s.requested_time:
|
||||
d['requested_time'] = s.requested_time.isoformat()
|
||||
|
||||
room_resources = s.resources.all()
|
||||
if room_resources:
|
||||
d['room_resources'] = [
|
||||
{
|
||||
'name': escape(r.name.name),
|
||||
}
|
||||
for r in room_resources
|
||||
]
|
||||
|
||||
if s.group:
|
||||
label = escape(s.group.acronym)
|
||||
elif s.name:
|
||||
label = escape(s.name)
|
||||
else:
|
||||
label = "???"
|
||||
|
||||
s.scheduling_label = label
|
||||
|
||||
if s.attendees is not None:
|
||||
d['attendees'] = s.attendees
|
||||
|
||||
if s.group and s.group.is_bof():
|
||||
d['bof'] = True
|
||||
|
||||
if s.group and s.group.parent:
|
||||
d['group_parent'] = escape(s.group.parent.acronym.upper())
|
||||
|
||||
if s.comments:
|
||||
d['comments'] = s.comments
|
||||
|
||||
s.requested_duration_in_hours = s.requested_duration.seconds / 60.0 / 60.0
|
||||
s.scheduling_height = s.requested_duration_in_hours * css_ems_per_hour
|
||||
|
||||
scheduled = False
|
||||
|
||||
for a in assignments_by_session.get(s.pk, []):
|
||||
timeslot = timeslots_by_pk.get(a.timeslot_id)
|
||||
if timeslot:
|
||||
timeslot.session_assignments.append((a, s))
|
||||
scheduled = True
|
||||
|
||||
session_data.append(d)
|
||||
|
||||
if not scheduled:
|
||||
unassigned_sessions.append(s)
|
||||
|
||||
schedule_data = {
|
||||
'schedule': schedule.id,
|
||||
'sessions': session_data,
|
||||
'can_edit': can_edit,
|
||||
'urls': {
|
||||
'assign': request.get_full_path()
|
||||
}
|
||||
}
|
||||
|
||||
return render(request, "meeting/edit_meeting_schedule.html", {
|
||||
'meeting': meeting,
|
||||
'schedule': schedule,
|
||||
'can_edit': can_edit,
|
||||
'schedule_data': json.dumps(schedule_data, indent=2),
|
||||
'rooms': rooms,
|
||||
'timeslot_matrix': timeslot_matrix,
|
||||
'unassigned_sessions': unassigned_sessions,
|
||||
'timeslot_width': (100.0 - 10) / len(rooms),
|
||||
#'areas': areas,
|
||||
'hide_menu': True,
|
||||
})
|
||||
|
||||
|
||||
##############################################################################
|
||||
#@role_required('Area Director','Secretariat')
|
||||
# disable the above security for now, check it below.
|
||||
|
@ -421,6 +683,7 @@ def edit_schedule(request, num=None, owner=None, name=None):
|
|||
"hide_menu": True,
|
||||
})
|
||||
|
||||
|
||||
##############################################################################
|
||||
# show the properties associated with a schedule (visible, public)
|
||||
#
|
||||
|
@ -446,7 +709,7 @@ def edit_schedule_properties(request, num=None, owner=None, name=None):
|
|||
if form.is_valid():
|
||||
form.save()
|
||||
return HttpResponseRedirect(reverse('ietf.meeting.views.list_schedules',kwargs={'num': num}))
|
||||
else:
|
||||
else:
|
||||
form = SchedulePropertiesForm(instance=schedule)
|
||||
return render(request, "meeting/properties_edit.html",
|
||||
{"schedule":schedule,
|
||||
|
|
|
@ -983,3 +983,80 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
padding: 8px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* === Edit Meeting Schedule ====================================== */
|
||||
|
||||
.edit-meeting-schedule {
|
||||
padding-bottom: 10em; /* ensure there's room for the scheduling panel */
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session {
|
||||
background-color: #fff;
|
||||
padding: 0 0.4em;
|
||||
margin: 0.2em 0.4em;
|
||||
border-radius: 0.3em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session[draggable] {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session.dragging {
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session i.fa-comment-o {
|
||||
width: 0; /* prevent icon from participating in text centering */
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-meeting-grid th {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-meeting-grid td.day {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-meeting-grid td.timeslot {
|
||||
border: 2px solid #fff;
|
||||
background-color: #f6f6f6;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-meeting-grid td.timeslot.disabled {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-meeting-grid td.timeslot.dropping {
|
||||
background-color: #f0f0f0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .scheduling-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0 1em;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
opacity: 0.95;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .unassigned-sessions {
|
||||
min-height: 4em;
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .unassigned-sessions.dropping {
|
||||
background-color: #f0f0f0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .unassigned-sessions .session {
|
||||
display: inline-block;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
|
94
ietf/static/ietf/js/edit-meeting-schedule.js
Normal file
94
ietf/static/ietf/js/edit-meeting-schedule.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
jQuery(document).ready(function () {
|
||||
if (!ietfScheduleData.can_edit)
|
||||
return;
|
||||
|
||||
var content = jQuery(".edit-meeting-schedule");
|
||||
|
||||
function failHandler(xhr, textStatus, error) {
|
||||
alert("Error: " + error);
|
||||
}
|
||||
|
||||
var sessions = content.find(".session");
|
||||
|
||||
// dragging
|
||||
sessions.on("dragstart", function (event) {
|
||||
event.originalEvent.dataTransfer.setData("text/plain", this.id);
|
||||
jQuery(this).addClass("dragging");
|
||||
});
|
||||
sessions.on("dragend", function () {
|
||||
jQuery(this).removeClass("dragging");
|
||||
|
||||
});
|
||||
|
||||
sessions.prop('draggable', true);
|
||||
|
||||
// dropping
|
||||
var dropElements = content.find(".timeslot,.unassigned-sessions");
|
||||
dropElements.on('dragenter', function (event) {
|
||||
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
|
||||
return;
|
||||
|
||||
if (jQuery(this).hasClass("disabled"))
|
||||
return;
|
||||
|
||||
event.preventDefault(); // default action is signalling that this is not a valid target
|
||||
jQuery(this).addClass("dropping");
|
||||
});
|
||||
|
||||
dropElements.on('dragover', function (event) {
|
||||
// we don't actually need this event, except we need to signal
|
||||
// that this is a valid drop target, by cancelling the default
|
||||
// action
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
dropElements.on('dragleave', function (event) {
|
||||
jQuery(this).removeClass("dropping");
|
||||
});
|
||||
|
||||
dropElements.on('drop', function (event) {
|
||||
jQuery(this).removeClass("dropping");
|
||||
|
||||
var sessionId = event.originalEvent.dataTransfer.getData("text/plain");
|
||||
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
|
||||
return;
|
||||
|
||||
var sessionElement = sessions.filter("#" + sessionId);
|
||||
if (sessionElement.length == 0)
|
||||
return;
|
||||
|
||||
event.preventDefault(); // prevent opening as link
|
||||
|
||||
if (sessionElement.parent().is(this))
|
||||
return;
|
||||
|
||||
var dropElement = jQuery(this);
|
||||
|
||||
function done() {
|
||||
dropElement.append(sessionElement); // move element
|
||||
}
|
||||
|
||||
if (dropElement.hasClass("unassigned-sessions")) {
|
||||
jQuery.ajax({
|
||||
url: ietfScheduleData.urls.assign,
|
||||
method: "post",
|
||||
data: {
|
||||
action: "unassign",
|
||||
session: sessionId.slice("session".length)
|
||||
}
|
||||
}).fail(failHandler).done(done);
|
||||
}
|
||||
else {
|
||||
jQuery.ajax({
|
||||
url: ietfScheduleData.urls.assign,
|
||||
method: "post",
|
||||
data: {
|
||||
action: "assign",
|
||||
session: sessionId.slice("session".length),
|
||||
timeslot: dropElement.data("timeslot")
|
||||
}
|
||||
}).fail(failHandler).done(done);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
20
ietf/templates/meeting/copy_meeting_schedule.html
Normal file
20
ietf/templates/meeting/copy_meeting_schedule.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load staticfiles %}
|
||||
{% load ietf_filters %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>{% block title %}Copy schedule {{ schedule.name }}{% endblock %}</h1>
|
||||
|
||||
<form class="form-horizontal" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-default">Create schedule</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endblock %}
|
110
ietf/templates/meeting/edit_meeting_schedule.html
Normal file
110
ietf/templates/meeting/edit_meeting_schedule.html
Normal file
|
@ -0,0 +1,110 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load staticfiles %}
|
||||
{% load ietf_filters %}
|
||||
|
||||
{% block morecss %}
|
||||
{# FIXME #}
|
||||
{% for area in area_list %}
|
||||
.group-colored-{{ area.upcase_acronym}} {
|
||||
border-color: {{ area.fg_color}};
|
||||
color:{{ area.fg_color }};
|
||||
background-color: {{ area.bg_color }}
|
||||
}
|
||||
{% endfor %}
|
||||
{% endblock morecss %}
|
||||
|
||||
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting schedule{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
jQuery.ajaxSetup({
|
||||
crossDomain: false, // obviates need for sameOrigin test
|
||||
beforeSend: function(xhr, settings) {
|
||||
if (!csrfSafeMethod(settings.type)) {
|
||||
xhr.setRequestHeader("X-CSRFToken", $.cookie('csrftoken'));
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript" src="{% static 'ietf/js/edit-meeting-schedule.js' %}"></script>
|
||||
<script type='text/javascript'>
|
||||
var ietfScheduleData = {{ schedule_data|safe }};
|
||||
</script>
|
||||
{% endblock js %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<div class="edit-meeting-schedule">
|
||||
|
||||
<p class="pull-right">
|
||||
<a href="{% url "ietf.meeting.views.copy_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">Copy schedule</a>
|
||||
·
|
||||
|
||||
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">All schedules for meeting</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Schedule name: {{ schedule.name }}
|
||||
|
||||
·
|
||||
|
||||
Owner: {{ schedule.owner }}
|
||||
|
||||
{% if not can_edit %}
|
||||
·
|
||||
|
||||
<em>You can't edit this schedule. Take a copy first.</em>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<table class="table edit-meeting-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for r in rooms %}
|
||||
<th>{{ r.name }}{% if r.capacity %} - {{ r.capacity }} <i class="fa fa-user-o"></i>{% endif %}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for start_time, end_time, hours, room_timeslots in timeslot_matrix %}
|
||||
{% ifchanged %}
|
||||
<tr>
|
||||
<td class="day" colspan="1000">
|
||||
<strong>{{ start_time|date:"l, F j, Y" }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{% endifchanged %}
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div class="period" style="min-height:{{ hours }}em;">{{ start_time|date:"G:i" }}-{{ end_time|date:"G:i" }}</div>
|
||||
</td>
|
||||
|
||||
{% for r, timeslot in room_timeslots %}
|
||||
<td class="timeslot {% if not timeslot %}disabled{% endif %}" {% if timeslot %}data-timeslot="{{ timeslot.pk }}"{% endif %} style="width:{{ timeslot_width }}%">
|
||||
{% for assignment, session in timeslot.session_assignments %}
|
||||
{% include "meeting/edit_meeting_schedule_session.html" %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="scheduling-panel">
|
||||
<h4>Unscheduled</h4>
|
||||
<div class="unassigned-sessions">
|
||||
{% for session in unassigned_sessions %}
|
||||
{% include "meeting/edit_meeting_schedule_session.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,3 @@
|
|||
<div id="session{{ session.pk }}" class="session" style="min-height:{{ session.scheduling_height }}em;">
|
||||
{{ session.scheduling_label }} {% if session.comments %}<i class="fa fa-comment-o"></i>{% endif %}
|
||||
</div>
|
Loading…
Reference in a new issue