This commit is contained in:
2025-10-02 00:54:34 +02:00
commit 84b34c9615
200 changed files with 42048 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
from rest_framework import viewsets
from rest_framework.exceptions import ValidationError
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
from rest_framework.views import APIView
from rest_framework.response import Response
from django.utils import timezone
from django.apps import apps as django_apps
from .models import AppConfig
from .serializers import AppConfigSerializer, TrashSerializer, AppConfigPublicSerializer
from account.permissions import OnlyRolesAllowed
@extend_schema(
tags=["AppConfig"],
description=(
"Globální konfigurace aplikace správa bankovního účtu, e-mailu odesílatele a dalších nastavení. "
"Umožňuje úpravu přes administrační rozhraní nebo API.\n\n"
"🛠️ **Singleton model** lze vytvořit pouze jednu instanci konfigurace.\n\n"
"📌 **Přístup pouze pro administrátory** (`role=admin`).\n\n"
"**Dostupné akce:**\n"
"- `GET /api/config/` Získání aktuální konfigurace (singleton)\n"
"- `PUT /api/config/` Úprava konfigurace\n\n"
"**Poznámka:** pokus o vytvoření více než jedné konfigurace vrací chybu 400."
)
)
class AppConfigViewSet(viewsets.ModelViewSet):
queryset = AppConfig.objects.all()
serializer_class = AppConfigSerializer
permission_classes = [OnlyRolesAllowed("admin")]
def get_object(self):
# Always return the singleton instance
return AppConfig.get_instance()
def perform_update(self, serializer):
serializer.save(last_changed_by=self.request.user)
def perform_create(self, serializer):
if AppConfig.objects.exists():
raise ValidationError("Only one AppConfig instance allowed.")
serializer.save(last_changed_by=self.request.user)
class AppConfigPublicView(APIView):
"""Read-only public endpoint with limited AppConfig data (logo, background, contact info).
Returns 404 if no configuration exists yet.
"""
authentication_classes = [] # allow anonymous
permission_classes = []
ALLOWED_FIELDS = {
"id",
"logo",
"background_image",
"contact_email",
"contact_phone",
"max_reservations_per_event",
}
def get(self, request):
cfg = AppConfig.get_instance()
if not cfg:
return Response({"detail": "Not configured"}, status=404)
fields_param = request.query_params.get("fields")
if fields_param:
requested = {f.strip() for f in fields_param.split(",") if f.strip()}
valid = [f for f in requested if f in self.ALLOWED_FIELDS]
if not valid:
return Response({
"detail": "No valid fields requested. Allowed: " + ", ".join(sorted(self.ALLOWED_FIELDS))
}, status=400)
data = {}
for f in valid:
data[f] = getattr(cfg, f, None)
return Response(data)
# default full public subset
return Response(AppConfigPublicSerializer(cfg).data)
@extend_schema(
tags=["Trash"],
description=(
"Agregovaný seznam všech soft-smazaných (is_deleted=True) objektů napříč aplikacemi definovanými v `settings.MY_CREATED_APPS`.\n\n"
"Pagination params:\n"
"- `page` (int, default=1)\n"
"- `page_size` nebo `limit` (int, default=20, max=200)\n\n"
"Volitelné parametry do budoucna: `apps` (comma-separated) pokud bude přidána filtrace.\n\n"
"Response obsahuje pole `trash` a objekt `pagination`. Každá položka má strukturu:\n"
"`{ model: 'app_label.model', id: <pk>, deleted_at: <datetime|null>, data: { ...fields } }`."
),
parameters=[
OpenApiParameter(name="page", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, required=False, description="Číslo stránky (>=1)"),
OpenApiParameter(name="page_size", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, required=False, description="Počet záznamů na stránce (default 20, max 200)"),
OpenApiParameter(name="limit", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, required=False, description="Alias pro page_size"),
],
)
class TrashView(APIView):
permission_classes = [OnlyRolesAllowed("admin")]
def get(self, request):
# Optional filtering by apps (?apps=account,booking)
ctx = {"request": request}
apps_param = request.query_params.get("apps")
if apps_param:
ctx["apps"] = [a.strip() for a in apps_param.split(",") if a.strip()]
serializer = TrashSerializer(context=ctx)
return Response(serializer.data)
@extend_schema(
request={
'application/json': {
'type': 'object',
'properties': {
'model': {'type': 'string', 'example': 'booking.event', 'description': 'app_label.model_name (lowercase)'},
'id': {'type': 'string', 'example': '5', 'description': 'Primární klíč objektu'},
},
'required': ['model', 'id']
}
},
responses={200: dict, 400: dict, 404: dict},
methods=["PATCH"],
description=(
"Obnovení (undelete) jednoho objektu dle model labelu a ID. Nastaví `is_deleted=False` a `deleted_at=None`.\n\n"
"Body JSON:\n"
"{ 'model': 'booking.event', 'id': '5' }\n\n"
"Pokud už objekt není smazaný, operace je idempotentní a jen vrátí informaci, že je aktivní."
),
)
def patch(self, request):
model_label = request.data.get("model")
obj_id = request.data.get("id")
if not model_label or not obj_id:
return Response({
"success": False,
"error": "Missing 'model' or 'id' in request body"
}, status=400)
if "." not in model_label:
return Response({"success": False, "error": "'model' must be in format app_label.model_name"}, status=400)
app_label, model_name = model_label.split(".", 1)
try:
model = django_apps.get_model(app_label, model_name)
except LookupError:
return Response({"success": False, "error": f"Model '{model_label}' not found"}, status=404)
# Ensure model has is_deleted
if not hasattr(model, 'is_deleted') and 'is_deleted' not in [f.name for f in model._meta.fields]:
return Response({"success": False, "error": f"Model '{model_label}' is not soft-deletable"}, status=400)
manager = getattr(model, 'all_objects', model._default_manager)
try:
instance = manager.get(pk=obj_id)
except model.DoesNotExist:
return Response({"success": False, "error": f"Object with id={obj_id} not found"}, status=404)
current_state = getattr(instance, 'is_deleted', False)
if current_state:
# Restore
setattr(instance, 'is_deleted', False)
if hasattr(instance, 'deleted_at'):
setattr(instance, 'deleted_at', None)
instance.save(update_fields=[f.name for f in instance._meta.fields if f.name in ('is_deleted', 'deleted_at')])
state_changed = True
message = "Object restored"
else:
state_changed = False
message = "No state change already active"
# Build minimal representation
data_repr = {}
for f in instance._meta.fields:
if f.name in ('is_deleted', 'deleted_at'):
continue
try:
val = getattr(instance, f.name)
if f.is_relation and hasattr(val, 'pk'):
val = val.pk
except Exception:
val = None
data_repr[f.name] = val
return Response({
"success": True,
"changed": state_changed,
"message": message,
"item": {
"model": model_label.lower(),
"id": instance.pk,
"is_deleted": getattr(instance, 'is_deleted', False),
"deleted_at": getattr(instance, 'deleted_at', None),
"data": data_repr,
}
}, status=200)