more fixes, AI
This commit is contained in:
@@ -145,7 +145,7 @@ INSTALLED_APPS = [
|
||||
"musicians",
|
||||
"eventplanner",
|
||||
"eventplanner_gcal",
|
||||
"simpleforum",
|
||||
# "simpleforum", # Disabled
|
||||
"location_field",
|
||||
"scoremanager",
|
||||
# 'imagestore', # Disabled
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
|
||||
/* Hide map dialog initially - jQuery UI dialog will show it */
|
||||
.map_dialog {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input.locationwidget
|
||||
{
|
||||
cursor: pointer !important;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user