4040from django .utils .encoding import force_str
4141from django .utils .functional import curry
4242from django .utils .text import slugify
43- from django .views .decorators .cache import cache_page
4443from django .utils .html import format_html
44+ from django .utils .timezone import now
45+ from django .views .decorators .cache import cache_page
4546from django .views .decorators .csrf import ensure_csrf_cookie , csrf_exempt
4647from 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
0 commit comments