Autosave: 20260402-161641
This commit is contained in:
parent
25c22e7107
commit
376d533341
Binary file not shown.
Binary file not shown.
@ -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">
|
||||
|
||||
@ -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]))
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user