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 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="col-xl-5">
<div class="card-surface embed-settings-card h-100">

View File

@ -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]))

View File

@ -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/<slug:slug>/', 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/<slug:slug>/edit/', event_edit, name='event_edit'),
path('dashboard/events/<slug:slug>/delete/', event_delete, name='event_delete'),

View File

@ -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: