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:
parent
4678f0b799
commit
45ed2c5a2c
|
@ -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
|
||||
|
|
19
ietf/meeting/migrations/0029_session_tombstone_for.py
Normal file
19
ietf/meeting/migrations/0029_session_tombstone_for.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -11653,6 +11653,16 @@
|
|||
"model": "name.sessionstatusname",
|
||||
"pk": "schedw"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"name": "Rescheduled",
|
||||
"order": 0,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.sessionstatusname",
|
||||
"pk": "resched"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
|
|
24
ietf/name/migrations/0013_add_rescheduled_session_name.py
Normal file
24
ietf/name/migrations/0013_add_rescheduled_session_name.py
Normal 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),
|
||||
]
|
|
@ -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,
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in a new issue