From 376d533341d0a9eccb0b3355b990f7ac7ec3c090 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 2 Apr 2026 16:16:41 +0000 Subject: [PATCH] Autosave: 20260402-161641 --- core/__pycache__/urls.cpython-311.pyc | Bin 1789 -> 1900 bytes core/__pycache__/views.cpython-311.pyc | Bin 14266 -> 16406 bytes core/templates/core/event_dashboard.html | 13 +++++++ core/tests.py | 13 +++++++ core/urls.py | 2 ++ core/views.py | 41 +++++++++++++++++++++++ 6 files changed, 69 insertions(+) diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 1c56cb85ee9fbba1ed308a585f1a3441e3e1758d..f89650d22a25c626753fb85f3ab2a854b576e596 100644 GIT binary patch delta 538 zcmey%`-V?rIWI340}yw$aIQqirgCcWlRhVtAQ8-QWSui`4xehVUA>2AUOFJqq4Np z8un$(K&3zo0qML^!YRtZ44NvFC75IwxhI=4<*=nz6yz6`OrFW4&nPhYB$Kr?KTvU% zaB5js_Xx)q49(=XzkEYB<>Es>H~oRO5DSd^jG=pndyVvqN@q*~Y|1!+%DZ)S&t4JzOiYSo5ESAbOIe<};nKMNa$hDM8Wy<1*nY%y$ z#zLl3q*G+p$Sz}IU|0>r5Rf7V)XXmr)C_YA!vcZHKNyuKUuG0kPv?meN>L1E&{TQ} zQm)B(i?u8>wY(U}U@1r}$#}^K66Km4%#<_vER#AT|KzVs){K0U4Vfhwc_w=^%S>)# zc3|Y4e4Kd?qwwSwmaNI2S#r4)fSN!Wi*HZPV%4kXW@P%nKz~Apft9ntr9)`4B3td` U&1|dq6$Sa38n{8QNDk;a0I*qKtpET3 diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index f3bcfc60611458e431efd0239b8358d214a8f317..b5887f3b630a102ef1a94166b7f378f4093030f7 100644 GIT binary patch delta 4396 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 delta 2735 zcmai0Yitx%6rQ`!w%dKO-AAF_EiGlIkL?2}g_c(|Bm#jTqGS@gU3R8)b!NBRnF3a& zK!_5N=S6%4(1g@fFcLQqW7Ho6h(zOGb)#T3YWQRDFZ#oC?(Fu3#7+9`x#ygF?s;#| z9w49PJs-H;RRVl2v2U}vrc<7X6b`;cZM*KB{-==_INo61I66KSYiGv0N5fg1ZjhUQNzp2&Ul5(t!*1I8NPrRh4toqF!NP#O zpKPfC@}AAqw1K8h@tSC%weUq9rog4;bH;E=H`3J5F(q+Y4_I`ia>_m<#ievHaANG9 zTl_Xdo^kbAK)}7j! z)|FHS(hdox6qFR^4EKP}>hQ5HbO$Qyq{{A5MW0m?Z%Wa2DzbThl&W+e=;qiz;RM`m zcShOj$g}>Zfk*cMluN-bM*zf5HI^vZTgd; zO4?B9yC6D(I=9)CWP}YZz6yRUNc=z?Cx8o&{4KUSjw5-JC0ovoA3}1tpPI>i6DiYsq~1b!8v!<*K;J?5kANt4wP?ermX(4XL{)S{$MJ{QO3AJ= zgHzRTBt9B*4sYR&LKJm!w?P3v)4Q58dxlk_P2v%SNN za}dO$`+veg=ZX&V`rJ6CYr?&s;e4^=X)diPb7H^(d9h=-M-)T2&MqadnclM(J0{2p zwy?8#$NT7B#iQMudYvb5veXv5Sq9kO&e4x*=E9k#(Y zO(K{RhEy;6rh5`vcF&Uj@kv4SP+Sc3WduI0ParilU34QJJpe^(F2C2DGo1A47N!~; zMLX}~qT5{H<$dxYYFPe6sJ0E~HHBUQ?jHhMyYx56+WknBtyy+Dh9QiF zGkL0XWYQ|^%rs5mCP)q&no19``^##!egr!3@SFgqGFhJTk3ntLl)GnHqy>;aK%Hk; zv^T-__bdTF?leZ(*F9IrcJ|Wp9!OVVdBa-%p5u}4QNGtcne!SofWg7Rg1sZac+o?VI?_;$9LRbE~ik4NXNyUz_t#xl7foZqQFb7-ZkA$nZuy@8FHp z#_q0M(9b*3K4Z|QP2zaa3A8(N3(+oQBgEPAzO63&y%fo2v5jtJtgnfE-q$xClAlZG zcIA^;u%xmtql_5Yd`>r4p|2#=hx63Pj2gQ6INu)lc+pi-fS(ba{{$AR%N!#MEZ%?i zp*+=ezL8+L=3(M=GegjS;{wg0AuEJVr+<7~fM=GABE|cb7wIS-ah>wT!^hSfD(pqx z5N6phjb`{*dy0|se@Grw^n(YZs +
+
+
+ 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: