Files
e-trznice/backend/configuration/views.py
2025-10-02 00:54:34 +02:00

201 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)