Add support in the new meeting schedule editor for making a tombstone

session when rescheduling a session after the schedule is made the
official meeting schedule.

Show both cancelled and rescheduled sessions as tombstones in the new
meeting schedule editor.

Add support for showing rescheduled tombstones in the meeting agenda
views.

Adjust the Secretariat session tool so that it's not possible to
(re)cancel cancelled or rescheduled tombstones.
 - Legacy-Id: 18108
This commit is contained in:
Ole Laursen 2020-06-30 16:55:24 +00:00
parent 4678f0b799
commit 45ed2c5a2c
14 changed files with 332 additions and 138 deletions

View file

@ -212,10 +212,15 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
parents = Group.objects.filter(pk__in=parent_id_set)
parent_replacements = find_history_replacements_active_at(parents, meeting_time)
timeslot_by_session_pk = {a.session_id: a.timeslot for a in assignments}
for a in assignments:
if a.session and a.session.historic_group and a.session.historic_group.parent_id:
a.session.historic_group.historic_parent = parent_replacements.get(a.session.historic_group.parent_id)
if a.session.current_status == 'resched':
a.session.rescheduled_to = timeslot_by_session_pk.get(a.session.tombstone_for_id)
for d in a.session.prefetched_active_materials:
# make sure these are precomputed with the meeting instead
# of having to look it up

View file

@ -0,0 +1,19 @@
# Copyright The IETF Trust 2020, All Rights Reserved
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('meeting', '0028_auto_20200501_0139'),
]
operations = [
migrations.AddField(
model_name='session',
name='tombstone_for',
field=models.ForeignKey(blank=True, help_text='This session is the tombstone for a session that was rescheduled', null=True, on_delete=django.db.models.deletion.CASCADE, to='meeting.Session'),
),
]

View file

@ -923,6 +923,8 @@ class Session(models.Model):
modified = models.DateTimeField(auto_now=True)
remote_instructions = models.CharField(blank=True,max_length=1024)
tombstone_for = models.ForeignKey('Session', blank=True, null=True, help_text="This session is the tombstone for a session that was rescheduled", on_delete=models.CASCADE)
materials = models.ManyToManyField(Document, through=SessionPresentation, blank=True)
resources = models.ManyToManyField(ResourceAssociation, blank=True)

View file

@ -16,9 +16,11 @@ import debug # pyflakes:ignore
from ietf.doc.factories import DocumentFactory
from ietf.group import colors
from ietf.person.models import Person
from ietf.meeting.factories import SessionFactory
from ietf.meeting.test_data import make_meeting_test_data
from ietf.meeting.models import Schedule, SchedTimeSessAssignment, Session, Room, TimeSlot, Constraint, ConstraintName
from ietf.meeting.models import SchedulingEvent, SessionStatusName
from ietf.utils.test_runner import IetfLiveServerTestCase
from ietf.utils.pipe import pipe
from ietf import settings
@ -107,6 +109,12 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
s2b = Session.objects.create(meeting=meeting, group=s2.group, attendees=10, requested_duration=datetime.timedelta(minutes=60), type_id='regular')
SchedulingEvent.objects.create(
session=s2b,
status=SessionStatusName.objects.get(slug='appr'),
by=Person.objects.get(name='(System)'),
)
Constraint.objects.create(
meeting=meeting,
source=s1.group,
@ -226,7 +234,7 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
self.assertTrue(s1_element.is_displayed())
# hide timeslots
self.driver.find_element_by_css_selector(".timeslot-group-toggles button".format(s1.group.parent.acronym)).click()
self.driver.find_element_by_css_selector(".timeslot-group-toggles button").click()
self.assertTrue(self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed())
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click()
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click()

View file

@ -1049,10 +1049,14 @@ class EditTests(TestCase):
self.assertEqual(r.status_code, 403)
# turn us into owner
meeting.schedule.owner = Person.objects.get(user__username="secretary")
meeting.schedule.save()
schedule = meeting.schedule
schedule.owner = Person.objects.get(user__username="secretary")
schedule.save()
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
meeting.schedule = None
meeting.save()
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=schedule.owner_email(), name=schedule.name))
r = self.client.get(url)
q = PyQuery(r.content)
self.assertTrue(not q("em:contains(\"You can't edit this schedule\")"))
@ -1065,25 +1069,52 @@ class EditTests(TestCase):
'timeslot': timeslots[0].pk,
'session': s1.pk,
})
self.assertEqual(r.content, b"OK")
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=meeting.schedule, session=s1).timeslot, timeslots[0])
self.assertEqual(json.loads(r.content)['success'], True)
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0])
# move assignment
# move assignment on unofficial schedule
r = self.client.post(url, {
'action': 'assign',
'timeslot': timeslots[1].pk,
'session': s1.pk,
})
self.assertEqual(r.content, b"OK")
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=meeting.schedule, session=s1).timeslot, timeslots[1])
self.assertEqual(json.loads(r.content)['success'], True)
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[1])
# move assignment on official schedule, leaving tombstone
meeting.schedule = schedule
meeting.save()
SchedulingEvent.objects.create(
session=s1,
status=SessionStatusName.objects.get(slug='sched'),
by=Person.objects.get(name='(System)')
)
r = self.client.post(url, {
'action': 'assign',
'timeslot': timeslots[0].pk,
'session': s1.pk,
})
json_content = json.loads(r.content)
self.assertEqual(json_content['success'], True)
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0])
sessions_for_group = Session.objects.filter(group=s1.group, meeting=meeting)
self.assertEqual(len(sessions_for_group), 2)
s_tombstone = [s for s in sessions_for_group if s != s1][0]
self.assertEqual(s_tombstone.tombstone_for, s1)
tombstone_event = SchedulingEvent.objects.get(session=s_tombstone)
self.assertEqual(tombstone_event.status_id, 'resched')
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s_tombstone).timeslot, timeslots[1])
self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.tombstone".format(s_tombstone.pk)).html())
# unassign
r = self.client.post(url, {
'action': 'unassign',
'session': s1.pk,
})
self.assertEqual(r.content, b"OK")
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule, session=s1)), [])
self.assertEqual(json.loads(r.content)['success'], True)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), [])
def test_copy_meeting_schedule(self):

View file

@ -26,14 +26,14 @@ import debug # pyflakes:ignore
from django import forms
from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404, JsonResponse
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.urls import reverse,reverse_lazy
from django.db.models import F, Min, Max, Prefetch, Q
from django.db.models import F, Min, Max, Q
from django.forms.models import modelform_factory, inlineformset_factory
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
@ -51,7 +51,6 @@ from ietf.doc.models import Document, State, DocEvent, NewRevisionDocEvent, DocA
from ietf.group.models import Group
from ietf.group.utils import can_manage_session_materials, can_manage_some_groups, can_manage_group
from ietf.person.models import Person
from ietf.person.name import plain_name
from ietf.ietfauth.utils import role_required, has_role
from ietf.mailtrigger.utils import gather_address_lists
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
@ -459,7 +458,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
assignments = get_all_assignments_from_schedule(schedule)
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('location', 'time', 'name')
tombstone_states = ['canceled', 'canceledpa', 'resched']
sessions = add_event_info_to_session_qs(
Session.objects.filter(
@ -469,45 +469,14 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
).order_by('pk'),
requested_time=True,
requested_by=True,
).exclude(current_status__in=['notmeet', 'disappr', 'deleted', 'apprw']).prefetch_related(
).filter(
Q(current_status__in=['appr', 'schedw', 'scheda', 'sched'])
| Q(current_status__in=tombstone_states, pk__in={a.session_id for a in assignments})
).prefetch_related(
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups',
)
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 layout
timeslots_qs = meeting.timeslot_set.filter(type='regular').prefetch_related('type', 'sessions').order_by('location', 'time', 'name')
min_duration = min(t.duration for t in timeslots_qs)
max_duration = max(t.duration for t in timeslots_qs)
@ -525,6 +494,119 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
scale = (capped_timedelta - capped_min_d) / (capped_max_d - capped_min_d) if capped_min_d != capped_max_d else 1
return min_d_css_rems + (max_d_css_rems - min_d_css_rems) * scale
def prepare_sessions_for_display(sessions):
# requesters
requested_by_lookup = {p.pk: p for p in Person.objects.filter(pk__in=set(s.requested_by for s in sessions if s.requested_by))}
# constraints
constraints_for_sessions, formatted_constraints_for_sessions, constraint_names = preprocess_constraints_for_meeting_schedule_editor(meeting, sessions)
sessions_for_group = defaultdict(list)
for s in sessions:
sessions_for_group[s.group_id].append(s)
for s in sessions:
s.requested_by_person = requested_by_lookup.get(s.requested_by)
s.scheduling_label = "???"
if s.group:
s.scheduling_label = s.group.acronym
elif s.name:
s.scheduling_label = s.name
s.requested_duration_in_hours = round(s.requested_duration.seconds / 60.0 / 60.0, 1)
session_layout_margin = 0.2
s.layout_width = timedelta_to_css_ems(s.requested_duration) - 2 * session_layout_margin
s.parent_acronym = s.group.parent.acronym if s.group and s.group.parent else ""
# compress the constraints, so similar constraint labels are
# shared between the conflicting sessions they cover - the JS
# then simply has to detect violations and show the
# preprocessed labels
constrained_sessions_grouped_by_label = defaultdict(set)
for name_id, ts in itertools.groupby(sorted(constraints_for_sessions.get(s.pk, [])), key=lambda t: t[0]):
ts = list(ts)
session_pks = (t[1] for t in ts)
constraint_name = constraint_names[name_id]
if "{count}" in constraint_name.formatted_editor_label:
for session_pk, grouped_session_pks in itertools.groupby(session_pks):
count = sum(1 for i in grouped_session_pks)
constrained_sessions_grouped_by_label[format_html(constraint_name.formatted_editor_label, count=count)].add(session_pk)
else:
constrained_sessions_grouped_by_label[constraint_name.formatted_editor_label].update(session_pks)
s.constrained_sessions = list(constrained_sessions_grouped_by_label.items())
s.formatted_constraints = formatted_constraints_for_sessions.get(s.pk, {})
s.other_sessions = [s_other for s_other in sessions_for_group.get(s.group_id) if s != s_other]
s.is_tombstone = s.current_status in tombstone_states
if request.method == 'POST': # handle ajax requests
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'])
tombstone_session = None
existing_assignments = SchedTimeSessAssignment.objects.filter(session=session, schedule=schedule)
if existing_assignments:
if schedule.pk == meeting.schedule_id and session.current_status == 'sched':
old_timeslot = existing_assignments[0].timeslot
# clone session and leave it as a tombstone
tombstone_session = session
tombstone_session.tombstone_for_id = session.pk
tombstone_session.pk = None
tombstone_session.save()
session = None
SchedulingEvent.objects.create(
session=tombstone_session,
status=SessionStatusName.objects.get(slug='resched'),
by=request.user.person,
)
tombstone_session.current_status = 'resched' # rematerialize status for the rendering
SchedTimeSessAssignment.objects.create(
session=tombstone_session,
schedule=schedule,
timeslot=old_timeslot,
)
existing_assignments.update(timeslot=timeslot, modified=datetime.datetime.now())
else:
SchedTimeSessAssignment.objects.create(
session=session,
schedule=schedule,
timeslot=timeslot,
)
r = {'success': True}
if tombstone_session:
prepare_sessions_for_display([tombstone_session])
r['tombstone'] = render_to_string("meeting/edit_meeting_schedule_session.html", {'session': tombstone_session})
return JsonResponse(r)
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 JsonResponse({'success': True})
return HttpResponse("Invalid parameters", status_code=400)
# prepare timeslot layout
timeslots_by_room_and_day = defaultdict(list)
room_has_timeslots = set()
for t in timeslots_qs:
@ -559,10 +641,28 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
timeslot_groups[ts.time.date()].add((ts.time, ts.end_time(), ts.start_end_group))
# prepare sessions
prepare_sessions_for_display(sessions)
for ts in timeslots_qs:
ts.session_assignments = []
timeslots_by_pk = {ts.pk: ts for ts in timeslots_qs}
assignments_by_session = defaultdict(list)
for a in assignments:
assignments_by_session[a.session_id].append(a)
unassigned_sessions = []
for s in sessions:
assigned = 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))
assigned = True
if not assigned:
unassigned_sessions.append(s)
# group parent colors
def cubehelix(i, total, hue=1.2, start_angle=0.5):
# theory in https://arxiv.org/pdf/1108.5083.pdf
@ -586,64 +686,6 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
p.scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round(x * 255)) for x in rgb_color))
p.light_scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round((0.9 + 0.1 * x) * 255)) for x in rgb_color))
# requesters
requested_by_lookup = {p.pk: p for p in Person.objects.filter(pk__in=set(s.requested_by for s in sessions if s.requested_by))}
# constraints
constraints_for_sessions, formatted_constraints_for_sessions, constraint_names = preprocess_constraints_for_meeting_schedule_editor(meeting, sessions)
sessions_for_group = defaultdict(list)
for s in sessions:
sessions_for_group[s.group_id].append(s)
unassigned_sessions = []
for s in sessions:
s.requested_by_person = requested_by_lookup.get(s.requested_by)
s.scheduling_label = "???"
if s.group:
s.scheduling_label = s.group.acronym
elif s.name:
s.scheduling_label = s.name
s.requested_duration_in_hours = round(s.requested_duration.seconds / 60.0 / 60.0, 1)
session_layout_margin = 0.2
s.layout_width = timedelta_to_css_ems(s.requested_duration) - 2 * session_layout_margin
s.parent_acronym = s.group.parent.acronym if s.group and s.group.parent else ""
# compress the constraints, so similar constraint labels are
# shared between the conflicting sessions they cover - the JS
# then simply has to detect violations and show the
# preprocessed labels
constrained_sessions_grouped_by_label = defaultdict(set)
for name_id, ts in itertools.groupby(sorted(constraints_for_sessions.get(s.pk, [])), key=lambda t: t[0]):
ts = list(ts)
session_pks = (t[1] for t in ts)
constraint_name = constraint_names[name_id]
if "{count}" in constraint_name.formatted_editor_label:
for session_pk, grouped_session_pks in itertools.groupby(session_pks):
count = sum(1 for i in grouped_session_pks)
constrained_sessions_grouped_by_label[format_html(constraint_name.formatted_editor_label, count=count)].add(session_pk)
else:
constrained_sessions_grouped_by_label[constraint_name.formatted_editor_label].update(session_pks)
s.constrained_sessions = list(constrained_sessions_grouped_by_label.items())
s.formatted_constraints = formatted_constraints_for_sessions.get(s.pk, {})
s.other_sessions = [s_other for s_other in sessions_for_group.get(s.group_id) if s != s_other]
assigned = 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))
assigned = True
if not assigned:
unassigned_sessions.append(s)
js_data = {
'can_edit': can_edit,
'urls': {
@ -1214,9 +1256,19 @@ def room_view(request, num=None, name=None, owner=None):
template = "meeting/room-view.html"
return render(request, template,{"meeting":meeting,"schedule":schedule,"unavailable":unavailable,"assignments":assignments,"rooms":rooms,"days":days})
def ical_session_status(session_with_current_status):
if session_with_current_status == 'canceled':
def ical_session_status(assignment):
if assignment.session.current_status == 'canceled':
return "CANCELLED"
elif assignment.session.current_status == 'resched':
t = "RESCHEDULED"
if assignment.session.tombstone_for_id is not None:
other_assignment = SchedTimeSessAssignment.objects.filter(schedule=assignment.schedule_id, session=assignment.session.tombstone_for_id).first()
if other_assignment:
t = "RESCHEDULED TO {}-{}".format(
other_assignment.timeslot.time.strftime("%A %H:%M").upper(),
other_assignment.timeslot.end_time().strftime("%H:%M")
)
return t
else:
return "CONFIRMED"
@ -1268,7 +1320,7 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None):
for a in assignments:
if a.session:
a.session.ical_status = ical_session_status(a.session.current_status)
a.session.ical_status = ical_session_status(a)
return render(request, "meeting/agenda.ics", {
"schedule": schedule,
@ -2655,7 +2707,7 @@ def upcoming_ical(request):
for a in assignments:
if a.session_id is not None:
a.session = sessions.get(a.session_id) or a.session
a.session.ical_status = ical_session_status(a.session.current_status)
a.session.ical_status = ical_session_status(a)
# gather vtimezones
vtimezones = set()

View file

@ -11653,6 +11653,16 @@
"model": "name.sessionstatusname",
"pk": "schedw"
},
{
"fields": {
"desc": "",
"name": "Rescheduled",
"order": 0,
"used": true
},
"model": "name.sessionstatusname",
"pk": "resched"
},
{
"fields": {
"desc": "",

View file

@ -0,0 +1,24 @@
# Copyright The IETF Trust 2020, All Rights Reserved
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('name', '0012_update_constraintname_order_and_label'),
]
def add_rescheduled_session_status_name(apps, schema_editor):
SessionStatusName = apps.get_model('name', 'SessionStatusName')
SessionStatusName.objects.get_or_create(
slug='resched',
name="Rescheduled",
)
def noop(apps, schema_editor):
pass
operations = [
migrations.RunPython(add_rescheduled_session_status_name, noop, elidable=True),
]

View file

@ -659,17 +659,21 @@ def regular_sessions(request, meeting_id, schedule_name):
if 'cancel' in request.POST:
pk = request.POST.get('pk')
session = get_object_or_404(sessions, pk=pk)
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='canceled'),
by=request.user.person,
)
messages.success(request, 'Session cancelled')
if session.current_status not in ['canceled', 'resched']:
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='canceled'),
by=request.user.person,
)
messages.success(request, 'Session cancelled')
return redirect('ietf.secr.meetings.views.regular_sessions', meeting_id=meeting_id, schedule_name=schedule_name)
status_names = {n.slug: n.name for n in SessionStatusName.objects.all()}
for s in sessions:
s.current_status_name = status_names.get(s.current_status, s.current_status)
s.can_cancel = s.current_status not in ['canceled', 'resched']
return render(request, 'meetings/sessions.html', {
'meeting': meeting,

View file

@ -36,11 +36,13 @@
<td>{{ session.current_status_name }}</td>
<td><a href="{% url 'ietf.secr.meetings.views.regular_session_edit' meeting_id=meeting.number schedule_name=schedule.name session_id=session.id %}">Edit</a></td>
<td>
<form method="post">
{% csrf_token %}
<input type="hidden" name="pk" value="{{ session.pk }}">
<input type="submit" name="cancel" value="Cancel">
</form>
{% if session.can_cancel %}
<form method="post">
{% csrf_token %}
<input type="hidden" name="pk" value="{{ session.pk }}">
<input type="submit" name="cancel" value="Cancel">
</form>
{% endif %}
</td>
</tr>
{% endfor %}

View file

@ -1118,6 +1118,11 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
cursor: default;
}
.edit-meeting-schedule .session.tombstone {
cursor: default;
background-color: #ddd;
}
.edit-meeting-schedule .session.selected .session-label {
font-weight: bold;
}

View file

@ -1,14 +1,14 @@
jQuery(document).ready(function () {
let content = jQuery(".edit-meeting-schedule");
function failHandler(xhr, textStatus, error) {
let errorText = error;
function reportServerError(xhr, textStatus, error) {
let errorText = error || textStatus;
if (xhr && xhr.responseText)
errorText += "\n\n" + xhr.responseText;
alert("Error: " + errorText);
}
let sessions = content.find(".session");
let sessions = content.find(".session").not(".tombstone");
let timeslots = content.find(".timeslot");
let days = content.find(".day-flow .day");
@ -130,7 +130,6 @@ jQuery(document).ready(function () {
});
sessions.on("dragend", function () {
jQuery(this).removeClass("dragging");
});
sessions.prop('draggable', true);
@ -161,31 +160,50 @@ jQuery(document).ready(function () {
});
dropElements.on('drop', function (event) {
jQuery(this).parent().removeClass("dropping");
let dropElement = jQuery(this);
let sessionId = event.originalEvent.dataTransfer.getData("text/plain");
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session") {
dropElement.parent().removeClass("dropping");
return;
}
let sessionElement = sessions.filter("#" + sessionId);
if (sessionElement.length == 0)
if (sessionElement.length == 0) {
dropElement.parent().removeClass("dropping");
return;
}
event.preventDefault(); // prevent opening as link
if (sessionElement.parent().is(this))
let dragParent = sessionElement.parent();
if (dragParent.is(this)) {
dropElement.parent().removeClass("dropping");
return;
}
let dropElement = jQuery(this);
let dropParent = dropElement.parent();
function failHandler(xhr, textStatus, error) {
dropElement.parent().removeClass("dropping");
console.log("xhr", xhr)
console.log("textstatus", textStatus)
console.log("error", error)
reportServerError(xhr, textStatus, error);
}
function done(response) {
if (response != "OK") {
failHandler(null, null, response);
dropElement.parent().removeClass("dropping");
if (!response.success) {
reportServerError(null, null, response);
return;
}
dropElement.append(sessionElement); // move element
if (response.tombstone)
dragParent.append(response.tombstone);
updateCurrentSchedulingHints();
if (dropParent.hasClass("unassigned-sessions"))
sortUnassigned();

View file

@ -234,7 +234,7 @@
<span class="hidden-xs">
{% if item.timeslot.type.slug == 'other' %}
{% if item.session.agenda %}
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
{% else %}
{% for slide in item.session.slides %}
<a href="{{slide.get_href}}">{{ slide.title|clean_whitespace }}</a>
@ -323,6 +323,20 @@
<span class="label label-danger pull-right">CANCELLED</span>
{% endif %}
{% if item.session.current_status == 'resched' %}
<span class="label label-danger pull-right">
RESCHEDULED
{% if item.session.rescheduled_to %}
TO
{% if "-utc" in request.path %}
{{ item.session.rescheduled_to.utc_start_time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.utc_end_time|date:"G:i" }}
{% else %}
{{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}
{% endif %}
{% endif %}
</span>
{% endif %}
{% if item.session.agenda_note|first_url %}
<br><a href={{item.session.agenda_note|first_url}}>{{item.session.agenda_note|slice:":23"}}</a>
{% elif item.session.agenda_note %}
@ -332,7 +346,7 @@
</td>
<td class="text-nowrap text-right">
<span class="hidden-xs">
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
</span>
</td>
</tr>

View file

@ -22,7 +22,7 @@
{% endif %}{% if item.timeslot.type_id == 'regular' %}{% if item.session.historic_group %}{% ifchanged %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% endif %}
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% elif item.session.current_status == 'resched' %} *** RESCHEDULED{% if item.session.rescheduled_to %} TO {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}{% endif %} ***{% endif %}
{% endif %}{% endif %}{% if item.timeslot.type.slug == "break" %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.break_area and item.timeslot.show_location %} - {{ schedule.meeting.break_area }}{% endif %}{% endif %}{% if item.timeslot.type.slug == "other" %}
{{ item.timeslot.time_desc }} {{ item.timeslot.name }} - {{ item.timeslot.location.name }}{% endif %}{% endfor %}