Repository: ietf-tools/datatracker

Issue:
Persistent meeting schedule editor view settings
Usability of the schedule editor can be improved by making the timeslot type / session purpose visibility settings persistent. At present, navigating away from the editor then returning will normally cause these settings to be reset to default. This adds many additional UI steps to tasks like editing session details.


Discussion:

Resolving patch:
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -728,10 +728,18 @@ def _json_response(success, status=None, **extra_data):
         return JsonResponse(data, status=status)
 
     if request.method == 'POST':
-        if not can_edit:
-            permission_denied(request, "Can't edit this schedule.")
-
         action = request.POST.get('action')
+        if action == 'updateview':
+            # allow updateview action even if can_edit is false, it affects only the user's session
+            sess_data = request.session.setdefault('edit_meeting_schedule', {})
+            enabled_types = [ts_type.slug for ts_type in TimeSlotTypeName.objects.filter(
+                used=True,
+                slug__in=request.POST.getlist('enabled_timeslot_types[]', [])
+            )]
+            sess_data['enabled_timeslot_types'] = enabled_types
+            return _json_response(True)
+        elif not can_edit:
+            permission_denied(request, "Can't edit this schedule.")
 
         # Handle ajax requests. Most of these return JSON responses with at least a 'success' key.
         # For the swapdays and swaptimeslots actions, the response is either a redirect to the
@@ -949,6 +957,16 @@ def cubehelix(i, total, hue=1.2, start_angle=0.5):
         key=lambda tstype: tstype.name,
     )
 
+    # extract view configuration from session store
+    session_data = request.session.get('edit_meeting_schedule', None)
+    if session_data is None:
+        enabled_timeslot_types = ['regular']
+    else:
+        enabled_timeslot_types = [
+            ts_type.slug for ts_type in timeslot_types
+            if ts_type.slug in session_data.get('enabled_timeslot_types', [])
+        ]
+
     return render(request, "meeting/edit_meeting_schedule.html", {
         'meeting': meeting,
         'schedule': schedule,
@@ -963,6 +981,7 @@ def cubehelix(i, total, hue=1.2, start_angle=0.5):
         'timeslot_types': timeslot_types,
         'hide_menu': True,
         'lock_time': lock_time,
+        'enabled_timeslot_types': enabled_timeslot_types,
     })
 
 


Test patch:
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -3898,6 +3898,69 @@ def test_slot_to_the_right(self):
         self.assertTrue(mars_slot.slot_to_the_right)
         self.assertTrue(mars_scheduled.slot_to_the_right)
 
+    def test_updateview(self):
+        """The updateview action should set visible timeslot types in the session"""
+        meeting = MeetingFactory(type_id='ietf')
+        url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number})
+        types_to_enable = ['regular', 'reg', 'other']
+        r = self.client.post(
+            url,
+            {
+                'action': 'updateview',
+                'enabled_timeslot_types[]': types_to_enable,
+            },
+        )
+        self.assertEqual(r.status_code, 200)
+        session_data = self.client.session
+        self.assertIn('edit_meeting_schedule', session_data)
+        self.assertCountEqual(
+            session_data['edit_meeting_schedule']['enabled_timeslot_types'],
+            types_to_enable,
+            'Should set types requested',
+        )
+
+        r = self.client.post(
+            url,
+            {
+                'action': 'updateview',
+                'enabled_timeslot_types[]': types_to_enable + ['faketype'],
+            },
+        )
+        self.assertEqual(r.status_code, 200)
+        session_data = self.client.session
+        self.assertIn('edit_meeting_schedule', session_data)
+        self.assertCountEqual(
+            session_data['edit_meeting_schedule']['enabled_timeslot_types'],
+            types_to_enable,
+            'Should ignore unknown types',
+        )
+
+    def test_persistent_enabled_timeslot_types(self):
+        meeting = MeetingFactory(type_id='ietf')
+        TimeSlotFactory(meeting=meeting, type_id='other')
+        TimeSlotFactory(meeting=meeting, type_id='reg')
+
+        # test default behavior (only 'regular' enabled)
+        r = self.client.get(urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number}))
+        self.assertEqual(r.status_code, 200)
+        q = PyQuery(r.content)
+        self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="regular"][checked]')), 1)
+        self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="other"]:not([checked])')), 1)
+        self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="reg"]:not([checked])')), 1)
+
+        # test with 'regular' and 'other' enabled via session store
+        client_session = self.client.session  # must store as var, new session is created on access
+        client_session['edit_meeting_schedule'] = {
+            'enabled_timeslot_types': ['regular', 'other']
+        }
+        client_session.save()
+        r = self.client.get(urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number}))
+        self.assertEqual(r.status_code, 200)
+        q = PyQuery(r.content)
+        self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="regular"][checked]')), 1)
+        self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="other"][checked]')), 1)
+        self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="reg"]:not([checked])')), 1)
+
 
 class SessionDetailsTests(TestCase):