Autosave: 20260402-161641

This commit is contained in:
Flatlogic Bot 2026-04-02 16:16:41 +00:00
parent 25c22e7107
commit 376d533341
6 changed files with 69 additions and 0 deletions

View File

@ -17,6 +17,19 @@
</div> </div>
</div> </div>
<div class="card-surface mb-4">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 align-items-lg-center">
<div>
<span class="eyebrow-pill">Backup</span>
<h2 class="h4 mt-3 mb-2">Download a full event backup</h2>
<p class="section-copy mb-0">Export every event into one JSON file so you can keep an offline backup before larger edits or restore the calendar later if needed.</p>
</div>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-primary-brand" href="{% url 'event_export' %}">Download backup JSON</a>
</div>
</div>
</div>
<div class="row g-4 mb-4"> <div class="row g-4 mb-4">
<div class="col-xl-5"> <div class="col-xl-5">
<div class="card-surface embed-settings-card h-100"> <div class="card-surface embed-settings-card h-100">

View File

@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
import json
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import Client, TestCase from django.test import Client, TestCase
@ -64,6 +65,18 @@ class EventDashboardMutationTests(TestCase):
is_published=True, 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): def test_staff_can_open_edit_page(self):
self.client.login(username='staffer', password='pass12345') self.client.login(username='staffer', password='pass12345')
response = self.client.get(reverse('event_edit', args=[self.event.slug])) response = self.client.get(reverse('event_edit', args=[self.event.slug]))

View File

@ -8,6 +8,7 @@ from .views import (
event_dashboard, event_dashboard,
event_dashboard_detail, event_dashboard_detail,
event_delete, event_delete,
event_export,
event_detail, event_detail,
event_edit, event_edit,
event_list, event_list,
@ -21,6 +22,7 @@ urlpatterns = [
path('events/', event_list, name='event_list'), path('events/', event_list, name='event_list'),
path('events/<slug:slug>/', event_detail, name='event_detail'), path('events/<slug:slug>/', event_detail, name='event_detail'),
path('dashboard/events/', event_dashboard, name='event_dashboard'), 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/new/', event_create, name='event_create'),
path('dashboard/events/<slug:slug>/edit/', event_edit, name='event_edit'), path('dashboard/events/<slug:slug>/edit/', event_edit, name='event_edit'),
path('dashboard/events/<slug:slug>/delete/', event_delete, name='event_delete'), path('dashboard/events/<slug:slug>/delete/', event_delete, name='event_delete'),

View File

@ -1,9 +1,11 @@
import calendar import calendar
from collections import defaultdict from collections import defaultdict
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
import json
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
@ -115,6 +117,30 @@ def _show_embed_header(request):
return request.GET.get('header', '1') != '0' 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') @login_required(login_url='login')
def event_dashboard(request): def event_dashboard(request):
if not request.user.is_staff: 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') @login_required(login_url='login')
def event_create(request): def event_create(request):
if not request.user.is_staff: if not request.user.is_staff: