mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 11:57:56 +01:00
fix: responsibility change and additional fields
This commit is contained in:
@@ -15,11 +15,10 @@ from django.utils import timezone
|
||||
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.porters.exporter import DataExporter
|
||||
from plane.utils.porters.serializers.issue import IssueExportSerializer
|
||||
from plane.utils.porters.formatters import CSVFormatter, JSONFormatter, XLSXFormatter
|
||||
|
||||
|
||||
def create_zip_file(files: List[tuple[str, str | bytes]]) -> io.BytesIO:
|
||||
@@ -161,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"),
|
||||
@@ -180,20 +185,9 @@ def issue_export_task(
|
||||
)
|
||||
)
|
||||
|
||||
# Map provider to formatter
|
||||
formatter_map = {
|
||||
"csv": CSVFormatter(),
|
||||
"json": JSONFormatter(),
|
||||
"xlsx": XLSXFormatter(list_joiner=", "),
|
||||
}
|
||||
|
||||
# Create exporter for the specified format
|
||||
try:
|
||||
if provider not in formatter_map:
|
||||
raise ValueError(f"Unsupported format: {provider}. Available: csv, json, xlsx")
|
||||
|
||||
formatter = formatter_map[provider]
|
||||
exporter = DataExporter(IssueExportSerializer)
|
||||
exporter = DataExporter(IssueExportSerializer, format_type=provider)
|
||||
except ValueError as e:
|
||||
# Invalid format type
|
||||
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||
@@ -208,14 +202,12 @@ def issue_export_task(
|
||||
for project_id in project_ids:
|
||||
project_issues = workspace_issues.filter(project_id=project_id)
|
||||
export_filename = f"{slug}-{project_id}"
|
||||
content = exporter.to_string(project_issues, formatter)
|
||||
filename = f"{export_filename}.{formatter.extension}"
|
||||
filename, content = exporter.export(export_filename, project_issues)
|
||||
files.append((filename, content))
|
||||
else:
|
||||
# Export all issues in a single file
|
||||
export_filename = f"{slug}-{workspace_id}"
|
||||
content = exporter.to_string(workspace_issues, formatter)
|
||||
filename = f"{export_filename}.{formatter.extension}"
|
||||
filename, content = exporter.export(export_filename, workspace_issues)
|
||||
files.append((filename, content))
|
||||
|
||||
zip_buffer = create_zip_file(files)
|
||||
|
||||
@@ -147,6 +147,11 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
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
|
||||
|
||||
@@ -1,19 +1,57 @@
|
||||
from typing import Dict, List
|
||||
from .formatters import BaseFormatter
|
||||
from typing import Dict, List, Union
|
||||
from .formatters import BaseFormatter, CSVFormatter, JSONFormatter, XLSXFormatter
|
||||
|
||||
|
||||
class DataExporter:
|
||||
"""
|
||||
Export data using DRF serializers.
|
||||
Export data using DRF serializers with built-in format support.
|
||||
|
||||
Usage:
|
||||
exporter = DataExporter(BookSerializer, exclude=['password'])
|
||||
exporter.to_file(queryset, 'books.csv', CSVFormatter())
|
||||
# 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())
|
||||
"""
|
||||
|
||||
def __init__(self, serializer_class, **serializer_kwargs):
|
||||
# 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"""
|
||||
@@ -24,14 +62,42 @@ class DataExporter:
|
||||
)
|
||||
return serializer.data
|
||||
|
||||
def to_string(self, queryset, formatter: BaseFormatter) -> str:
|
||||
"""Export to formatted string"""
|
||||
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"""
|
||||
"""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())
|
||||
|
||||
@@ -16,13 +16,18 @@ class IssueExportSerializer(IssueSerializer):
|
||||
project_name = serializers.CharField(source='project.name', read_only=True, default="")
|
||||
project_identifier = serializers.CharField(source='project.identifier', read_only=True, default="")
|
||||
state_name = serializers.CharField(source='state.name', read_only=True, default="")
|
||||
created_by_name = serializers.CharField(source='created_by.full_name', read_only=True, default="")
|
||||
|
||||
created_by_name = serializers.SerializerMethodField()
|
||||
assignees = serializers.SerializerMethodField()
|
||||
parent = serializers.SerializerMethodField()
|
||||
labels = serializers.SerializerMethodField()
|
||||
cycles = serializers.SerializerMethodField()
|
||||
modules = serializers.SerializerMethodField()
|
||||
comments = serializers.SerializerMethodField()
|
||||
estimate = serializers.SerializerMethodField()
|
||||
links = serializers.SerializerMethodField()
|
||||
relations = serializers.SerializerMethodField()
|
||||
subscribers = serializers.SerializerMethodField()
|
||||
|
||||
class Meta(IssueSerializer.Meta):
|
||||
fields = [
|
||||
@@ -35,6 +40,7 @@ class IssueExportSerializer(IssueSerializer):
|
||||
"state_name",
|
||||
"priority",
|
||||
"assignees",
|
||||
"subscribers",
|
||||
"created_by_name",
|
||||
"start_date",
|
||||
"target_date",
|
||||
@@ -42,9 +48,13 @@ class IssueExportSerializer(IssueSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"archived_at",
|
||||
"estimate",
|
||||
"labels",
|
||||
"cycles",
|
||||
"modules",
|
||||
"links",
|
||||
"relations",
|
||||
"comments",
|
||||
"sub_issues_count",
|
||||
"link_count",
|
||||
"attachment_count",
|
||||
@@ -54,17 +64,12 @@ class IssueExportSerializer(IssueSerializer):
|
||||
def get_identifier(self, obj):
|
||||
return f"{obj.project.identifier}-{obj.sequence_id}"
|
||||
|
||||
def get_created_by_name(self, obj):
|
||||
if not obj.created_by:
|
||||
return ""
|
||||
return f"{obj.created_by.first_name} {obj.created_by.last_name}".strip()
|
||||
|
||||
def get_assignees(self, obj):
|
||||
return [
|
||||
f"{u.first_name} {u.last_name}".strip()
|
||||
for u in obj.assignees.all()
|
||||
if u.is_active
|
||||
]
|
||||
return [u.full_name for u in obj.assignees.all() if u.is_active]
|
||||
|
||||
def get_subscribers(self, obj):
|
||||
"""Return list of subscriber names."""
|
||||
return [sub.subscriber.full_name for sub in obj.issue_subscribers.all() if sub.subscriber]
|
||||
|
||||
def get_parent(self, obj):
|
||||
if not obj.parent:
|
||||
@@ -83,3 +88,54 @@ class IssueExportSerializer(IssueSerializer):
|
||||
|
||||
def get_modules(self, obj):
|
||||
return [im.module.name for im in obj.issue_module.all()]
|
||||
|
||||
def get_estimate(self, obj):
|
||||
"""Return estimate point value."""
|
||||
if obj.estimate_point:
|
||||
return obj.estimate_point.value if hasattr(obj.estimate_point, 'value') else str(obj.estimate_point)
|
||||
return ""
|
||||
|
||||
def get_links(self, obj):
|
||||
"""Return list of issue links with titles."""
|
||||
return [
|
||||
{
|
||||
"url": link.url,
|
||||
"title": link.title if link.title else link.url,
|
||||
}
|
||||
for link in obj.issue_link.all()
|
||||
]
|
||||
|
||||
def get_relations(self, obj):
|
||||
"""Return list of related issues."""
|
||||
relations = []
|
||||
|
||||
# Outgoing relations (this issue relates to others)
|
||||
for rel in obj.issue_relation.all():
|
||||
if rel.related_issue:
|
||||
relations.append({
|
||||
"type": rel.relation_type if hasattr(rel, 'relation_type') else "related",
|
||||
"issue": f"{rel.related_issue.project.identifier}-{rel.related_issue.sequence_id}",
|
||||
"direction": "outgoing"
|
||||
})
|
||||
|
||||
# Incoming relations (other issues relate to this one)
|
||||
for rel in obj.issue_related.all():
|
||||
if rel.issue:
|
||||
relations.append({
|
||||
"type": rel.relation_type if hasattr(rel, 'relation_type') else "related",
|
||||
"issue": f"{rel.issue.project.identifier}-{rel.issue.sequence_id}",
|
||||
"direction": "incoming"
|
||||
})
|
||||
|
||||
return relations
|
||||
|
||||
def get_comments(self, obj):
|
||||
"""Return list of comments with author and timestamp."""
|
||||
return [
|
||||
{
|
||||
"comment": comment.comment_stripped if hasattr(comment, 'comment_stripped') else comment.comment_html,
|
||||
"created_by": comment.actor.full_name if comment.actor else "",
|
||||
"created_at": comment.created_at.strftime("%Y-%m-%d %H:%M:%S") if comment.created_at else "",
|
||||
}
|
||||
for comment in obj.issue_comments.all()
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user