From 09b4e13d140087cec9fd4d2808ce357c6afac2d0 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Tue, 17 Oct 2023 21:51:01 +0200 Subject: [PATCH 1/7] Tweak wording on default groups --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4bef17bd..b65a94d4 100644 --- a/README.rst +++ b/README.rst @@ -76,7 +76,7 @@ Installation 7. Wafer uses the Django caching infrastructure in several places, so the cache table needs to be created using ``manage.py createcachetable``. -8. Create the default 'Page Editors' and 'Talk Mentors' groups using +8. Create the default 'Page Editors', 'Talk Mentors' and other useful groups using ``manage.py wafer_add_default_groups``. 9. Log in and configure the Site: From b17db5112d5ae9aeda761fee91389af2fde73918 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Wed, 18 Oct 2023 15:16:24 +0200 Subject: [PATCH 2/7] Fix weird formatting left over from previous changes --- wafer/schedule/admin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wafer/schedule/admin.py b/wafer/schedule/admin.py index 3bee7b45..f49f6410 100644 --- a/wafer/schedule/admin.py +++ b/wafer/schedule/admin.py @@ -356,8 +356,7 @@ def changelist_view(self, request, extra_context=None): if failed_items: errors[err_type].extend(failed_items) extra_context['errors'] = errors - return super().changelist_view(request, - extra_context) + return super().changelist_view(request, extra_context) def get_urls(self): from wafer.schedule.views import ScheduleEditView From 43be5f8c4ba609e96a59243c08a8b9cc9adcc1c7 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Wed, 18 Oct 2023 15:30:19 +0200 Subject: [PATCH 3/7] Add tests for new behaviour --- wafer/schedule/tests/test_views.py | 184 +++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/wafer/schedule/tests/test_views.py b/wafer/schedule/tests/test_views.py index 975a6e80..1d4c7055 100644 --- a/wafer/schedule/tests/test_views.py +++ b/wafer/schedule/tests/test_views.py @@ -991,6 +991,113 @@ def validate_schedule(response): items[2].get_details(), '']), html=True) + def test_view_invalid(self): + """Test that invalid schedules return a inactive schedule.""" + day1 = ScheduleBlock.objects.create( + start_time=D.datetime(2013, 9, 22, 7, 0, 0, + tzinfo=D.timezone.utc), + end_time=D.datetime(2013, 9, 22, 19, 0, 0, + tzinfo=D.timezone.utc), + ) + venue1 = Venue.objects.create(order=1, name='Venue 1') + venue1.blocks.add(day1) + start1 = D.datetime(2013, 9, 22, 10, 0, 0, tzinfo=D.timezone.utc) + start2 = D.datetime(2013, 9, 22, 11, 0, 0, tzinfo=D.timezone.utc) + end = D.datetime(2013, 9, 22, 12, 0, 0, tzinfo=D.timezone.utc) + cur1 = D.datetime(2013, 9, 22, 10, 30, 0, tzinfo=D.timezone.utc) + + slot1 = Slot.objects.create(start_time=start1, end_time=start2) + slot2 = Slot.objects.create(start_time=start1, end_time=end) + + talk = create_talk('Test talk', status=ACCEPTED, username='john') + + item1 = ScheduleItem.objects.create(venue=venue1, + talk_id=talk.pk) + item1.slots.add(slot1) + item2 = ScheduleItem.objects.create(venue=venue1, + talk_id=talk.pk) + item2.slots.add(slot2) + + c = Client() + response = c.get('/schedule/') + assert response.context['active'] is False + assert 'validation_errors' not in response.context + + # Check the logged in users get the same result + ordinary = create_client('jill', False) + response = ordinary.get('/schedule/') + assert response.context['active'] is False + assert 'validation_errors' not in response.context + + # Check that a user with elevated privileges sees validation errors + admin = create_client('admin', True) + + response = admin.get('/schedule/') + assert response.context['active'] is False + assert 'validation_errors' in response.context + + def test_view_hidden(self): + """Test that the schedule is hidden by the appropriate setting.""" + with self.settings(WAFER_HIDE_SCHEDULE=True): + + day1 = ScheduleBlock.objects.create(start_time=D.datetime(2013, 9, 22, 1, 0, 0, + tzinfo=D.timezone.utc), + end_time=D.datetime(2013, 9, 22, 23, 0, 0, + tzinfo=D.timezone.utc), + ) + day2 = ScheduleBlock.objects.create(start_time=D.datetime(2013, 9, 23, 1, 0, 0, + tzinfo=D.timezone.utc), + end_time=D.datetime(2013, 9, 23, 23, 0, 0, + tzinfo=D.timezone.utc), + ) + venue1 = Venue.objects.create(order=1, name='Venue 1') + venue1.blocks.add(day1) + venue1.blocks.add(day2) + venue2 = Venue.objects.create(order=2, name='Venue 2') + venue2.blocks.add(day1) + venue2.blocks.add(day2) + + start1 = D.datetime(2013, 9, 22, 10, 0, 0, tzinfo=D.timezone.utc) + start2 = D.datetime(2013, 9, 22, 11, 0, 0, tzinfo=D.timezone.utc) + end1 = D.datetime(2013, 9, 22, 12, 0, 0, tzinfo=D.timezone.utc) + + start3 = D.datetime(2013, 9, 23, 12, 0, 0, tzinfo=D.timezone.utc) + end2 = D.datetime(2013, 9, 23, 13, 0, 0, tzinfo=D.timezone.utc) + + pages = make_pages(6) + venues = [venue1, venue1, venue1, venue2, venue2, venue2] + items = make_items(venues, pages) + + slot1 = Slot.objects.create(start_time=start1, end_time=start2) + slot2 = Slot.objects.create(start_time=start2, end_time=end1) + slot3 = Slot.objects.create(start_time=start3, end_time=end2) + + items[0].slots.add(slot1) + items[3].slots.add(slot1) + items[1].slots.add(slot2) + items[4].slots.add(slot2) + items[2].slots.add(slot3) + items[5].slots.add(slot3) + + + c = Client() + response = c.get('/schedule/') + assert response.context['active'] is False + assert 'draft_warning' not in response.context + + # Check the logged in users get the same result + ordinary = create_client('jill', False) + response = ordinary.get('/schedule/') + assert response.context['active'] is False + assert 'draft_warning' not in response.context + + # Check that a user with elevated privileges sees validation errors + admin = create_client('admin', True) + + response = admin.get('/schedule/') + assert response.context['active'] is True + assert 'draft_warning' in response.context + class CurrentViewTests(TestCase): @@ -1461,6 +1568,83 @@ def test_current_view_invalid(self): response = c.get('/schedule/current/', {'timestamp': cur1.isoformat()}) assert response.context['active'] is False + assert 'validation_errors' not in response.context + + # Check the logged in users get the same result + ordinary = create_client('jill', False) + response = ordinary.get('/schedule/current/', + {'timestamp': cur1.isoformat()}) + assert response.context['active'] is False + assert 'validation_errors' not in response.context + + # Check that a user with elevated privileges sees validation errors + admin = create_client('admin', True) + + response = admin.get('/schedule/current/', + {'timestamp': cur1.isoformat()}) + assert response.context['active'] is False + assert 'validation_errors' in response.context + + def test_view_hidden(self): + """Test that the schedule is hidden by the appropriate setting.""" + with self.settings(WAFER_HIDE_SCHEDULE=True): + + day1 = ScheduleBlock.objects.create(start_time=D.datetime(2013, 9, 22, 1, 0, 0, + tzinfo=D.timezone.utc), + end_time=D.datetime(2013, 9, 22, 23, 0, 0, + tzinfo=D.timezone.utc), + ) + day2 = ScheduleBlock.objects.create(start_time=D.datetime(2013, 9, 23, 1, 0, 0, + tzinfo=D.timezone.utc), + end_time=D.datetime(2013, 9, 23, 23, 0, 0, + tzinfo=D.timezone.utc), + ) + venue1 = Venue.objects.create(order=1, name='Venue 1') + venue1.blocks.add(day1) + venue1.blocks.add(day2) + venue2 = Venue.objects.create(order=2, name='Venue 2') + venue2.blocks.add(day1) + venue2.blocks.add(day2) + + start1 = D.datetime(2013, 9, 22, 10, 0, 0, tzinfo=D.timezone.utc) + start2 = D.datetime(2013, 9, 22, 11, 0, 0, tzinfo=D.timezone.utc) + end1 = D.datetime(2013, 9, 22, 12, 0, 0, tzinfo=D.timezone.utc) + + start3 = D.datetime(2013, 9, 23, 12, 0, 0, tzinfo=D.timezone.utc) + end2 = D.datetime(2013, 9, 23, 13, 0, 0, tzinfo=D.timezone.utc) + + pages = make_pages(6) + venues = [venue1, venue1, venue1, venue2, venue2, venue2] + items = make_items(venues, pages) + + slot1 = Slot.objects.create(start_time=start1, end_time=start2) + slot2 = Slot.objects.create(start_time=start2, end_time=end1) + slot3 = Slot.objects.create(start_time=start3, end_time=end2) + + items[0].slots.add(slot1) + items[3].slots.add(slot1) + items[1].slots.add(slot2) + items[4].slots.add(slot2) + items[2].slots.add(slot3) + items[5].slots.add(slot3) + + c = Client() + response = c.get('/schedule/current/') + assert response.context['active'] is False + assert 'draft_warning' not in response.context + + # Check the logged in users get the same result + ordinary = create_client('jill', False) + response = ordinary.get('/schedule/current/') + assert response.context['active'] is False + assert 'draft_warning' not in response.context + + # Check that a user with elevated privileges sees validation errors + admin = create_client('admin', True) + + response = admin.get('/schedule/current/') + assert response.context['active'] is True + assert 'draft_warning' in response.context class NonHTMLViewTests(TestCase): From 59902f7ccd38c34f6deb652a625a9c5a8e5bbd85 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Wed, 18 Oct 2023 15:30:40 +0200 Subject: [PATCH 4/7] Add new setting --- wafer/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wafer/settings.py b/wafer/settings.py index 78623a69..0a0b5982 100644 --- a/wafer/settings.py +++ b/wafer/settings.py @@ -359,3 +359,6 @@ 'gitlab': _('gitlab profile'), 'bitbucket': _('bitbucket profile'), } + +# Hide the schedule from users without permission to edit it +WAFER_HIDE_SCHEDULE = False From 24598ce98b8b667eefeb85bb344af948651cbf68 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Wed, 18 Oct 2023 15:32:17 +0200 Subject: [PATCH 5/7] Use new settings in the view --- wafer/schedule/views.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/wafer/schedule/views.py b/wafer/schedule/views.py index 87255f5c..281d4a64 100644 --- a/wafer/schedule/views.py +++ b/wafer/schedule/views.py @@ -169,7 +169,13 @@ def get_context_data(self, **kwargs): context['prev_block'] = None context['next_block'] = None if not check_schedule(): + if self.request.user.is_staff: + context['validation_errors'] = validate_schedule() return context + if settings.WAFER_HIDE_SCHEDULE: + if not self.request.user.is_staff: + return context + context['draft_warning'] = True context['active'] = True try: block_id = int(self.request.GET.get('block', -1)) @@ -220,24 +226,24 @@ def get_context_data(self, **kwargs): class CurrentView(TemplateView): template_name = 'wafer.schedule/current.html' - def _parse_timestamp(self, given_timestamp): + def _parse_timestamp(self, timestamp): """ Parse a user provided timestamp query string parameter. Return a TZ aware datetime, or None. """ - if not given_timestamp: + if not timestamp: return None try: - timestamp = parse_datetime(given_timestamp) + timestamp = parse_datetime(timestamp) except ValueError as e: messages.error(self.request, - f'Failed to parse timestamp ({given_timestamp}): {e}') + 'Failed to parse timestamp: %s' % e) # Short circuit out here return None if timestamp is None: # If parse_datetime completely fails to extract anything # we end up here - messages.error(self.request, f'Failed to parse timestamp {given_timestamp}') + messages.error(self.request, 'Failed to parse timestamp') return None if not timezone.is_aware(timestamp): timestamp = timezone.make_aware(timestamp) @@ -299,8 +305,19 @@ def get_context_data(self, **kwargs): # If the schedule is invalid, return a context with active=False context['active'] = False if not check_schedule(): + if self.request.user.is_staff: + context['validation_errors'] = validate_schedule() return context - # The schedule is valid, so add active=True and empty slots + # The schedule is valid + if settings.WAFER_HIDE_SCHEDULE: + if not self.request.user.is_staff: + # The schedule is hidden, and the user doesn't get to see + # the draft version + return context + # Display the 'This is a draft warning' because we're hiding the + # schedule + context['draft_warning'] = True + # We're displaying the schedule so add active=True and empty slots context['active'] = True context['slots'] = [] # Allow refresh time to be overridden From e4c65f5a308a2bd3dd0e0dd6af95fa51c7e39081 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Wed, 18 Oct 2023 15:32:40 +0200 Subject: [PATCH 6/7] Display additional stuff in the templates --- .../templates/wafer.schedule/current.html | 18 +++++++++++++++++- .../wafer.schedule/full_schedule.html | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/wafer/schedule/templates/wafer.schedule/current.html b/wafer/schedule/templates/wafer.schedule/current.html index 358da4aa..d940affa 100644 --- a/wafer/schedule/templates/wafer.schedule/current.html +++ b/wafer/schedule/templates/wafer.schedule/current.html @@ -5,8 +5,24 @@

{% trans "Happening now" %}

+ {% if draft_warning %} +
+

{% trans "This is a draft that is not visible to the public" %}

+
+ {% endif %} {% if not active %} - {# Schedule is incomplete / invalid, so show nothing #} + {# Show errors to authorised users (checked in get_context_data) #} + {% if validation_errors %} +
+

{% trans "Validation errors:" %}

+
    + {% for validation_error in validation_errors %} +
  • {{ validation_error }}
  • + {% endfor %} +
+
+ {% endif %} + {# Schedule is incomplete / invalid, so show general message #} {% blocktrans trimmed %}

The final schedule has not been published yet.

{% endblocktrans %} diff --git a/wafer/schedule/templates/wafer.schedule/full_schedule.html b/wafer/schedule/templates/wafer.schedule/full_schedule.html index b0bf3530..5ad90bdc 100644 --- a/wafer/schedule/templates/wafer.schedule/full_schedule.html +++ b/wafer/schedule/templates/wafer.schedule/full_schedule.html @@ -15,7 +15,23 @@

{% trans "Schedule" %}

+ {% if draft_warning %} +
+

{% trans "This is a draft that is not visible to the public" %}

+
+ {% endif %} {% if not schedule_pages %} + {# Show errors to authorised users (checked in get_context_data) #} + {% if validation_errors %} +
+

{% trans "Validation errors:" %}

+
    + {% for validation_error in validation_errors %} +
  • {{ validation_error }}
  • + {% endfor %} +
+
+ {% endif %} {# Schedule is incomplete / invalid, so show nothing #} {% blocktrans trimmed %}

The final schedule has not been published yet.

From 029e5a63ae4e449f79daaa124efc88779552bf62 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Wed, 18 Oct 2023 15:32:58 +0200 Subject: [PATCH 7/7] Extend schedule --- docs/schedule.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/schedule.rst b/docs/schedule.rst index 18460cbc..45eca40a 100644 --- a/docs/schedule.rst +++ b/docs/schedule.rst @@ -2,6 +2,12 @@ Schedule ======== +Permissions +=========== + +Setting up the schedule blocks and using the schedule editor requires access +to the admin site. + Specifying Block and Venues =========================== @@ -104,3 +110,23 @@ to add the validators to the list. To display the errors in the admin form, you will also need to extend the ``displayerrors`` block in ``scheduleitem_list.html`` and ``slot_list.html`` templates. + +Schedule fails to render +======================== + +To avoid displaying misleading or incorrect information to attendees, the +schedule will not be rendered if the schedule fails to validate, and it +will display a "The final schedule has not been published" message instead. + +Users with permissions to use the schedule editor will see a list of validation +errors as well, to help diagnose the problem preventing the schedule from +rendering correctly. These errors will also be displayed in the schedule editor +and in the admin site. + + +Hiding the schedule while editing +================================= + +The setting ``WAFER_HIDE_SCHEDULE`` will prevent the schedule from rendering for +users without admin access. Users with admin access will see a note that the +schedule is a draft and not public.