Skip to content

Commit cba4d26

Browse files
committed
Merge branch 'preview' of https://github.com/makeplane/plane into chore-custom-theming-enhancements
2 parents 3913270 + be1113b commit cba4d26

File tree

19 files changed

+654
-66
lines changed

19 files changed

+654
-66
lines changed

apps/api/plane/bgtasks/export_task.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
from django.db.models import Prefetch
1616

1717
# Module imports
18-
from plane.db.models import ExporterHistory, Issue, IssueRelation
18+
from plane.db.models import ExporterHistory, Issue, IssueComment, IssueRelation, IssueSubscriber
1919
from plane.utils.exception_logger import log_exception
20-
from plane.utils.exporters import Exporter, IssueExportSchema
20+
from plane.utils.porters.exporter import DataExporter
21+
from plane.utils.porters.serializers.issue import IssueExportSerializer
2122

2223

2324
def create_zip_file(files: List[tuple[str, str | bytes]]) -> io.BytesIO:
@@ -159,10 +160,16 @@ def issue_export_task(
159160
"labels",
160161
"issue_cycle__cycle",
161162
"issue_module__module",
162-
"issue_comments",
163163
"assignees",
164-
"issue_subscribers",
165164
"issue_link",
165+
Prefetch(
166+
"issue_subscribers",
167+
queryset=IssueSubscriber.objects.select_related("subscriber"),
168+
),
169+
Prefetch(
170+
"issue_comments",
171+
queryset=IssueComment.objects.select_related("actor").order_by("created_at"),
172+
),
166173
Prefetch(
167174
"issue_relation",
168175
queryset=IssueRelation.objects.select_related("related_issue", "related_issue__project"),
@@ -180,11 +187,7 @@ def issue_export_task(
180187

181188
# Create exporter for the specified format
182189
try:
183-
exporter = Exporter(
184-
format_type=provider,
185-
schema_class=IssueExportSchema,
186-
options={"list_joiner": ", "},
187-
)
190+
exporter = DataExporter(IssueExportSerializer, format_type=provider)
188191
except ValueError as e:
189192
# Invalid format type
190193
exporter_instance = ExporterHistory.objects.get(token=token_id)

apps/api/plane/db/models/user.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ def cover_image_url(self):
147147
return self.cover_image
148148
return None
149149

150+
@property
151+
def full_name(self):
152+
"""Return user's full name (first + last)."""
153+
return f"{self.first_name} {self.last_name}".strip()
154+
150155
def save(self, *args, **kwargs):
151156
self.email = self.email.lower().strip()
152157
self.mobile_number = self.mobile_number
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from .formatters import BaseFormatter, CSVFormatter, JSONFormatter, XLSXFormatter
2+
from .exporter import DataExporter
3+
from .serializers import IssueExportSerializer
4+
5+
__all__ = [
6+
# Formatters
7+
"BaseFormatter",
8+
"CSVFormatter",
9+
"JSONFormatter",
10+
"XLSXFormatter",
11+
# Exporters
12+
"DataExporter",
13+
# Export Serializers
14+
"IssueExportSerializer",
15+
]
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from typing import Dict, List, Union
2+
from .formatters import BaseFormatter, CSVFormatter, JSONFormatter, XLSXFormatter
3+
4+
5+
class DataExporter:
6+
"""
7+
Export data using DRF serializers with built-in format support.
8+
9+
Usage:
10+
# New simplified interface
11+
exporter = DataExporter(BookSerializer, format_type='csv')
12+
filename, content = exporter.export('books_export', queryset)
13+
14+
# Legacy interface (still supported)
15+
exporter = DataExporter(BookSerializer)
16+
csv_string = exporter.to_string(queryset, CSVFormatter())
17+
"""
18+
19+
# Available formatters
20+
FORMATTERS = {
21+
"csv": CSVFormatter,
22+
"json": JSONFormatter,
23+
"xlsx": XLSXFormatter,
24+
}
25+
26+
def __init__(self, serializer_class, format_type: str = None, **serializer_kwargs):
27+
"""
28+
Initialize exporter with serializer and optional format type.
29+
30+
Args:
31+
serializer_class: DRF serializer class to use for data serialization
32+
format_type: Optional format type (csv, json, xlsx). If provided, enables export() method.
33+
**serializer_kwargs: Additional kwargs to pass to serializer
34+
"""
35+
self.serializer_class = serializer_class
36+
self.serializer_kwargs = serializer_kwargs
37+
self.format_type = format_type
38+
self.formatter = None
39+
40+
if format_type:
41+
if format_type not in self.FORMATTERS:
42+
raise ValueError(f"Unsupported format: {format_type}. Available: {list(self.FORMATTERS.keys())}")
43+
# Create formatter with default options
44+
self.formatter = self._create_formatter(format_type)
45+
46+
def _create_formatter(self, format_type: str) -> BaseFormatter:
47+
"""Create formatter instance with appropriate options."""
48+
formatter_class = self.FORMATTERS[format_type]
49+
50+
# Apply format-specific options
51+
if format_type == "xlsx":
52+
return formatter_class(list_joiner=", ")
53+
else:
54+
return formatter_class()
55+
56+
def serialize(self, queryset) -> List[Dict]:
57+
"""QuerySet → list of dicts"""
58+
serializer = self.serializer_class(
59+
queryset,
60+
many=True,
61+
**self.serializer_kwargs
62+
)
63+
return serializer.data
64+
65+
def export(self, filename: str, queryset) -> tuple[str, Union[str, bytes]]:
66+
"""
67+
Export queryset to file with configured format.
68+
69+
Args:
70+
filename: Base filename (without extension)
71+
queryset: Django QuerySet to export
72+
73+
Returns:
74+
Tuple of (filename_with_extension, content)
75+
76+
Raises:
77+
ValueError: If format_type was not provided during initialization
78+
"""
79+
if not self.formatter:
80+
raise ValueError("format_type must be provided during initialization to use export() method")
81+
82+
data = self.serialize(queryset)
83+
content = self.formatter.encode(data)
84+
full_filename = f"{filename}.{self.formatter.extension}"
85+
86+
return full_filename, content
87+
88+
def to_string(self, queryset, formatter: BaseFormatter) -> Union[str, bytes]:
89+
"""Export to formatted string (legacy interface)"""
90+
data = self.serialize(queryset)
91+
return formatter.encode(data)
92+
93+
def to_file(self, queryset, filepath: str, formatter: BaseFormatter) -> str:
94+
"""Export to file (legacy interface)"""
95+
content = self.to_string(queryset, formatter)
96+
with open(filepath, 'w', encoding='utf-8') as f:
97+
f.write(content)
98+
return filepath
99+
100+
@classmethod
101+
def get_available_formats(cls) -> List[str]:
102+
"""Get list of available export formats."""
103+
return list(cls.FORMATTERS.keys())

0 commit comments

Comments
 (0)