more fixes, AI
This commit is contained in:
@@ -145,7 +145,7 @@ INSTALLED_APPS = [
|
|||||||
"musicians",
|
"musicians",
|
||||||
"eventplanner",
|
"eventplanner",
|
||||||
"eventplanner_gcal",
|
"eventplanner_gcal",
|
||||||
"simpleforum",
|
# "simpleforum", # Disabled
|
||||||
"location_field",
|
"location_field",
|
||||||
"scoremanager",
|
"scoremanager",
|
||||||
# 'imagestore', # Disabled
|
# 'imagestore', # Disabled
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ from django.conf.urls.static import static
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
|
|
||||||
from simpleforum import views as simpleforum_views
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", include("website.urls")),
|
path("", include("website.urls")),
|
||||||
path("events/", include("eventplanner.urls")),
|
path("events/", include("eventplanner.urls")),
|
||||||
path("musicians/", include("musicians.urls")),
|
path("musicians/", include("musicians.urls")),
|
||||||
path("scores/", include("scoremanager.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("admin/", admin.site.urls),
|
||||||
path("location_field/", include("location_field.urls")),
|
path("location_field/", include("location_field.urls")),
|
||||||
path("eventplanner_gcal/", include("eventplanner_gcal.urls")),
|
path("eventplanner_gcal/", include("eventplanner_gcal.urls")),
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{% load sekizai_tags static %} {% addtoblock "css" strip %}
|
{% 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 '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-responsive.min.css' %}" />
|
||||||
{% endaddtoblock %} {% addtoblock "css" strip %}
|
{% endaddtoblock %} {% addtoblock "css" strip %}
|
||||||
<link rel="stylesheet" href="{{STATIC_URL}}/css/bootstrap-overrides.css" />{% endaddtoblock %} {% addtoblock "css" strip %}
|
<link rel="stylesheet" href="{% static '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/theme.css' %}" type="text/css" />{% endaddtoblock %} {% addtoblock "css" strip %}
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="{{STATIC_URL}}/css/index.css"
|
href="{% static 'css/index.css' %}"
|
||||||
type="text/css"
|
type="text/css"
|
||||||
media="screen"
|
media="screen"
|
||||||
/>
|
/>
|
||||||
@@ -20,18 +20,16 @@
|
|||||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
{% endaddtoblock %} {% addtoblock "js" strip %}
|
{% 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 %}
|
{% 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 %}
|
{% 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 %}
|
{% endaddtoblock %} {% addtoblock "js" strip %}
|
||||||
<script src="{{STATIC_URL}}/js/theme.js"></script>
|
<script src="{% static 'js/index-slider.js' %}" type="text/javascript"></script>
|
||||||
{% endaddtoblock %} {% addtoblock "js" strip %}
|
|
||||||
<script src="{{STATIC_URL}}/js/index-slider.js" type="text/javascript"></script>
|
|
||||||
{% endaddtoblock %} {% addtoblock "js" strip %}
|
{% endaddtoblock %} {% addtoblock "js" strip %}
|
||||||
<script
|
<script
|
||||||
src="{{STATIC_URL}}/js/bindWithDelay.js"
|
src="{% static 'js/bindWithDelay.js' %}"
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
></script>
|
></script>
|
||||||
{% endaddtoblock %} {% addtoblock "js" %}
|
{% endaddtoblock %} {% addtoblock "js" %}
|
||||||
|
|||||||
@@ -159,13 +159,18 @@ class EventParticipation(models.Model):
|
|||||||
return self.user.username
|
return self.user.username
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
prev = EventParticipation.objects.filter(event=self.event, user=self.user)
|
# For new objects, just save directly
|
||||||
if len(prev) == 0:
|
if self.pk is None:
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
else:
|
return
|
||||||
prev = prev[0]
|
|
||||||
|
# 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:
|
if prev.status != self.status or prev.comment != self.comment:
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
except EventParticipation.DoesNotExist:
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def hasUserSetParticipationForAllEvents(user):
|
def hasUserSetParticipationForAllEvents(user):
|
||||||
|
|||||||
@@ -1,21 +1,48 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Event, EventParticipation
|
from .models import Event, EventParticipation
|
||||||
|
|
||||||
|
|
||||||
class ParticipationSerializer(serializers.ModelSerializer):
|
class ParticipationSerializer(serializers.Serializer):
|
||||||
event = serializers.PrimaryKeyRelatedField(queryset=Event.objects.all())
|
"""Serializer for EventParticipation that handles username lookup."""
|
||||||
user = serializers.CharField(source="get_username", read_only=True)
|
|
||||||
status = serializers.CharField(required=False)
|
|
||||||
|
|
||||||
class Meta:
|
event = serializers.PrimaryKeyRelatedField(queryset=Event.objects.all())
|
||||||
model = EventParticipation
|
user = serializers.CharField()
|
||||||
fields = ("event", "user", "status", "comment")
|
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):
|
def create(self, validated_data):
|
||||||
# Remove the get_username source field as it's read-only
|
"""Create or update EventParticipation based on event and user."""
|
||||||
validated_data.pop("get_username", None)
|
event = validated_data.get("event")
|
||||||
return super().create(validated_data)
|
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):
|
def update(self, instance, validated_data):
|
||||||
instance.status = validated_data.get("status", instance.status)
|
instance.status = validated_data.get("status", instance.status)
|
||||||
|
|||||||
@@ -14,26 +14,26 @@
|
|||||||
|
|
||||||
{% addtoblock "css" strip %}<link
|
{% addtoblock "css" strip %}<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="{{STATIC_URL}}/css/datepicker.css"
|
href="{% static 'css/datepicker.css' %}"
|
||||||
type="text/css"
|
type="text/css"
|
||||||
media="screen"
|
media="screen"
|
||||||
/>
|
/>
|
||||||
{% endaddtoblock %} {% addtoblock "css" strip %}<link
|
{% endaddtoblock %} {% addtoblock "css" strip %}<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="{{STATIC_URL}}/css/timepicker.css"
|
href="{% static 'css/timepicker.css' %}"
|
||||||
type="text/css"
|
type="text/css"
|
||||||
media="screen"
|
media="screen"
|
||||||
/>
|
/>
|
||||||
{% 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="https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/themes/smoothness/jquery-ui.css"
|
||||||
type="text/css"
|
type="text/css"
|
||||||
media="screen"
|
media="screen"
|
||||||
/>
|
/>
|
||||||
{% endaddtoblock %} {% addtoblock "js" %}
|
{% endaddtoblock %} {% addtoblock "js" %}
|
||||||
<script src="{{STATIC_URL}}/js/bootstrap-timepicker.js"></script>
|
<script src="{% static 'js/bootstrap-timepicker.js' %}"></script>
|
||||||
<script src="{{STATIC_URL}}/js/bootstrap-datepicker.js"></script>
|
<script src="{% static 'js/bootstrap-datepicker.js' %}"></script>
|
||||||
<script src="{{STATIC_URL}}/js/bootstrap-datepicker.de.js"></script>
|
<script src="{% static 'js/bootstrap-datepicker.de.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Coordinates or textual adresses: {{route.origin}} {{route.destination}} Event
|
|||||||
object: {{route.event}} {% endcomment %} {% load sekizai_tags static %}
|
object: {{route.event}} {% endcomment %} {% load sekizai_tags static %}
|
||||||
{% if route %} {% addtoblock "css" strip %}<link
|
{% if route %} {% addtoblock "css" strip %}<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="{{STATIC_URL}}/css/concert_route.css"
|
href="{% static 'css/concert_route.css' %}"
|
||||||
type="text/css"
|
type="text/css"
|
||||||
media="screen"
|
media="screen"
|
||||||
/>{% endaddtoblock %} {% addtoblock "js" strip %}
|
/>{% endaddtoblock %} {% addtoblock "js" strip %}
|
||||||
@@ -74,23 +74,21 @@ object: {{route.event}} {% endcomment %} {% load sekizai_tags static %}
|
|||||||
google.maps.event.addDomListener(controlUI, 'click', function()
|
google.maps.event.addDomListener(controlUI, 'click', function()
|
||||||
{
|
{
|
||||||
{% if not route.event.map_location %}
|
{% if not route.event.map_location %}
|
||||||
geocoder = new google.maps.Geocoder();
|
var geocoder = new google.maps.Geocoder();
|
||||||
geocoder.region = "de";
|
geocoder.geocode( {"address": "{{ route.event.location }}", "region": "de" }, function(results, status) {
|
||||||
geocoder.geocode( {"address": "{{ route.event.location }}" }, function(results, status) {
|
if (status === google.maps.GeocoderStatus.OK) {
|
||||||
if (status == google.maps.GeocoderStatus.OK) {
|
|
||||||
map.setMapTypeId(google.maps.MapTypeId.HYBRID);
|
map.setMapTypeId(google.maps.MapTypeId.HYBRID);
|
||||||
map.setZoom(15);
|
map.setZoom(15);
|
||||||
|
|
||||||
map.setCenter(results[0].geometry.location);
|
map.setCenter(results[0].geometry.location);
|
||||||
|
} else {
|
||||||
|
console.error("Geocoding failed: " + status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
{% else %}
|
{% else %}
|
||||||
var loc = new google.maps.LatLng({{ route.event.map_location }});
|
var loc = new google.maps.LatLng({{ route.event.map_location }});
|
||||||
map.setMapTypeId(google.maps.MapTypeId.HYBRID);
|
map.setMapTypeId(google.maps.MapTypeId.HYBRID);
|
||||||
map.setZoom(20);
|
map.setZoom(20);
|
||||||
|
|
||||||
map.setCenter(loc);
|
map.setCenter(loc);
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -116,11 +114,11 @@ object: {{route.event}} {% endcomment %} {% load sekizai_tags static %}
|
|||||||
var request = {
|
var request = {
|
||||||
origin: "{{route.origin}}",
|
origin: "{{route.origin}}",
|
||||||
destination: "{{route.destination}}",
|
destination: "{{route.destination}}",
|
||||||
travelMode: google.maps.DirectionsTravelMode.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];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ app_name = "eventplanner"
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.eventplanning, name="eventplanning"),
|
path("", views.eventplanning, name="eventplanning"),
|
||||||
path("grid/", views.events_grid, name="events_grid"),
|
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"),
|
path("delete/<int:pk>/", views.deleteEvent, name="delete_event"),
|
||||||
# Event detail/update views - support both URL patterns
|
# Event detail/update views - support both URL patterns
|
||||||
path("<int:pk>/", views.EventUpdate.as_view(), name="event_detail"),
|
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)
|
EventParticipation.isMember(request.user)
|
||||||
or EventParticipation.isAdmin(request.user)
|
or EventParticipation.isAdmin(request.user)
|
||||||
):
|
):
|
||||||
return Response(status=status.HTTP_403_FORBIDDEN)
|
return Response(
|
||||||
if user_obj != request.user:
|
{"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):
|
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()
|
instances = serializer.save()
|
||||||
return Response(serializer.data)
|
# Re-serialize the saved instances to return proper data
|
||||||
|
response_serializer = ParticipationSerializer(instances, many=True)
|
||||||
|
return Response(response_serializer.data)
|
||||||
else:
|
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)
|
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
|
input.locationwidget
|
||||||
{
|
{
|
||||||
cursor: pointer !important;
|
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">
|
<div style="margin: 4px 0 0 0">
|
||||||
<label></label>
|
<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"
|
maps_url = f"https://maps.googleapis.com/maps/api/js?key={api_key}&language=de"
|
||||||
|
|
||||||
return widgets.Media(
|
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=(
|
js=(
|
||||||
# jQuery and jQuery UI
|
# jQuery UI from CDN (jQuery is already loaded in base template)
|
||||||
"https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js",
|
|
||||||
"https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js",
|
"https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js",
|
||||||
# Google Maps API with API key
|
# Google Maps API with API key
|
||||||
maps_url,
|
maps_url,
|
||||||
"js/bindWithDelay.js",
|
|
||||||
"location_field/form.js",
|
"location_field/form.js",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ from .models import Musician
|
|||||||
class MusicianList(ListView):
|
class MusicianList(ListView):
|
||||||
model = Musician
|
model = Musician
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Musician.objects.filter(user__is_active=True)
|
||||||
|
|
||||||
|
|
||||||
class UserEditForm(forms.ModelForm):
|
class UserEditForm(forms.ModelForm):
|
||||||
email = forms.EmailField()
|
email = forms.EmailField()
|
||||||
@@ -95,7 +98,7 @@ class MusicianUpdate(UpdateView):
|
|||||||
|
|
||||||
def addressbook(request):
|
def addressbook(request):
|
||||||
context = {}
|
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)
|
return render(request, "musicians/addressbook.html", context)
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Blechreiz</title></title>
|
<title>Blechreiz</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{% render_block "css" %}
|
{% 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>
|
</head>
|
||||||
|
|
||||||
<body class="pull_top">
|
<body class="pull_top">
|
||||||
@@ -33,7 +35,6 @@
|
|||||||
{% block menu_contents %}
|
{% block menu_contents %}
|
||||||
<li><a href="/">HOME</a></li>
|
<li><a href="/">HOME</a></li>
|
||||||
<li><a href="/events/">Termine</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="/musicians/">Adressbuch</a></li>
|
||||||
<!-- <li><a href="/scores/">Noten</a></li> -->
|
<!-- <li><a href="/scores/">Noten</a></li> -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user