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,159 @@
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: <app_label.model_name>
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}