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: , deleted_at: , 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)