diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 1c56cb8..f89650d 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index f3bcfc6..b5887f3 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/templates/core/event_dashboard.html b/core/templates/core/event_dashboard.html index a88cf28..73cddd4 100644 --- a/core/templates/core/event_dashboard.html +++ b/core/templates/core/event_dashboard.html @@ -17,6 +17,19 @@ +
+
+
+ Backup +

Download a full event backup

+

Export every event into one JSON file so you can keep an offline backup before larger edits or restore the calendar later if needed.

+
+ +
+
+
diff --git a/core/tests.py b/core/tests.py index 60b2f28..75c4e28 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,4 +1,5 @@ from datetime import datetime +import json from django.contrib.auth.models import User from django.test import Client, TestCase @@ -64,6 +65,18 @@ class EventDashboardMutationTests(TestCase): is_published=True, ) + def test_staff_can_export_event_backup(self): + self.client.login(username='staffer', password='pass12345') + response = self.client.get(reverse('event_export')) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json') + self.assertIn('attachment; filename="roadshow-calendar-events-', response['Content-Disposition']) + payload = json.loads(response.content) + self.assertEqual(payload['version'], 1) + self.assertEqual(payload['event_count'], 1) + self.assertEqual(payload['events'][0]['slug'], self.event.slug) + self.assertEqual(payload['events'][0]['name'], self.event.name) + def test_staff_can_open_edit_page(self): self.client.login(username='staffer', password='pass12345') response = self.client.get(reverse('event_edit', args=[self.event.slug])) diff --git a/core/urls.py b/core/urls.py index 4f0ba4c..fd44bcf 100644 --- a/core/urls.py +++ b/core/urls.py @@ -8,6 +8,7 @@ from .views import ( event_dashboard, event_dashboard_detail, event_delete, + event_export, event_detail, event_edit, event_list, @@ -21,6 +22,7 @@ urlpatterns = [ path('events/', event_list, name='event_list'), path('events//', event_detail, name='event_detail'), path('dashboard/events/', event_dashboard, name='event_dashboard'), + path('dashboard/events/export/', event_export, name='event_export'), path('dashboard/events/new/', event_create, name='event_create'), path('dashboard/events//edit/', event_edit, name='event_edit'), path('dashboard/events//delete/', event_delete, name='event_delete'), diff --git a/core/views.py b/core/views.py index 47f241f..615a255 100644 --- a/core/views.py +++ b/core/views.py @@ -1,9 +1,11 @@ import calendar from collections import defaultdict from datetime import datetime, time, timedelta +import json from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.http import HttpResponse from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse @@ -115,6 +117,30 @@ def _show_embed_header(request): return request.GET.get('header', '1') != '0' +def _serialize_event(event): + return { + 'name': event.name, + 'slug': event.slug, + 'location': event.location, + 'start': event.start.isoformat(), + 'end': event.end.isoformat(), + 'event_url': event.event_url, + 'summary': event.summary, + 'is_published': event.is_published, + } + + +def _build_event_backup_payload(): + events = list(Event.objects.all().order_by('start', 'name')) + return { + 'version': 1, + 'project': PROJECT_NAME, + 'exported_at': timezone.now().isoformat(), + 'event_count': len(events), + 'events': [_serialize_event(event) for event in events], + } + + @login_required(login_url='login') def event_dashboard(request): if not request.user.is_staff: @@ -133,6 +159,21 @@ def event_dashboard(request): ) +@login_required(login_url='login') +def event_export(request): + if not request.user.is_staff: + raise PermissionDenied + + payload = _build_event_backup_payload() + timestamp = timezone.localtime().strftime('%Y%m%d-%H%M%S') + response = HttpResponse( + json.dumps(payload, indent=2), + content_type='application/json', + ) + response['Content-Disposition'] = f'attachment; filename="roadshow-calendar-events-{timestamp}.json"' + return response + + @login_required(login_url='login') def event_create(request): if not request.user.is_staff: