Various fixes

This commit is contained in:
2026-04-08 22:09:51 +02:00
parent 149a488795
commit 6bd9119093
13 changed files with 1613 additions and 152 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -8,146 +8,163 @@ object: {{route.event}} {% endcomment %} {% load sekizai_tags static %}
media="screen" media="screen"
/>{% endaddtoblock %} {% addtoblock "js" strip %} />{% endaddtoblock %} {% addtoblock "js" strip %}
<script <script
async
type="text/javascript" type="text/javascript"
src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAPS_API_KEY }}&amp;language=de" src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAPS_API_KEY }}&amp;language=de&amp;loading=async&amp;callback=initRouteMap"
></script> ></script>
{% endaddtoblock %} {% addtoblock "js" %} {% endaddtoblock %} {% addtoblock "js" %}
<script type="text/javascript"> <script type="text/javascript">
function OpenWindowControl(controlDiv, map) { function OpenWindowControl(controlDiv, map) {
// Set CSS styles for the DIV containing the control controlDiv.style.paddingTop = '6px';
// Setting padding to 5 px will offset the control
// from the edge of the map
controlDiv.style.paddingTop = '6px';
// Set CSS for the control border var controlUI = document.createElement('div');
var controlUI = document.createElement('div'); controlUI.style.backgroundColor = 'white';
controlUI.style.backgroundColor = 'white'; controlUI.style.borderStyle = 'solid';
controlUI.style.borderStyle = 'solid'; controlUI.style.borderWidth = '1px';
controlUI.style.borderWidth = '1px'; controlUI.style.cursor = 'pointer';
controlUI.style.cursor = 'pointer'; controlUI.style.textAlign = 'center';
controlUI.style.textAlign = 'center'; controlUI.title = 'Fenster mit Konzert Info anzeigen';
controlUI.title = 'Fenster mit Konzert Info anzeigen'; controlDiv.appendChild(controlUI);
controlDiv.appendChild(controlUI);
// Set CSS for the control interior var controlText = document.createElement('div');
var controlText = document.createElement('div'); controlText.style.fontFamily = 'Arial,sans-serif';
controlText.style.fontFamily = 'Arial,sans-serif'; controlText.style.fontSize = '12px';
controlText.style.fontSize = '12px'; controlText.style.paddingLeft = '4px';
controlText.style.paddingLeft = '4px'; controlText.style.paddingRight = '4px';
controlText.style.paddingRight = '4px'; controlText.innerHTML = 'Konzert Info anzeigen';
controlText.innerHTML = 'Konzert Info anzeigen'; controlUI.appendChild(controlText);
controlUI.appendChild(controlText);
google.maps.event.addDomListener(controlUI, 'click', function() { controlUI.addEventListener('click', function() {
$("#map_box").show(); $("#map_box").show();
}); });
} }
function ToggleTargetControl(controlDiv, map, directionsDisplay, getRouteBounds) {
controlDiv.style.paddingTop = '6px';
controlDiv.style.paddingRight = '6px';
function ShowTargetControl(controlDiv, map) { var showingLocation = false;
// Set CSS styles for the DIV containing the control {% if not route.event.map_location %}
// Setting padding to 5 px will offset the control var geocodedLocation = null;
// from the edge of the map {% endif %}
controlDiv.style.paddingTop = '6px';
controlDiv.style.paddingRight = '6px';
// Set CSS for the control border var controlUI = document.createElement('div');
var controlUI = document.createElement('div'); controlUI.style.backgroundColor = 'white';
controlUI.style.backgroundColor = 'white'; controlUI.style.borderStyle = 'solid';
controlUI.style.borderStyle = 'solid'; controlUI.style.borderWidth = '1px';
controlUI.style.borderWidth = '1px'; controlUI.style.cursor = 'pointer';
controlUI.style.cursor = 'pointer'; controlUI.style.textAlign = 'center';
controlUI.style.textAlign = 'center'; controlUI.title = 'Zwischen Route und Konzertort wechseln';
controlUI.title = 'Zum Zielpunkt springen'; controlDiv.appendChild(controlUI);
controlDiv.appendChild(controlUI);
// Set CSS for the control interior var controlText = document.createElement('div');
var controlText = document.createElement('div'); controlText.style.fontFamily = 'Arial,sans-serif';
controlText.style.fontFamily = 'Arial,sans-serif'; controlText.style.fontSize = '12px';
controlText.style.fontSize = '12px'; controlText.style.paddingLeft = '4px';
controlText.style.paddingLeft = '4px'; controlText.style.paddingRight = '4px';
controlText.style.paddingRight = '4px'; controlText.innerHTML = 'Konzertort anzeigen';
controlText.innerHTML = 'Konzertort anzeigen'; controlUI.appendChild(controlText);
controlUI.appendChild(controlText);
google.maps.event.addDomListener(controlUI, 'click', function() function goToLocation() {
{ {% if not route.event.map_location %}
{% if not route.event.map_location %} if (geocodedLocation) {
var geocoder = new google.maps.Geocoder(); map.setMapTypeId(google.maps.MapTypeId.HYBRID);
geocoder.geocode( {"address": "{{ route.event.location }}", "region": "de" }, function(results, status) { map.setZoom(15);
if (status === google.maps.GeocoderStatus.OK) { map.setCenter(geocodedLocation);
map.setMapTypeId(google.maps.MapTypeId.HYBRID); } else {
map.setZoom(15); var geocoder = new google.maps.Geocoder();
map.setCenter(results[0].geometry.location); geocoder.geocode({"address": "{{ route.event.location }}", "region": "de"}, function(results, status) {
} else { if (status === google.maps.GeocoderStatus.OK) {
console.error("Geocoding failed: " + status); geocodedLocation = results[0].geometry.location;
} map.setMapTypeId(google.maps.MapTypeId.HYBRID);
}); map.setZoom(15);
{% else %} map.setCenter(geocodedLocation);
var loc = new google.maps.LatLng({{ route.event.map_location }}); } else {
map.setMapTypeId(google.maps.MapTypeId.HYBRID); console.error("Geocoding failed: " + status);
map.setZoom(20); // revert toggle state on failure
map.setCenter(loc); showingLocation = false;
{% endif %} controlText.innerHTML = 'Konzertort anzeigen';
}); directionsDisplay.setMap(map);
}
});
}
{% else %}
var loc = new google.maps.LatLng({{ route.event.map_location }});
map.setMapTypeId(google.maps.MapTypeId.HYBRID);
map.setZoom(20);
map.setCenter(loc);
{% endif %}
}
controlUI.addEventListener('click', function() {
if (!showingLocation) {
showingLocation = true;
controlText.innerHTML = 'Route anzeigen';
directionsDisplay.setMap(null);
goToLocation();
} else {
showingLocation = false;
controlText.innerHTML = 'Konzertort anzeigen';
directionsDisplay.setMap(map);
map.setMapTypeId(google.maps.MapTypeId.ROADMAP);
var bounds = getRouteBounds();
if (bounds) {
map.fitBounds(bounds);
}
}
});
} }
$(document).ready(function() { function initRouteMap() {
var m = document.getElementById("map");
var m = $("#map")[0];
var myOptions = { var myOptions = {
zoom: 10, zoom: 10,
mapTypeId: google.maps.MapTypeId.ROAD, mapTypeId: google.maps.MapTypeId.ROADMAP,
zoomControl: false, zoomControl: false,
panControl: false,
streetViewControl: false, streetViewControl: false,
scrollwheel: false scrollwheel: false
} };
var map = new google.maps.Map(m, myOptions);
var directionsService = new google.maps.DirectionsService(); var directionsService = new google.maps.DirectionsService();
var directionsDisplay = new google.maps.DirectionsRenderer(); var directionsDisplay = new google.maps.DirectionsRenderer();
var map = new google.maps.Map(m, myOptions); directionsDisplay.setMap(map);
directionsDisplay.setMap( map ); var routeBounds = null;
var request = { var request = {
origin: "{{route.origin}}", origin: "{{route.origin}}",
destination: "{{route.destination}}", destination: "{{route.destination}}",
travelMode: google.maps.TravelMode.DRIVING travelMode: google.maps.TravelMode.DRIVING
} };
directionsService.route(request, function(response, status) { directionsService.route(request, function(response, status) {
if (status === google.maps.DirectionsStatus.OK) { if (status === google.maps.DirectionsStatus.OK) {
directionsDisplay.setDirections(response); directionsDisplay.setDirections(response);
var leg = response.routes[0].legs[0];
var leg = response.routes[0].legs[0]; $("#route_duration").html(leg.duration.text);
$("#route_distance").html(leg.distance.text);
$("#route_duration").html( leg.duration.text ); routeBounds = response.routes[0].bounds;
$("#route_distance").html( leg.distance.text ) ;
} }
}); });
var showInfoControlDiv = document.createElement('div'); var showInfoControlDiv = document.createElement('div');
var showInfoControl = new OpenWindowControl(showInfoControlDiv, map); new OpenWindowControl(showInfoControlDiv, map);
showInfoControlDiv.index = 1; showInfoControlDiv.index = 1;
map.controls[google.maps.ControlPosition.TOP_RIGHT].push( showInfoControlDiv ); map.controls[google.maps.ControlPosition.TOP_RIGHT].push(showInfoControlDiv);
var showTargetControlDiv = document.createElement('div'); var showTargetControlDiv = document.createElement('div');
var showTargetControl = new ShowTargetControl(showTargetControlDiv, map); new ToggleTargetControl(showTargetControlDiv, map, directionsDisplay, function() { return routeBounds; });
showTargetControlDiv.index = 2; showTargetControlDiv.index = 2;
map.controls[ google.maps.ControlPosition.TOP_RIGHT ].push(showTargetControlDiv); map.controls[google.maps.ControlPosition.TOP_RIGHT].push(showTargetControlDiv);
$("#map_box a").click(function() {
$("#map_box").hide();
$("#map_box a").click( function() { map.setOptions({ scrollwheel: true });
$("#map_box").hide(); });
map.setOptions( { scrollwheel: true } ); }
});
}
);
</script> </script>
{% endaddtoblock %} {% endaddtoblock %}

View File

@@ -0,0 +1,7 @@
from django.contrib import admin
from .models import GCalMapping, GCalPushChannel, UserGCalCoupling
admin.site.register(UserGCalCoupling)
admin.site.register(GCalMapping)
admin.site.register(GCalPushChannel)

View File

@@ -79,11 +79,20 @@ def create_gcal_service_object():
return None return None
def _invalidate_service_on_error(exc):
"""Reset the cached service object so the next call retries credential loading."""
global _service_object
logger.warning(f"Invalidating cached GCal service due to error: {exc}")
_service_object = None
def get_service_object(): def get_service_object():
"""Get or create the Google Calendar service object.""" """Get or create the Google Calendar service object."""
global _service_object global _service_object
if _service_object is None: if _service_object is None:
_service_object = create_gcal_service_object() _service_object = create_gcal_service_object()
if _service_object is None:
logger.error("Failed to create Google Calendar service object")
return _service_object return _service_object
@@ -93,6 +102,12 @@ def reset_service_object():
_service_object = None _service_object = None
def get_service_object_fresh():
"""Force-create a new service object, bypassing and replacing the cache."""
reset_service_object()
return get_service_object()
# --------------------- Building GCal event representation ------------------------------------ # --------------------- Building GCal event representation ------------------------------------
@@ -303,6 +318,7 @@ def delete_all_gcal_events(service=None):
batch.execute() batch.execute()
except Exception as e: except Exception as e:
logger.error(f"Error deleting GCal events: {e}") logger.error(f"Error deleting GCal events: {e}")
_invalidate_service_on_error(e)
GCalMapping.objects.all().delete() GCalMapping.objects.all().delete()
@@ -389,6 +405,7 @@ def sync_from_local_to_google(service=None):
batch.execute() batch.execute()
except Exception as e: except Exception as e:
logger.error(f"Error executing batch request: {e}") logger.error(f"Error executing batch request: {e}")
_invalidate_service_on_error(e)
return len(events_to_create_django_id), len(events_to_delete_google_id) return len(events_to_create_django_id), len(events_to_delete_google_id)
@@ -485,33 +502,39 @@ def check_gcal_subscription(
callback_url = settings.GCAL_COUPLING["push_url"] callback_url = settings.GCAL_COUPLING["push_url"]
try: channels = GCalPushChannel.objects.filter(address=callback_url)
db_channel = GCalPushChannel.objects.get(address=callback_url)
cur_time = int(time.time() * 1000) if channels.count() > 1:
logger.warning(
f"Multiple GCal channels found for {callback_url}. Stopping all and creating fresh one."
)
for ch in channels:
ch.stop(service)
channels = GCalPushChannel.objects.none()
if db_channel.expiration > cur_time: db_channel = channels.first()
# not yet expired
if cur_time + renew_before_expiry * 1000 > db_channel.expiration:
# will expire in less than "renew_before_expiry"
logger.info(f"Renewing Google Calendar Subscription: {callback_url}")
db_channel.stop(service)
GCalPushChannel.create_new(callback_url, service, time_to_live)
else:
logger.info(f"Channel active until {db_channel.expiration}")
else:
logger.info(
"Google calendar subscription had expired - getting new subscription"
)
# to get back in sync again we have to decide which data to take
# so we use the local data as reference
sync_from_local_to_google(service)
GCalPushChannel.create_new(callback_url, service, time_to_live)
except GCalPushChannel.DoesNotExist: if db_channel is None:
logger.info(f"No GCalCallback Channel exists yet for: {callback_url}") logger.info(f"No GCalCallback Channel exists yet for: {callback_url}")
# to get back in sync again we have to decide which data to take sync_from_local_to_google(service)
# so we use the local data as reference GCalPushChannel.create_new(callback_url, service, time_to_live)
return
cur_time = int(time.time() * 1000)
if db_channel.expiration > cur_time:
# not yet expired
if cur_time + renew_before_expiry * 1000 > db_channel.expiration:
# will expire in less than "renew_before_expiry"
logger.info(f"Renewing Google Calendar Subscription: {callback_url}")
db_channel.stop(service)
GCalPushChannel.create_new(callback_url, service, time_to_live)
else:
logger.info(f"Channel active until {db_channel.expiration}")
else:
logger.info(
"Google calendar subscription had expired - getting new subscription"
)
sync_from_local_to_google(service) sync_from_local_to_google(service)
GCalPushChannel.create_new(callback_url, service, time_to_live) GCalPushChannel.create_new(callback_url, service, time_to_live)

View File

@@ -1,9 +1,11 @@
from django.core.management.base import NoArgsCommand from django.core.management.base import BaseCommand
from eventplanner_gcal.google_sync import checkGCalSubscription from eventplanner_gcal.google_sync import check_gcal_subscription
class Command(NoArgsCommand):
class Command(BaseCommand):
help = 'Checks if the GCal notification channel is still active' help = 'Checks if the GCal notification channel is still active'
def handle_noargs(self, **options):
print ( "Checking Subscription") def handle(self, *args, **options):
checkGCalSubscription() self.stdout.write('Checking Subscription')
check_gcal_subscription()

View File

@@ -1,9 +1,12 @@
from django.core.management.base import NoArgsCommand from django.core.management.base import BaseCommand
from eventplanner_gcal.google_sync import deleteAllGCalEvents
class Command(NoArgsCommand): from eventplanner_gcal.google_sync import delete_all_gcal_events
class Command(BaseCommand):
help = 'Delete all events in the google calendar created by this app' help = 'Delete all events in the google calendar created by this app'
def handle_noargs(self, **options):
print ("Deleting all GCal Events.") def handle(self, *args, **options):
nrOfDeletedEvents = deleteAllGCalEvents() self.stdout.write('Deleting all GCal Events.')
print ("Deleted %d events. To Restore them from local database run gcal_sync" % (nrOfDeletedEvents, ) ) nr_deleted = delete_all_gcal_events()
self.stdout.write(f'Deleted {nr_deleted} events. To restore them run gcal_sync')

View File

@@ -1,8 +1,10 @@
from django.core.management.base import NoArgsCommand from django.core.management.base import BaseCommand
from eventplanner_gcal.google_sync import stopAllGCalSubscriptions from eventplanner_gcal.google_sync import stop_all_gcal_subscriptions
class Command(NoArgsCommand):
class Command(BaseCommand):
help = 'Stops all GCal subscriptions' help = 'Stops all GCal subscriptions'
def handle_noargs(self, **options):
stopAllGCalSubscriptions() def handle(self, *args, **options):
stop_all_gcal_subscriptions()

View File

@@ -1,10 +1,12 @@
from django.core.management.base import NoArgsCommand from django.core.management.base import BaseCommand
from eventplanner_gcal.google_sync import syncFromLocalToGoogle from eventplanner_gcal.google_sync import sync_from_local_to_google
class Command(NoArgsCommand):
class Command(BaseCommand):
help = 'Synchronize Google Calendar with locally stored Events' help = 'Synchronize Google Calendar with locally stored Events'
def handle_noargs(self, **options):
print ( "Running Sync") def handle(self, *args, **options):
created, deleted = syncFromLocalToGoogle() self.stdout.write('Running Sync')
print ( "Created %d and deleted %d events" % (created,deleted) ) created, deleted = sync_from_local_to_google()
self.stdout.write(f'Created {created} and deleted {deleted} events')

View File

@@ -4,6 +4,7 @@ Views for Google Calendar integration management.
import logging import logging
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@@ -18,12 +19,14 @@ from .models import UserGCalCoupling
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@login_required
def run_sync(request): def run_sync(request):
"""Manually trigger a sync from local to Google Calendar.""" """Manually trigger a sync from local to Google Calendar."""
sync_from_local_to_google() sync_from_local_to_google()
return redirect("/") return redirect("/")
@login_required
def manage(request): def manage(request):
""" """
View for managing Google Calendar integration settings. View for managing Google Calendar integration settings.

View File

@@ -30,7 +30,7 @@
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script>
{% endaddtoblock %} {% addtoblock "css" strip %}<link {% endaddtoblock %} {% addtoblock "css" strip %}<link
rel="stylesheet" rel="stylesheet"
href="{{STATIC_URL}}/css/jquery-ui-1.8.21.custom.css" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/themes/smoothness/jquery-ui.css"
type="text/css" type="text/css"
media="screen" media="screen"
/> />

View File

@@ -136,7 +136,7 @@ def change_password(request):
def logout_view(request): def logout_view(request):
logout(request) logout(request)
return redirect(login_view) return redirect('musicians:login')
def userlistForAutocompletion(request): def userlistForAutocompletion(request):

View File

@@ -22,3 +22,5 @@ reportlab>=4.2
# Image handling # Image handling
Pillow>=10.4 Pillow>=10.4
dotenv