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>
|
</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">
|
||||||
|
|||||||
@ -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]))
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user