more fixes, AI

This commit is contained in:
2026-03-31 22:10:30 +02:00
parent de1fa76e40
commit 2beb7aa75a
14 changed files with 121 additions and 67 deletions

View File

@@ -145,7 +145,7 @@ INSTALLED_APPS = [
"musicians",
"eventplanner",
"eventplanner_gcal",
"simpleforum",
# "simpleforum", # Disabled
"location_field",
"scoremanager",
# 'imagestore', # Disabled

View File

@@ -3,14 +3,12 @@ from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path, re_path
from simpleforum import views as simpleforum_views
urlpatterns = [
path("", include("website.urls")),
path("events/", include("eventplanner.urls")),
path("musicians/", include("musicians.urls")),
path("scores/", include("scoremanager.urls")),
path("messages/", simpleforum_views.message_view),
# path("messages/", simpleforum_views.message_view), # Disabled
path("admin/", admin.site.urls),
path("location_field/", include("location_field.urls")),
path("eventplanner_gcal/", include("eventplanner_gcal.urls")),

View File

@@ -1,12 +1,12 @@
{% load sekizai_tags static %} {% addtoblock "css" strip %}
<link rel="stylesheet" href="{{STATIC_URL}}/css/bootstrap.min.css" />{% endaddtoblock %} {% addtoblock "css" strip %}
<link rel="stylesheet" href="{{STATIC_URL}}/css/bootstrap-responsive.min.css" />
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" />{% endaddtoblock %} {% addtoblock "css" strip %}
<link rel="stylesheet" href="{% static 'css/bootstrap-responsive.min.css' %}" />
{% endaddtoblock %} {% addtoblock "css" strip %}
<link rel="stylesheet" href="{{STATIC_URL}}/css/bootstrap-overrides.css" />{% endaddtoblock %} {% addtoblock "css" strip %}
<link rel="stylesheet" href="{{STATIC_URL}}/css/theme.css" type="text/css" />{% endaddtoblock %} {% addtoblock "css" strip %}
<link rel="stylesheet" href="{% static 'css/bootstrap-overrides.css' %}"/>{% endaddtoblock %} {% addtoblock "css" strip %}
<link rel="stylesheet" href="{% static 'css/theme.css' %}" type="text/css" />{% endaddtoblock %} {% addtoblock "css" strip %}
<link
rel="stylesheet"
href="{{STATIC_URL}}/css/index.css"
href="{% static 'css/index.css' %}"
type="text/css"
media="screen"
/>
@@ -20,18 +20,16 @@
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
{% endaddtoblock %} {% addtoblock "js" strip %}
<script src="{{STATIC_URL}}/js/jquery-2.0.3.min.js"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
{% endaddtoblock %} {% addtoblock "js" strip %}
<script src="{{STATIC_URL}}/js/bootstrap.min.js"></script>
<script src="{% static 'js/jquery.countdown.min.js' %}"></script>
{% endaddtoblock %} {% addtoblock "js" strip %}
<script src="{{STATIC_URL}}/js/jquery.countdown.min.js"></script>
<script src="{% static 'js/theme.js' %}"></script>
{% endaddtoblock %} {% addtoblock "js" strip %}
<script src="{{STATIC_URL}}/js/theme.js"></script>
{% endaddtoblock %} {% addtoblock "js" strip %}
<script src="{{STATIC_URL}}/js/index-slider.js" type="text/javascript"></script>
<script src="{% static 'js/index-slider.js' %}" type="text/javascript"></script>
{% endaddtoblock %} {% addtoblock "js" strip %}
<script
src="{{STATIC_URL}}/js/bindWithDelay.js"
src="{% static 'js/bindWithDelay.js' %}"
type="text/javascript"
></script>
{% endaddtoblock %} {% addtoblock "js" %}

View File

@@ -159,13 +159,18 @@ class EventParticipation(models.Model):
return self.user.username
def save(self, *args, **kwargs):
prev = EventParticipation.objects.filter(event=self.event, user=self.user)
if len(prev) == 0:
# For new objects, just save directly
if self.pk is None:
super().save(*args, **kwargs)
else:
prev = prev[0]
return
# For existing objects, only save if values changed
try:
prev = EventParticipation.objects.get(pk=self.pk)
if prev.status != self.status or prev.comment != self.comment:
super().save(*args, **kwargs)
except EventParticipation.DoesNotExist:
super().save(*args, **kwargs)
@staticmethod
def hasUserSetParticipationForAllEvents(user):

View File

@@ -1,21 +1,48 @@
from django.contrib.auth.models import User
from rest_framework import serializers
from .models import Event, EventParticipation
class ParticipationSerializer(serializers.ModelSerializer):
event = serializers.PrimaryKeyRelatedField(queryset=Event.objects.all())
user = serializers.CharField(source="get_username", read_only=True)
status = serializers.CharField(required=False)
class ParticipationSerializer(serializers.Serializer):
"""Serializer for EventParticipation that handles username lookup."""
class Meta:
model = EventParticipation
fields = ("event", "user", "status", "comment")
event = serializers.PrimaryKeyRelatedField(queryset=Event.objects.all())
user = serializers.CharField()
status = serializers.CharField(required=False, default="-")
comment = serializers.CharField(required=False, allow_blank=True, default="")
def to_representation(self, instance):
"""Serialize an EventParticipation instance."""
return {
"event": instance.event.pk,
"user": instance.user.username,
"status": instance.status,
"comment": instance.comment,
}
def validate_user(self, value):
"""Look up user by username (case-insensitive)."""
try:
return User.objects.get(username__iexact=value)
except User.DoesNotExist:
raise serializers.ValidationError(f"User '{value}' does not exist")
def create(self, validated_data):
# Remove the get_username source field as it's read-only
validated_data.pop("get_username", None)
return super().create(validated_data)
"""Create or update EventParticipation based on event and user."""
event = validated_data.get("event")
user = validated_data.get("user")
status = validated_data.get("status", "-")
comment = validated_data.get("comment", "")
# Use update_or_create to handle both new and existing participations
participation, created = EventParticipation.objects.update_or_create(
event=event,
user=user,
defaults={"status": status, "comment": comment},
)
return participation
def update(self, instance, validated_data):
instance.status = validated_data.get("status", instance.status)

View File

@@ -14,26 +14,26 @@
{% addtoblock "css" strip %}<link
rel="stylesheet"
href="{{STATIC_URL}}/css/datepicker.css"
href="{% static 'css/datepicker.css' %}"
type="text/css"
media="screen"
/>
{% endaddtoblock %} {% addtoblock "css" strip %}<link
rel="stylesheet"
href="{{STATIC_URL}}/css/timepicker.css"
href="{% static 'css/timepicker.css' %}"
type="text/css"
media="screen"
/>
{% endaddtoblock %} {% addtoblock "css" strip %}<link
rel="stylesheet"
href="{{STATIC_URL}}/css/jquery-ui-1.8.21.custom.css"
href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/themes/smoothness/jquery-ui.css"
type="text/css"
media="screen"
/>
{% endaddtoblock %} {% addtoblock "js" %}
<script src="{{STATIC_URL}}/js/bootstrap-timepicker.js"></script>
<script src="{{STATIC_URL}}/js/bootstrap-datepicker.js"></script>
<script src="{{STATIC_URL}}/js/bootstrap-datepicker.de.js"></script>
<script src="{% static 'js/bootstrap-timepicker.js' %}"></script>
<script src="{% static 'js/bootstrap-datepicker.js' %}"></script>
<script src="{% static 'js/bootstrap-datepicker.de.js' %}"></script>
<script>

View File

@@ -3,7 +3,7 @@ Coordinates or textual adresses: {{route.origin}} {{route.destination}} Event
object: {{route.event}} {% endcomment %} {% load sekizai_tags static %}
{% if route %} {% addtoblock "css" strip %}<link
rel="stylesheet"
href="{{STATIC_URL}}/css/concert_route.css"
href="{% static 'css/concert_route.css' %}"
type="text/css"
media="screen"
/>{% endaddtoblock %} {% addtoblock "js" strip %}
@@ -74,23 +74,21 @@ object: {{route.event}} {% endcomment %} {% load sekizai_tags static %}
google.maps.event.addDomListener(controlUI, 'click', function()
{
{% if not route.event.map_location %}
geocoder = new google.maps.Geocoder();
geocoder.region = "de";
geocoder.geocode( {"address": "{{ route.event.location }}" }, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
var geocoder = new google.maps.Geocoder();
geocoder.geocode( {"address": "{{ route.event.location }}", "region": "de" }, function(results, status) {
if (status === google.maps.GeocoderStatus.OK) {
map.setMapTypeId(google.maps.MapTypeId.HYBRID);
map.setZoom(15);
map.setCenter(results[0].geometry.location);
} else {
console.error("Geocoding failed: " + status);
}
});
{% else %}
var loc = new google.maps.LatLng({{ route.event.map_location }});
map.setMapTypeId(google.maps.MapTypeId.HYBRID);
map.setZoom(20);
map.setCenter(loc);
{% endif %}
});
}
@@ -116,11 +114,11 @@ object: {{route.event}} {% endcomment %} {% load sekizai_tags static %}
var request = {
origin: "{{route.origin}}",
destination: "{{route.destination}}",
travelMode: google.maps.DirectionsTravelMode.DRIVING
travelMode: google.maps.TravelMode.DRIVING
}
directionsService.route(request, function(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
if (status === google.maps.DirectionsStatus.OK) {
directionsDisplay.setDirections(response);
var leg = response.routes[0].legs[0];

View File

@@ -7,6 +7,7 @@ app_name = "eventplanner"
urlpatterns = [
path("", views.eventplanning, name="eventplanning"),
path("grid/", views.events_grid, name="events_grid"),
path("grid/add", views.EventCreate.as_view(), name="event_create_from_grid"),
path("delete/<int:pk>/", views.deleteEvent, name="delete_event"),
# Event detail/update views - support both URL patterns
path("<int:pk>/", views.EventUpdate.as_view(), name="event_detail"),

View File

@@ -46,14 +46,29 @@ def event_api(request, username=None, eventId=None):
EventParticipation.isMember(request.user)
or EventParticipation.isAdmin(request.user)
):
return Response(status=status.HTTP_403_FORBIDDEN)
if user_obj != request.user:
return Response(
{"error": "Permission denied - not a member"},
status=status.HTTP_403_FORBIDDEN,
)
# Allow users to update their own participation
# Admins can update anyone's participation
if user_obj.pk != request.user.pk:
if not EventParticipation.isAdmin(request.user):
return Response(status=status.HTTP_403_FORBIDDEN)
return Response(
{"error": "Permission denied - can only update own status"},
status=status.HTTP_403_FORBIDDEN,
)
serializer.save()
return Response(serializer.data)
instances = serializer.save()
# Re-serialize the saved instances to return proper data
response_serializer = ParticipationSerializer(instances, many=True)
return Response(response_serializer.data)
else:
import logging
logger = logging.getLogger(__name__)
logger.error(f"API validation errors: {serializer.errors}")
logger.error(f"Request data: {request.data}")
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -1,4 +1,9 @@
/* Hide map dialog initially - jQuery UI dialog will show it */
.map_dialog {
display: none;
}
input.locationwidget
{
cursor: pointer !important;

View File

@@ -1,4 +1,4 @@
<div id="map_dialog_%(name)s" class="map_dialog" title="Genauen Ort bestimmen">
<div id="map_dialog_%(name)s" class="map_dialog" title="Genauen Ort bestimmen" style="display: none;">
<div style="margin: 4px 0 0 0">
<label></label>

View File

@@ -61,14 +61,17 @@ class LocationWidget(widgets.TextInput):
maps_url = f"https://maps.googleapis.com/maps/api/js?key={api_key}&language=de"
return widgets.Media(
css={"all": ("location_field/form.css",)},
css={
"all": (
"https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/themes/smoothness/jquery-ui.css",
"location_field/form.css",
)
},
js=(
# jQuery and jQuery UI
"https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js",
# jQuery UI from CDN (jQuery is already loaded in base template)
"https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js",
# Google Maps API with API key
maps_url,
"js/bindWithDelay.js",
"location_field/form.js",
)
)

View File

@@ -17,6 +17,9 @@ from .models import Musician
class MusicianList(ListView):
model = Musician
def get_queryset(self):
return Musician.objects.filter(user__is_active=True)
class UserEditForm(forms.ModelForm):
email = forms.EmailField()
@@ -95,7 +98,7 @@ class MusicianUpdate(UpdateView):
def addressbook(request):
context = {}
context["musicians"] = Musician.objects.all().order_by("user__first_name")
context["musicians"] = Musician.objects.filter(user__is_active=True).order_by("user__first_name")
return render(request, "musicians/addressbook.html", context)

View File

@@ -5,10 +5,12 @@
<!DOCTYPE html>
<html>
<head>
<title>Blechreiz</title></title>
<title>Blechreiz</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% render_block "css" %}
<!-- jQuery must load in head before any form widget media -->
<script src="{% static 'js/jquery-2.0.3.min.js' %}"></script>
</head>
<body class="pull_top">
@@ -33,7 +35,6 @@
{% block menu_contents %}
<li><a href="/">HOME</a></li>
<li><a href="/events/">Termine</a></li>
<li><a href="/messages/">Forum</a></li>
<li><a href="/musicians/">Adressbuch</a></li>
<!-- <li><a href="/scores/">Noten</a></li> -->
{% endblock %}