from django.apps import apps from django.conf import settings from django.db.models.fields.related import ForeignObjectRel from rest_framework import serializers from trznice.utils import RoundedDateTimeField # noqa: F401 (kept if used elsewhere later) from .models import AppConfig class AppConfigSerializer(serializers.ModelSerializer): class Meta: model = AppConfig fields = "__all__" read_only_fields = ["last_changed_by", "last_changed_at"] class AppConfigPublicSerializer(serializers.ModelSerializer): """Public-facing limited subset used for navbar assets and basic contact info.""" class Meta: model = AppConfig fields = [ "id", "logo", "background_image", "contact_email", "contact_phone", "max_reservations_per_event", ] class TrashItemSerializer(serializers.Serializer): """Represents a single soft-deleted instance across any model. Fields: model: id: primary key value deleted_at: timestamp (if model defines it) data: remaining field values (excluding soft-delete bookkeeping fields) """ model = serializers.CharField() id = serializers.CharField() # CharField to allow UUIDs as well deleted_at = serializers.DateTimeField(allow_null=True, required=False) data = serializers.DictField(child=serializers.CharField(allow_blank=True, allow_null=True)) class TrashSerializer(serializers.Serializer): """Aggregates all soft-deleted objects (is_deleted=True) from selected apps. This dynamically inspects registered models and collects those that: * Have a concrete field named `is_deleted` * (Optional) Have a manager named `all_objects`; otherwise fall back to default `objects` Usage: Serialize with `TrashSerializer()` (no instance needed) and access `.data`. Optionally you can pass a context key `apps` with an iterable of app labels to restrict search (default: account, booking, commerce, product, servicedesk). """ items = serializers.SerializerMethodField() SETTINGS_APPS = set(getattr(settings, "MY_CREATED_APPS", [])) EXCLUDE_FIELD_NAMES = {"is_deleted", "deleted_at"} def get_items(self, _obj): # _obj unused (serializer acts as a data provider) # Allow overriding via context['apps']; otherwise use all custom apps from settings target_apps = set(self.context.get("apps", self.SETTINGS_APPS)) results = [] for model in apps.get_models(): app_label = model._meta.app_label if app_label not in target_apps: continue # Fast check for is_deleted field field_names = {f.name for f in model._meta.get_fields() if not isinstance(f, ForeignObjectRel)} if "is_deleted" not in field_names: continue manager = getattr(model, "all_objects", model._default_manager) queryset = manager.filter(is_deleted=True) if not queryset.exists(): continue # Prepare list of simple (non-relational) field objects for extraction concrete_fields = [ f for f in model._meta.get_fields() if not isinstance(f, ForeignObjectRel) and getattr(f, "concrete", False) ] for instance in queryset: data = {} for f in concrete_fields: if f.name in self.EXCLUDE_FIELD_NAMES: continue try: value = f.value_from_object(instance) # Represent related FK by its PK only if f.is_relation and hasattr(value, "pk"): value = value.pk except Exception: # noqa: BLE001 - defensive; skip problematic field value = None data[f.name] = None if value == "" else value results.append({ "model": f"{app_label}.{model._meta.model_name}", "id": instance.pk, "deleted_at": getattr(instance, "deleted_at", None), "data": data, }) # Optional: sort by deleted_at descending if available results.sort(key=lambda i: (i.get("deleted_at") is None, i.get("deleted_at")), reverse=True) return results def to_representation(self, instance): # instance unused all_items = self.get_items(instance) request = self.context.get("request") # ---- Pagination params ---- def _to_int(val, default): try: return max(1, int(val)) except Exception: return default if request is not None: page = _to_int(request.query_params.get("page", 1), 1) page_size = _to_int(request.query_params.get("page_size") or request.query_params.get("limit", 20), 20) else: # Fallback when no request in context (e.g., manual usage) page = 1 page_size = 20 # Enforce reasonable upper bound MAX_PAGE_SIZE = 200 if page_size > MAX_PAGE_SIZE: page_size = MAX_PAGE_SIZE total_items = len(all_items) total_pages = (total_items + page_size - 1) // page_size if page_size else 1 if page > total_pages and total_pages != 0: page = total_pages start = (page - 1) * page_size end = start + page_size page_items = all_items[start:end] pagination = { "page": page, "page_size": page_size, "total_items": total_items, "total_pages": total_pages, "has_next": page < total_pages, "has_previous": page > 1, } return {"trash": page_items, "pagination": pagination}