Skip to content

Commit 2eacd88

Browse files
Disable modification of past timeslots on official schedules. Fixes ietf-tools#3166.
- Legacy-Id: 19295
1 parent 24ec2ff commit 2eacd88

8 files changed

Lines changed: 1074 additions & 55 deletions

File tree

ietf/meeting/tests_js.py

Lines changed: 329 additions & 1 deletion
Large diffs are not rendered by default.

ietf/meeting/tests_views.py

Lines changed: 471 additions & 1 deletion
Large diffs are not rendered by default.

ietf/meeting/views.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@
4040
from django.utils.encoding import force_str
4141
from django.utils.functional import curry
4242
from django.utils.text import slugify
43-
from django.views.decorators.cache import cache_page
4443
from django.utils.html import format_html
44+
from django.utils.timezone import now
45+
from django.views.decorators.cache import cache_page
4546
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
4647
from django.views.generic import RedirectView
4748

@@ -468,6 +469,13 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
468469

469470
can_see, can_edit, secretariat = schedule_permissions(meeting, schedule, request.user)
470471

472+
lock_time = settings.MEETING_SESSION_LOCK_TIME
473+
def timeslot_locked(ts):
474+
meeting_now = now().astimezone(pytz.timezone(meeting.time_zone))
475+
if not settings.USE_TZ:
476+
meeting_now = meeting_now.replace(tzinfo=None)
477+
return schedule.is_official and (ts.time - meeting_now < lock_time)
478+
471479
if not can_see:
472480
if request.method == 'POST':
473481
permission_denied(request, "Can't view this schedule.")
@@ -713,21 +721,39 @@ def prepare_timeslots_for_display(timeslots, rooms):
713721

714722
return days
715723

724+
def _json_response(success, status=None, **extra_data):
725+
if status is None:
726+
status = 200 if success else 400
727+
data = dict(success=success, **extra_data)
728+
return JsonResponse(data, status=status)
729+
716730
if request.method == 'POST':
717731
if not can_edit:
718732
permission_denied(request, "Can't edit this schedule.")
719733

720734
action = request.POST.get('action')
721735

722-
# handle ajax requests
736+
# Handle ajax requests. Most of these return JSON responses with at least a 'success' key.
737+
# For the swapdays and swaptimeslots actions, the response is either a redirect to the
738+
# updated page or a simple BadRequest error page. The latter should not normally be seen
739+
# by the user, because the front end should be preventing most invalid requests.
723740
if action == 'assign' and request.POST.get('session', '').isdigit() and request.POST.get('timeslot', '').isdigit():
724741
session = get_object_or_404(sessions, pk=request.POST['session'])
725742
timeslot = get_object_or_404(timeslots_qs, pk=request.POST['timeslot'])
743+
if timeslot_locked(timeslot):
744+
return _json_response(False, error="Can't assign to this timeslot.")
726745

727746
tombstone_session = None
728747

729748
existing_assignments = SchedTimeSessAssignment.objects.filter(session=session, schedule=schedule)
749+
730750
if existing_assignments:
751+
assertion('len(existing_assignments) <= 1',
752+
note='Multiple assignments for {} in schedule {}'.format(session, schedule))
753+
754+
if timeslot_locked(existing_assignments[0].timeslot):
755+
return _json_response(False, error="Can't reassign this session.")
756+
731757
if schedule.pk == meeting.schedule_id and session.current_status == 'sched':
732758
old_timeslot = existing_assignments[0].timeslot
733759
# clone session and leave it as a tombstone
@@ -760,31 +786,44 @@ def prepare_timeslots_for_display(timeslots, rooms):
760786
timeslot=timeslot,
761787
)
762788

763-
r = {'success': True}
764789
if tombstone_session:
765790
prepare_sessions_for_display([tombstone_session])
766-
r['tombstone'] = render_to_string("meeting/edit_meeting_schedule_session.html", {'session': tombstone_session})
767-
return JsonResponse(r)
791+
return _json_response(
792+
True,
793+
tombstone=render_to_string("meeting/edit_meeting_schedule_session.html",
794+
{'session': tombstone_session})
795+
)
796+
else:
797+
return _json_response(True)
768798

769799
elif action == 'unassign' and request.POST.get('session', '').isdigit():
770800
session = get_object_or_404(sessions, pk=request.POST['session'])
771-
SchedTimeSessAssignment.objects.filter(session=session, schedule=schedule).delete()
801+
existing_assignments = SchedTimeSessAssignment.objects.filter(session=session, schedule=schedule)
802+
assertion('len(existing_assignments) <= 1',
803+
note='Multiple assignments for {} in schedule {}'.format(session, schedule))
804+
if not any(timeslot_locked(ea.timeslot) for ea in existing_assignments):
805+
existing_assignments.delete()
806+
else:
807+
return _json_response(False, error="Can't unassign this session.")
772808

773-
return JsonResponse({'success': True})
809+
return _json_response(True)
774810

775811
elif action == 'swapdays':
776812
# updating the client side is a bit complicated, so just
777813
# do a full refresh
778814

779815
swap_days_form = SwapDaysForm(request.POST)
780816
if not swap_days_form.is_valid():
781-
return HttpResponse("Invalid swap: {}".format(swap_days_form.errors), status=400)
817+
return HttpResponseBadRequest("Invalid swap: {}".format(swap_days_form.errors))
782818

783819
source_day = swap_days_form.cleaned_data['source_day']
784820
target_day = swap_days_form.cleaned_data['target_day']
785821

786822
source_timeslots = [ts for ts in timeslots_qs if ts.time.date() == source_day]
787823
target_timeslots = [ts for ts in timeslots_qs if ts.time.date() == target_day]
824+
if any(timeslot_locked(ts) for ts in source_timeslots + target_timeslots):
825+
return HttpResponseBadRequest("Can't swap these days.")
826+
788827
swap_meeting_schedule_timeslot_assignments(schedule, source_timeslots, target_timeslots, target_day - source_day)
789828

790829
return HttpResponseRedirect(request.get_full_path())
@@ -796,7 +835,7 @@ def prepare_timeslots_for_display(timeslots, rooms):
796835
# The origin/target timeslots do not need to be the same duration.
797836
swap_timeslots_form = SwapTimeslotsForm(meeting, request.POST)
798837
if not swap_timeslots_form.is_valid():
799-
return HttpResponse("Invalid swap: {}".format(swap_timeslots_form.errors), status=400)
838+
return HttpResponseBadRequest("Invalid swap: {}".format(swap_timeslots_form.errors))
800839

801840
affected_rooms = swap_timeslots_form.cleaned_data['rooms']
802841
origin_timeslot = swap_timeslots_form.cleaned_data['origin_timeslot']
@@ -812,6 +851,10 @@ def prepare_timeslots_for_display(timeslots, rooms):
812851
time=target_timeslot.time,
813852
duration=target_timeslot.duration,
814853
)
854+
if (any(timeslot_locked(ts) for ts in origin_timeslots)
855+
or any(timeslot_locked(ts) for ts in target_timeslots)):
856+
return HttpResponseBadRequest("Can't swap these timeslots.")
857+
815858
swap_meeting_schedule_timeslot_assignments(
816859
schedule,
817860
list(origin_timeslots),
@@ -820,7 +863,7 @@ def prepare_timeslots_for_display(timeslots, rooms):
820863
)
821864
return HttpResponseRedirect(request.get_full_path())
822865

823-
return HttpResponse("Invalid parameters", status=400)
866+
return _json_response(False, error="Invalid parameters")
824867

825868
# Show only rooms that have regular sessions
826869
rooms = meeting.room_set.filter(session_types__slug='regular')
@@ -904,6 +947,7 @@ def cubehelix(i, total, hue=1.2, start_angle=0.5):
904947
'unassigned_sessions': unassigned_sessions,
905948
'session_parents': session_parents,
906949
'hide_menu': True,
950+
'lock_time': lock_time,
907951
})
908952

909953

ietf/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,9 @@ def skip_unreadable_post(record):
953953

954954
MEETING_USES_CODIMD_DATE = datetime.date(2020,7,6)
955955

956+
# Session assignments on the official schedule lock this long before the timeslot starts
957+
MEETING_SESSION_LOCK_TIME = datetime.timedelta(minutes=10)
958+
956959
# === OpenID Connect Provide Related Settings ==================================
957960

958961
# Used by django-oidc-provider

ietf/static/ietf/css/ietf.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
11121112

11131113
.edit-meeting-schedule .edit-grid .timeslot .time-label {
11141114
display: flex;
1115+
flex-direction: column;
11151116
position: absolute;
11161117
height: 100%;
11171118
width: 100%;
@@ -1141,6 +1142,10 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
11411142
outline: #ffe0e0 solid 0.4em;
11421143
}
11431144

1145+
.edit-meeting-schedule .edit-grid .timeslot.would-violate-hint.dropping {
1146+
background-color: #ccb3b3;
1147+
}
1148+
11441149
.edit-meeting-schedule .constraints .encircled,
11451150
.edit-meeting-schedule .formatted-constraints .encircled {
11461151
border: 1px solid #000;

0 commit comments

Comments
 (0)