snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
This commit is contained in:
238
apps/api/plane/utils/exporters/schemas/base.py
Normal file
238
apps/api/plane/utils/exporters/schemas/base.py
Normal file
@@ -0,0 +1,238 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from django.db.models import QuerySet
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExportField:
|
||||
"""Base export field class for generic fields."""
|
||||
|
||||
source: Optional[str] = None
|
||||
default: Any = ""
|
||||
label: Optional[str] = None # Display name for export headers
|
||||
|
||||
def get_value(self, obj: Any, context: Dict[str, Any]) -> Any:
|
||||
raw: Any
|
||||
if self.source:
|
||||
raw = self._resolve_dotted_path(obj, self.source)
|
||||
else:
|
||||
raw = obj
|
||||
|
||||
return self._format_value(raw)
|
||||
|
||||
def _format_value(self, raw: Any) -> Any:
|
||||
"""Format the raw value. Override in subclasses for type-specific formatting."""
|
||||
return raw if raw is not None else self.default
|
||||
|
||||
def _resolve_dotted_path(self, obj: Any, path: str) -> Any:
|
||||
current = obj
|
||||
for part in path.split("."):
|
||||
if current is None:
|
||||
return None
|
||||
if hasattr(current, part):
|
||||
current = getattr(current, part)
|
||||
elif isinstance(current, dict):
|
||||
current = current.get(part)
|
||||
else:
|
||||
return None
|
||||
return current
|
||||
|
||||
|
||||
@dataclass
|
||||
class StringField(ExportField):
|
||||
"""Export field for string values."""
|
||||
|
||||
default: str = ""
|
||||
|
||||
def _format_value(self, raw: Any) -> str:
|
||||
if raw is None:
|
||||
return self.default
|
||||
return str(raw)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DateField(ExportField):
|
||||
"""Export field for date values with automatic conversion."""
|
||||
|
||||
default: str = ""
|
||||
|
||||
def _format_value(self, raw: Any) -> str:
|
||||
if raw is None:
|
||||
return self.default
|
||||
# Convert date to formatted string
|
||||
if hasattr(raw, "strftime"):
|
||||
return raw.strftime("%a, %d %b %Y")
|
||||
return str(raw)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DateTimeField(ExportField):
|
||||
"""Export field for datetime values with automatic conversion."""
|
||||
|
||||
default: str = ""
|
||||
|
||||
def _format_value(self, raw: Any) -> str:
|
||||
if raw is None:
|
||||
return self.default
|
||||
# Convert datetime to formatted string
|
||||
if hasattr(raw, "strftime"):
|
||||
return raw.strftime("%a, %d %b %Y %I:%M:%S %Z%z")
|
||||
return str(raw)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NumberField(ExportField):
|
||||
"""Export field for numeric values."""
|
||||
|
||||
default: Any = ""
|
||||
|
||||
def _format_value(self, raw: Any) -> Any:
|
||||
if raw is None:
|
||||
return self.default
|
||||
return raw
|
||||
|
||||
|
||||
@dataclass
|
||||
class BooleanField(ExportField):
|
||||
"""Export field for boolean values."""
|
||||
|
||||
default: bool = False
|
||||
|
||||
def _format_value(self, raw: Any) -> bool:
|
||||
if raw is None:
|
||||
return self.default
|
||||
return bool(raw)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListField(ExportField):
|
||||
"""Export field for list/array values.
|
||||
|
||||
Returns the list as-is by default. The formatter will handle conversion to strings
|
||||
when needed (e.g., CSV/XLSX will join with separator, JSON will keep as array).
|
||||
"""
|
||||
|
||||
default: Optional[List] = field(default_factory=list)
|
||||
|
||||
def _format_value(self, raw: Any) -> List[Any]:
|
||||
if raw is None:
|
||||
return self.default if self.default is not None else []
|
||||
if isinstance(raw, (list, tuple)):
|
||||
return list(raw)
|
||||
return [raw] # Wrap single items in a list
|
||||
|
||||
|
||||
@dataclass
|
||||
class JSONField(ExportField):
|
||||
"""Export field for complex JSON-serializable values (dicts, lists of dicts, etc).
|
||||
|
||||
Preserves the structure as-is for JSON exports. For CSV/XLSX, the formatter
|
||||
will handle serialization (e.g., JSON stringify).
|
||||
"""
|
||||
|
||||
default: Any = field(default_factory=dict)
|
||||
|
||||
def _format_value(self, raw: Any) -> Any:
|
||||
if raw is None:
|
||||
return self.default
|
||||
# Return as-is - should be JSON-serializable
|
||||
return raw
|
||||
|
||||
|
||||
class ExportSchemaMeta(type):
|
||||
def __new__(mcls, name, bases, attrs):
|
||||
declared: Dict[str, ExportField] = {
|
||||
key: value for key, value in list(attrs.items()) if isinstance(value, ExportField)
|
||||
}
|
||||
for key in declared.keys():
|
||||
attrs.pop(key)
|
||||
cls = super().__new__(mcls, name, bases, attrs)
|
||||
base_fields: Dict[str, ExportField] = {}
|
||||
for base in bases:
|
||||
if hasattr(base, "_declared_fields"):
|
||||
base_fields.update(base._declared_fields)
|
||||
base_fields.update(declared)
|
||||
cls._declared_fields = base_fields
|
||||
return cls
|
||||
|
||||
|
||||
class ExportSchema(metaclass=ExportSchemaMeta):
|
||||
"""Base schema for exporting data in various formats.
|
||||
|
||||
Subclasses should define fields as class attributes and can override:
|
||||
- prepare_<field_name> methods for custom field serialization
|
||||
- get_context_data() class method to pre-fetch related data for the queryset
|
||||
"""
|
||||
|
||||
def __init__(self, context: Optional[Dict[str, Any]] = None) -> None:
|
||||
self.context = context or {}
|
||||
|
||||
def serialize(self, obj: Any, fields: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
"""Serialize a single object.
|
||||
|
||||
Args:
|
||||
obj: The object to serialize
|
||||
fields: Optional list of field names to include. If None, all fields are serialized.
|
||||
|
||||
Returns:
|
||||
Dictionary of serialized data
|
||||
"""
|
||||
output: Dict[str, Any] = {}
|
||||
# Determine which fields to process
|
||||
fields_to_process = fields if fields else list(self._declared_fields.keys())
|
||||
|
||||
for field_name in fields_to_process:
|
||||
# Skip if field doesn't exist in schema
|
||||
if field_name not in self._declared_fields:
|
||||
continue
|
||||
|
||||
export_field = self._declared_fields[field_name]
|
||||
|
||||
# Prefer explicit preparer methods if present
|
||||
preparer = getattr(self, f"prepare_{field_name}", None)
|
||||
if callable(preparer):
|
||||
output[field_name] = preparer(obj)
|
||||
continue
|
||||
|
||||
output[field_name] = export_field.get_value(obj, self.context)
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def get_context_data(cls, queryset: QuerySet) -> Dict[str, Any]:
|
||||
"""Get context data for serialization. Override in subclasses to pre-fetch related data.
|
||||
|
||||
Args:
|
||||
queryset: QuerySet of objects to be serialized
|
||||
|
||||
Returns:
|
||||
Dictionary of context data to be passed to the schema instance
|
||||
"""
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def serialize_queryset(cls, queryset: QuerySet, fields: List[str] = None) -> List[Dict[str, Any]]:
|
||||
"""Serialize a queryset of objects to export data.
|
||||
|
||||
Args:
|
||||
queryset: QuerySet of objects to serialize
|
||||
fields: Optional list of field names to include. Defaults to all fields.
|
||||
|
||||
Returns:
|
||||
List of dictionaries containing serialized data
|
||||
"""
|
||||
# Get context data (can be extended by subclasses)
|
||||
context = cls.get_context_data(queryset)
|
||||
|
||||
# Serialize each object, passing fields to only process requested fields
|
||||
schema = cls(context=context)
|
||||
data = []
|
||||
for obj in queryset:
|
||||
obj_data = schema.serialize(obj, fields=fields)
|
||||
data.append(obj_data)
|
||||
|
||||
return data
|
||||
Reference in New Issue
Block a user