Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions apps/api/plane/bgtasks/export_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
from django.db.models import Prefetch

# Module imports
from plane.db.models import ExporterHistory, Issue, IssueRelation
from plane.db.models import ExporterHistory, Issue, IssueComment, IssueRelation, IssueSubscriber
from plane.utils.exception_logger import log_exception
from plane.utils.exporters import Exporter, IssueExportSchema
from plane.utils.porters.exporter import DataExporter
from plane.utils.porters.serializers.issue import IssueExportSerializer


def create_zip_file(files: List[tuple[str, str | bytes]]) -> io.BytesIO:
Expand Down Expand Up @@ -159,10 +160,16 @@ def issue_export_task(
"labels",
"issue_cycle__cycle",
"issue_module__module",
"issue_comments",
"assignees",
"issue_subscribers",
"issue_link",
Prefetch(
"issue_subscribers",
queryset=IssueSubscriber.objects.select_related("subscriber"),
),
Prefetch(
"issue_comments",
queryset=IssueComment.objects.select_related("actor").order_by("created_at"),
),
Prefetch(
"issue_relation",
queryset=IssueRelation.objects.select_related("related_issue", "related_issue__project"),
Expand All @@ -180,11 +187,7 @@ def issue_export_task(

# Create exporter for the specified format
try:
exporter = Exporter(
format_type=provider,
schema_class=IssueExportSchema,
options={"list_joiner": ", "},
)
exporter = DataExporter(IssueExportSerializer, format_type=provider)
except ValueError as e:
# Invalid format type
exporter_instance = ExporterHistory.objects.get(token=token_id)
Expand Down
5 changes: 5 additions & 0 deletions apps/api/plane/db/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ def cover_image_url(self):
return self.cover_image
return None

@property
def full_name(self):
"""Return user's full name (first + last)."""
return f"{self.first_name} {self.last_name}".strip()

def save(self, *args, **kwargs):
self.email = self.email.lower().strip()
self.mobile_number = self.mobile_number
Expand Down
15 changes: 15 additions & 0 deletions apps/api/plane/utils/porters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from .formatters import BaseFormatter, CSVFormatter, JSONFormatter, XLSXFormatter
from .exporter import DataExporter
from .serializers import IssueExportSerializer

__all__ = [
# Formatters
"BaseFormatter",
"CSVFormatter",
"JSONFormatter",
"XLSXFormatter",
# Exporters
"DataExporter",
# Export Serializers
"IssueExportSerializer",
]
103 changes: 103 additions & 0 deletions apps/api/plane/utils/porters/exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from typing import Dict, List, Union
from .formatters import BaseFormatter, CSVFormatter, JSONFormatter, XLSXFormatter


class DataExporter:
"""
Export data using DRF serializers with built-in format support.

Usage:
# New simplified interface
exporter = DataExporter(BookSerializer, format_type='csv')
filename, content = exporter.export('books_export', queryset)

# Legacy interface (still supported)
exporter = DataExporter(BookSerializer)
csv_string = exporter.to_string(queryset, CSVFormatter())
"""

# Available formatters
FORMATTERS = {
"csv": CSVFormatter,
"json": JSONFormatter,
"xlsx": XLSXFormatter,
}

def __init__(self, serializer_class, format_type: str = None, **serializer_kwargs):
"""
Initialize exporter with serializer and optional format type.

Args:
serializer_class: DRF serializer class to use for data serialization
format_type: Optional format type (csv, json, xlsx). If provided, enables export() method.
**serializer_kwargs: Additional kwargs to pass to serializer
"""
self.serializer_class = serializer_class
self.serializer_kwargs = serializer_kwargs
self.format_type = format_type
self.formatter = None

if format_type:
if format_type not in self.FORMATTERS:
raise ValueError(f"Unsupported format: {format_type}. Available: {list(self.FORMATTERS.keys())}")
# Create formatter with default options
self.formatter = self._create_formatter(format_type)

def _create_formatter(self, format_type: str) -> BaseFormatter:
"""Create formatter instance with appropriate options."""
formatter_class = self.FORMATTERS[format_type]

# Apply format-specific options
if format_type == "xlsx":
return formatter_class(list_joiner=", ")
else:
return formatter_class()

def serialize(self, queryset) -> List[Dict]:
"""QuerySet → list of dicts"""
serializer = self.serializer_class(
queryset,
many=True,
**self.serializer_kwargs
)
return serializer.data

def export(self, filename: str, queryset) -> tuple[str, Union[str, bytes]]:
"""
Export queryset to file with configured format.

Args:
filename: Base filename (without extension)
queryset: Django QuerySet to export

Returns:
Tuple of (filename_with_extension, content)

Raises:
ValueError: If format_type was not provided during initialization
"""
if not self.formatter:
raise ValueError("format_type must be provided during initialization to use export() method")

data = self.serialize(queryset)
content = self.formatter.encode(data)
full_filename = f"{filename}.{self.formatter.extension}"

return full_filename, content

def to_string(self, queryset, formatter: BaseFormatter) -> Union[str, bytes]:
"""Export to formatted string (legacy interface)"""
data = self.serialize(queryset)
return formatter.encode(data)

def to_file(self, queryset, filepath: str, formatter: BaseFormatter) -> str:
"""Export to file (legacy interface)"""
content = self.to_string(queryset, formatter)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
return filepath

@classmethod
def get_available_formats(cls) -> List[str]:
"""Get list of available export formats."""
return list(cls.FORMATTERS.keys())
Loading
Loading