init
This commit is contained in:
200
backend/configuration/views.py
Normal file
200
backend/configuration/views.py
Normal 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)
|
||||
Reference in New Issue
Block a user