django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +279 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -1,516 +0,0 @@
|
|
1
|
-
try:
|
2
|
-
import openpyxl
|
3
|
-
from openpyxl.styles import Font, Alignment, PatternFill
|
4
|
-
from openpyxl.utils import get_column_letter
|
5
|
-
HAS_OPENPYXL = True
|
6
|
-
except ImportError:
|
7
|
-
openpyxl = None
|
8
|
-
HAS_OPENPYXL = False
|
9
|
-
|
10
|
-
import io
|
11
|
-
from decimal import Decimal
|
12
|
-
from datetime import datetime, date, time
|
13
|
-
from django.http import HttpResponse
|
14
|
-
from django.db.models import QuerySet
|
15
|
-
from mojo.helpers import logit
|
16
|
-
|
17
|
-
logger = logit.get_logger("excel_formatter", "excel_formatter.log")
|
18
|
-
|
19
|
-
|
20
|
-
class ExcelFormatter:
|
21
|
-
"""
|
22
|
-
Advanced Excel formatter with openpyxl integration and RestMeta.GRAPHS support.
|
23
|
-
"""
|
24
|
-
|
25
|
-
def __init__(self, sheet_name="Sheet1", freeze_panes=True, auto_width=True,
|
26
|
-
header_style=True, date_format="YYYY-MM-DD", datetime_format="YYYY-MM-DD HH:MM:SS"):
|
27
|
-
"""
|
28
|
-
Initialize Excel formatter.
|
29
|
-
|
30
|
-
:param sheet_name: Name for the Excel worksheet
|
31
|
-
:param freeze_panes: Freeze header row for easier navigation
|
32
|
-
:param auto_width: Auto-adjust column widths
|
33
|
-
:param header_style: Apply styling to header row
|
34
|
-
:param date_format: Format for date cells
|
35
|
-
:param datetime_format: Format for datetime cells
|
36
|
-
"""
|
37
|
-
if not HAS_OPENPYXL:
|
38
|
-
raise ImportError("openpyxl is required for Excel export. Install with: pip install openpyxl")
|
39
|
-
|
40
|
-
self.sheet_name = sheet_name
|
41
|
-
self.freeze_panes = freeze_panes
|
42
|
-
self.auto_width = auto_width
|
43
|
-
self.header_style = header_style
|
44
|
-
self.date_format = date_format
|
45
|
-
self.datetime_format = datetime_format
|
46
|
-
|
47
|
-
# Style configurations
|
48
|
-
self.header_font = Font(bold=True, color="FFFFFF")
|
49
|
-
self.header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
|
50
|
-
self.header_alignment = Alignment(horizontal="center", vertical="center")
|
51
|
-
|
52
|
-
def serialize_queryset(self, queryset, fields=None, graph=None, filename="export.xlsx",
|
53
|
-
headers=None, localize=None):
|
54
|
-
"""
|
55
|
-
Serialize a Django QuerySet to Excel format.
|
56
|
-
|
57
|
-
:param queryset: Django QuerySet to serialize
|
58
|
-
:param fields: List of field names or tuples (field_name, display_name)
|
59
|
-
:param graph: RestMeta graph name to use for field configuration
|
60
|
-
:param filename: Output filename
|
61
|
-
:param headers: Custom header names (overrides field names)
|
62
|
-
:param localize: Localization configuration
|
63
|
-
:return: HttpResponse with Excel file
|
64
|
-
"""
|
65
|
-
# Get fields configuration
|
66
|
-
field_config = self._get_field_config(queryset, fields, graph)
|
67
|
-
|
68
|
-
# Create workbook and worksheet
|
69
|
-
workbook = openpyxl.Workbook()
|
70
|
-
worksheet = workbook.active
|
71
|
-
worksheet.title = self.sheet_name
|
72
|
-
|
73
|
-
# Write header row
|
74
|
-
self._write_header_row(worksheet, field_config['headers'])
|
75
|
-
|
76
|
-
# Write data rows
|
77
|
-
row_num = 2 # Start after header
|
78
|
-
for obj in queryset:
|
79
|
-
try:
|
80
|
-
row_data = self._extract_row_data(obj, field_config['field_names'], localize)
|
81
|
-
self._write_data_row(worksheet, row_num, row_data)
|
82
|
-
row_num += 1
|
83
|
-
except Exception as e:
|
84
|
-
logger.error(f"Error processing row for object {obj.pk}: {e}")
|
85
|
-
continue
|
86
|
-
|
87
|
-
# Apply formatting
|
88
|
-
self._apply_formatting(worksheet, len(field_config['headers']), row_num - 1)
|
89
|
-
|
90
|
-
# Create HTTP response
|
91
|
-
return self._create_excel_response(workbook, filename)
|
92
|
-
|
93
|
-
def serialize_data(self, data, fields=None, filename="export.xlsx", headers=None):
|
94
|
-
"""
|
95
|
-
Serialize list of dictionaries or objects to Excel.
|
96
|
-
|
97
|
-
:param data: List of dictionaries or objects
|
98
|
-
:param fields: Field names to include
|
99
|
-
:param filename: Output filename
|
100
|
-
:param headers: Custom header names
|
101
|
-
:return: HttpResponse with Excel file
|
102
|
-
"""
|
103
|
-
if not data:
|
104
|
-
return self._create_empty_excel_response(filename)
|
105
|
-
|
106
|
-
# Auto-detect fields if not provided
|
107
|
-
if not fields:
|
108
|
-
fields = self._auto_detect_fields(data[0])
|
109
|
-
|
110
|
-
# Prepare field configuration
|
111
|
-
field_config = self._prepare_field_config(fields, headers)
|
112
|
-
|
113
|
-
# Create workbook and worksheet
|
114
|
-
workbook = openpyxl.Workbook()
|
115
|
-
worksheet = workbook.active
|
116
|
-
worksheet.title = self.sheet_name
|
117
|
-
|
118
|
-
# Write header row
|
119
|
-
self._write_header_row(worksheet, field_config['headers'])
|
120
|
-
|
121
|
-
# Write data rows
|
122
|
-
for row_num, item in enumerate(data, start=2):
|
123
|
-
try:
|
124
|
-
row_data = self._extract_row_data(item, field_config['field_names'])
|
125
|
-
self._write_data_row(worksheet, row_num, row_data)
|
126
|
-
except Exception as e:
|
127
|
-
logger.error(f"Error processing row {row_num}: {e}")
|
128
|
-
continue
|
129
|
-
|
130
|
-
# Apply formatting
|
131
|
-
self._apply_formatting(worksheet, len(field_config['headers']), len(data) + 1)
|
132
|
-
|
133
|
-
return self._create_excel_response(workbook, filename)
|
134
|
-
|
135
|
-
def _get_field_config(self, queryset, fields, graph):
|
136
|
-
"""
|
137
|
-
Get field configuration from various sources.
|
138
|
-
"""
|
139
|
-
if fields:
|
140
|
-
return self._prepare_field_config(fields)
|
141
|
-
|
142
|
-
# Try to get from RestMeta.GRAPHS
|
143
|
-
if graph and hasattr(queryset.model, 'RestMeta'):
|
144
|
-
rest_meta = queryset.model.RestMeta
|
145
|
-
if hasattr(rest_meta, 'GRAPHS') and graph in rest_meta.GRAPHS:
|
146
|
-
graph_config = rest_meta.GRAPHS[graph]
|
147
|
-
graph_fields = graph_config.get('fields', [])
|
148
|
-
if graph_fields:
|
149
|
-
return self._prepare_field_config(graph_fields)
|
150
|
-
|
151
|
-
# Fallback to model fields
|
152
|
-
model_fields = [f.name for f in queryset.model._meta.fields]
|
153
|
-
return self._prepare_field_config(model_fields)
|
154
|
-
|
155
|
-
def _prepare_field_config(self, fields, headers=None):
|
156
|
-
"""
|
157
|
-
Prepare field configuration for Excel generation.
|
158
|
-
"""
|
159
|
-
field_names = []
|
160
|
-
field_headers = []
|
161
|
-
|
162
|
-
for field in fields:
|
163
|
-
if isinstance(field, (tuple, list)):
|
164
|
-
field_name, display_name = field
|
165
|
-
field_names.append(field_name)
|
166
|
-
field_headers.append(display_name)
|
167
|
-
else:
|
168
|
-
field_names.append(field)
|
169
|
-
field_headers.append(field.replace('_', ' ').title())
|
170
|
-
|
171
|
-
# Override with custom headers if provided
|
172
|
-
if headers:
|
173
|
-
field_headers = headers[:len(field_names)]
|
174
|
-
|
175
|
-
return {
|
176
|
-
'field_names': field_names,
|
177
|
-
'headers': field_headers
|
178
|
-
}
|
179
|
-
|
180
|
-
def _write_header_row(self, worksheet, headers):
|
181
|
-
"""
|
182
|
-
Write header row to worksheet with styling.
|
183
|
-
"""
|
184
|
-
for col_num, header in enumerate(headers, start=1):
|
185
|
-
cell = worksheet.cell(row=1, column=col_num, value=header)
|
186
|
-
|
187
|
-
if self.header_style:
|
188
|
-
cell.font = self.header_font
|
189
|
-
cell.fill = self.header_fill
|
190
|
-
cell.alignment = self.header_alignment
|
191
|
-
|
192
|
-
def _write_data_row(self, worksheet, row_num, row_data):
|
193
|
-
"""
|
194
|
-
Write data row to worksheet with proper type handling.
|
195
|
-
"""
|
196
|
-
for col_num, value in enumerate(row_data, start=1):
|
197
|
-
cell = worksheet.cell(row=row_num, column=col_num)
|
198
|
-
|
199
|
-
# Handle different data types
|
200
|
-
if isinstance(value, datetime):
|
201
|
-
cell.value = value
|
202
|
-
cell.number_format = self.datetime_format
|
203
|
-
elif isinstance(value, date):
|
204
|
-
cell.value = value
|
205
|
-
cell.number_format = self.date_format
|
206
|
-
elif isinstance(value, time):
|
207
|
-
cell.value = value
|
208
|
-
cell.number_format = "HH:MM:SS"
|
209
|
-
elif isinstance(value, (int, float, Decimal)):
|
210
|
-
try:
|
211
|
-
cell.value = float(value) if isinstance(value, Decimal) else value
|
212
|
-
except (ValueError, TypeError):
|
213
|
-
cell.value = str(value)
|
214
|
-
elif isinstance(value, bool):
|
215
|
-
cell.value = value
|
216
|
-
else:
|
217
|
-
cell.value = str(value) if value is not None else ""
|
218
|
-
|
219
|
-
def _apply_formatting(self, worksheet, num_cols, num_rows):
|
220
|
-
"""
|
221
|
-
Apply formatting to the worksheet.
|
222
|
-
"""
|
223
|
-
# Freeze panes (header row)
|
224
|
-
if self.freeze_panes and num_rows > 1:
|
225
|
-
worksheet.freeze_panes = 'A2'
|
226
|
-
|
227
|
-
# Auto-adjust column widths
|
228
|
-
if self.auto_width:
|
229
|
-
self._auto_adjust_column_widths(worksheet, num_cols, num_rows)
|
230
|
-
|
231
|
-
def _auto_adjust_column_widths(self, worksheet, num_cols, num_rows):
|
232
|
-
"""
|
233
|
-
Auto-adjust column widths based on content.
|
234
|
-
"""
|
235
|
-
for col_num in range(1, num_cols + 1):
|
236
|
-
column_letter = get_column_letter(col_num)
|
237
|
-
max_length = 0
|
238
|
-
|
239
|
-
# Check all cells in the column
|
240
|
-
for row_num in range(1, min(num_rows + 1, 100)): # Limit to first 100 rows for performance
|
241
|
-
try:
|
242
|
-
cell_value = worksheet[f"{column_letter}{row_num}"].value
|
243
|
-
if cell_value:
|
244
|
-
max_length = max(max_length, len(str(cell_value)))
|
245
|
-
except Exception:
|
246
|
-
continue
|
247
|
-
|
248
|
-
# Set column width with some padding
|
249
|
-
adjusted_width = min(max_length + 2, 50) # Cap at 50 characters
|
250
|
-
worksheet.column_dimensions[column_letter].width = max(adjusted_width, 10)
|
251
|
-
|
252
|
-
def _extract_row_data(self, obj, field_names, localize=None):
|
253
|
-
"""
|
254
|
-
Extract row data from an object based on field names.
|
255
|
-
"""
|
256
|
-
row = []
|
257
|
-
|
258
|
-
for field_name in field_names:
|
259
|
-
try:
|
260
|
-
value = self._get_field_value(obj, field_name)
|
261
|
-
value = self._process_field_value(value, field_name, localize)
|
262
|
-
row.append(value)
|
263
|
-
except Exception as e:
|
264
|
-
logger.warning(f"Error extracting field '{field_name}': {e}")
|
265
|
-
row.append(None)
|
266
|
-
|
267
|
-
return row
|
268
|
-
|
269
|
-
def _get_field_value(self, obj, field_name):
|
270
|
-
"""
|
271
|
-
Get field value from object, supporting nested field access.
|
272
|
-
"""
|
273
|
-
# Handle nested field access (e.g., "user.email", "profile.address.city")
|
274
|
-
if '.' in field_name:
|
275
|
-
return self._get_nested_field_value(obj, field_name)
|
276
|
-
|
277
|
-
# Handle special metadata fields
|
278
|
-
if field_name.startswith('metadata.') and hasattr(obj, 'getProperty'):
|
279
|
-
parts = field_name.split('.', 2)
|
280
|
-
if len(parts) == 3:
|
281
|
-
return obj.getProperty(parts[2], category=parts[1])
|
282
|
-
elif len(parts) == 2:
|
283
|
-
return obj.getProperty(parts[1])
|
284
|
-
|
285
|
-
# Standard field access
|
286
|
-
if hasattr(obj, field_name):
|
287
|
-
value = getattr(obj, field_name)
|
288
|
-
return value() if callable(value) else value
|
289
|
-
|
290
|
-
# Dictionary-style access
|
291
|
-
if isinstance(obj, dict):
|
292
|
-
return obj.get(field_name, None)
|
293
|
-
|
294
|
-
return None
|
295
|
-
|
296
|
-
def _get_nested_field_value(self, obj, field_path):
|
297
|
-
"""
|
298
|
-
Get value from nested field path like "user.profile.name".
|
299
|
-
"""
|
300
|
-
try:
|
301
|
-
current = obj
|
302
|
-
for field_part in field_path.split('.'):
|
303
|
-
if current is None:
|
304
|
-
return None
|
305
|
-
|
306
|
-
if hasattr(current, field_part):
|
307
|
-
current = getattr(current, field_part)
|
308
|
-
elif isinstance(current, dict):
|
309
|
-
current = current.get(field_part)
|
310
|
-
else:
|
311
|
-
return None
|
312
|
-
|
313
|
-
# Handle callable attributes
|
314
|
-
if callable(current):
|
315
|
-
current = current()
|
316
|
-
|
317
|
-
return current
|
318
|
-
except Exception as e:
|
319
|
-
logger.warning(f"Error accessing nested field '{field_path}': {e}")
|
320
|
-
return None
|
321
|
-
|
322
|
-
def _process_field_value(self, value, field_name, localize=None):
|
323
|
-
"""
|
324
|
-
Process field value with localization and special handling.
|
325
|
-
"""
|
326
|
-
if value is None:
|
327
|
-
return None
|
328
|
-
|
329
|
-
# Apply localization if configured
|
330
|
-
if localize and field_name in localize:
|
331
|
-
try:
|
332
|
-
localizer_config = localize[field_name]
|
333
|
-
if '|' in localizer_config:
|
334
|
-
localizer_name, extra = localizer_config.split('|', 1)
|
335
|
-
else:
|
336
|
-
localizer_name, extra = localizer_config, None
|
337
|
-
|
338
|
-
# Import and apply localizer
|
339
|
-
from mojo.serializers.formats.localizers import get_localizer
|
340
|
-
localizer = get_localizer(localizer_name)
|
341
|
-
if localizer:
|
342
|
-
return localizer(value, extra)
|
343
|
-
except Exception as e:
|
344
|
-
logger.warning(f"Localization failed for field '{field_name}': {e}")
|
345
|
-
|
346
|
-
# Handle model instances
|
347
|
-
if hasattr(value, 'pk'):
|
348
|
-
return str(value)
|
349
|
-
|
350
|
-
# Handle collections (flatten to string for Excel)
|
351
|
-
elif isinstance(value, (list, tuple)):
|
352
|
-
return '; '.join(str(item) for item in value)
|
353
|
-
elif isinstance(value, dict):
|
354
|
-
return str(value)
|
355
|
-
|
356
|
-
return value
|
357
|
-
|
358
|
-
def _auto_detect_fields(self, sample_item):
|
359
|
-
"""
|
360
|
-
Auto-detect fields from a sample data item.
|
361
|
-
"""
|
362
|
-
if isinstance(sample_item, dict):
|
363
|
-
return list(sample_item.keys())
|
364
|
-
elif hasattr(sample_item, '_meta'):
|
365
|
-
return [f.name for f in sample_item._meta.fields]
|
366
|
-
elif hasattr(sample_item, '__dict__'):
|
367
|
-
return list(sample_item.__dict__.keys())
|
368
|
-
else:
|
369
|
-
return ['value'] # Fallback for primitive types
|
370
|
-
|
371
|
-
def _create_excel_response(self, workbook, filename):
|
372
|
-
"""
|
373
|
-
Create HTTP response with Excel file.
|
374
|
-
"""
|
375
|
-
# Save workbook to BytesIO buffer
|
376
|
-
excel_buffer = io.BytesIO()
|
377
|
-
workbook.save(excel_buffer)
|
378
|
-
excel_buffer.seek(0)
|
379
|
-
|
380
|
-
# Create HTTP response
|
381
|
-
response = HttpResponse(
|
382
|
-
excel_buffer.getvalue(),
|
383
|
-
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
384
|
-
)
|
385
|
-
|
386
|
-
# Ensure filename has .xlsx extension
|
387
|
-
if not filename.endswith('.xlsx'):
|
388
|
-
filename += '.xlsx'
|
389
|
-
|
390
|
-
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
391
|
-
response['Content-Length'] = str(len(excel_buffer.getvalue()))
|
392
|
-
|
393
|
-
return response
|
394
|
-
|
395
|
-
def _create_empty_excel_response(self, filename):
|
396
|
-
"""
|
397
|
-
Create response for empty dataset.
|
398
|
-
"""
|
399
|
-
workbook = openpyxl.Workbook()
|
400
|
-
worksheet = workbook.active
|
401
|
-
worksheet.title = self.sheet_name
|
402
|
-
worksheet['A1'] = "No data available"
|
403
|
-
|
404
|
-
return self._create_excel_response(workbook, filename)
|
405
|
-
|
406
|
-
|
407
|
-
# Convenience functions for backwards compatibility
|
408
|
-
def generate_excel(queryset, fields, filename, headers=None, localize=None, sheet_name="Sheet1"):
|
409
|
-
"""
|
410
|
-
Generate Excel file from queryset.
|
411
|
-
|
412
|
-
:param queryset: Django QuerySet
|
413
|
-
:param fields: List of field names
|
414
|
-
:param filename: Output filename
|
415
|
-
:param headers: Custom header names
|
416
|
-
:param localize: Localization config
|
417
|
-
:param sheet_name: Excel sheet name
|
418
|
-
:return: HttpResponse with Excel file
|
419
|
-
"""
|
420
|
-
formatter = ExcelFormatter(sheet_name=sheet_name)
|
421
|
-
return formatter.serialize_queryset(
|
422
|
-
queryset=queryset,
|
423
|
-
fields=fields,
|
424
|
-
filename=filename,
|
425
|
-
headers=headers,
|
426
|
-
localize=localize
|
427
|
-
)
|
428
|
-
|
429
|
-
|
430
|
-
def qsetToExcel(request, queryset, fields, filename, localize=None):
|
431
|
-
"""
|
432
|
-
Legacy function name for backwards compatibility.
|
433
|
-
"""
|
434
|
-
return generate_excel(queryset, fields, filename, localize=localize)
|
435
|
-
|
436
|
-
|
437
|
-
def serialize_to_excel(data, fields=None, filename="export.xlsx", headers=None, sheet_name="Sheet1"):
|
438
|
-
"""
|
439
|
-
Serialize list of data to Excel.
|
440
|
-
|
441
|
-
:param data: List of dictionaries or objects
|
442
|
-
:param fields: Field names to include
|
443
|
-
:param filename: Output filename
|
444
|
-
:param headers: Custom header names
|
445
|
-
:param sheet_name: Excel sheet name
|
446
|
-
:return: HttpResponse with Excel file
|
447
|
-
"""
|
448
|
-
formatter = ExcelFormatter(sheet_name=sheet_name)
|
449
|
-
return formatter.serialize_data(
|
450
|
-
data=data,
|
451
|
-
fields=fields,
|
452
|
-
filename=filename,
|
453
|
-
headers=headers
|
454
|
-
)
|
455
|
-
|
456
|
-
|
457
|
-
def create_multi_sheet_excel(data_sets, filename="export.xlsx"):
|
458
|
-
"""
|
459
|
-
Create Excel file with multiple sheets.
|
460
|
-
|
461
|
-
:param data_sets: List of tuples (sheet_name, queryset_or_data, fields, headers)
|
462
|
-
:param filename: Output filename
|
463
|
-
:return: HttpResponse with Excel file
|
464
|
-
"""
|
465
|
-
if not HAS_OPENPYXL:
|
466
|
-
raise ImportError("openpyxl is required for Excel export")
|
467
|
-
|
468
|
-
workbook = openpyxl.Workbook()
|
469
|
-
# Remove default sheet
|
470
|
-
workbook.remove(workbook.active)
|
471
|
-
|
472
|
-
for sheet_name, data, fields, headers in data_sets:
|
473
|
-
formatter = ExcelFormatter(sheet_name=sheet_name)
|
474
|
-
|
475
|
-
# Create new worksheet
|
476
|
-
worksheet = workbook.create_sheet(title=sheet_name)
|
477
|
-
|
478
|
-
if isinstance(data, QuerySet):
|
479
|
-
field_config = formatter._get_field_config(data, fields, None)
|
480
|
-
else:
|
481
|
-
if not fields and data:
|
482
|
-
fields = formatter._auto_detect_fields(data[0])
|
483
|
-
field_config = formatter._prepare_field_config(fields, headers)
|
484
|
-
|
485
|
-
# Write header
|
486
|
-
formatter._write_header_row(worksheet, field_config['headers'])
|
487
|
-
|
488
|
-
# Write data
|
489
|
-
row_num = 2
|
490
|
-
for item in data:
|
491
|
-
try:
|
492
|
-
row_data = formatter._extract_row_data(item, field_config['field_names'])
|
493
|
-
formatter._write_data_row(worksheet, row_num, row_data)
|
494
|
-
row_num += 1
|
495
|
-
except Exception as e:
|
496
|
-
logger.error(f"Error processing row in sheet '{sheet_name}': {e}")
|
497
|
-
continue
|
498
|
-
|
499
|
-
# Apply formatting
|
500
|
-
formatter._apply_formatting(worksheet, len(field_config['headers']), row_num - 1)
|
501
|
-
|
502
|
-
# Create response
|
503
|
-
excel_buffer = io.BytesIO()
|
504
|
-
workbook.save(excel_buffer)
|
505
|
-
excel_buffer.seek(0)
|
506
|
-
|
507
|
-
response = HttpResponse(
|
508
|
-
excel_buffer.getvalue(),
|
509
|
-
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
510
|
-
)
|
511
|
-
|
512
|
-
if not filename.endswith('.xlsx'):
|
513
|
-
filename += '.xlsx'
|
514
|
-
|
515
|
-
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
516
|
-
return response
|