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