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
@@ -13,12 +13,12 @@ class CsvFormatter:
|
|
13
13
|
"""
|
14
14
|
Advanced CSV formatter with streaming support and RestMeta.GRAPHS integration.
|
15
15
|
"""
|
16
|
-
|
17
|
-
def __init__(self, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL,
|
16
|
+
|
17
|
+
def __init__(self, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL,
|
18
18
|
encoding='utf-8', streaming_threshold=1000):
|
19
19
|
"""
|
20
20
|
Initialize CSV formatter.
|
21
|
-
|
21
|
+
|
22
22
|
:param delimiter: Field delimiter (default comma)
|
23
23
|
:param quotechar: Quote character for fields containing special chars
|
24
24
|
:param quoting: Quoting behavior (csv.QUOTE_MINIMAL, etc.)
|
@@ -30,12 +30,12 @@ class CsvFormatter:
|
|
30
30
|
self.quoting = quoting
|
31
31
|
self.encoding = encoding
|
32
32
|
self.streaming_threshold = streaming_threshold
|
33
|
-
|
34
|
-
def serialize_queryset(self, queryset, fields=None, graph=None, filename="export.csv",
|
33
|
+
|
34
|
+
def serialize_queryset(self, queryset, fields=None, graph=None, filename="export.csv",
|
35
35
|
headers=None, localize=None, stream=True):
|
36
36
|
"""
|
37
37
|
Serialize a Django QuerySet to CSV format.
|
38
|
-
|
38
|
+
|
39
39
|
:param queryset: Django QuerySet to serialize
|
40
40
|
:param fields: List of field names or tuples (field_name, display_name)
|
41
41
|
:param graph: RestMeta graph name to use for field configuration
|
@@ -47,62 +47,62 @@ class CsvFormatter:
|
|
47
47
|
"""
|
48
48
|
# Determine if we should stream based on queryset size
|
49
49
|
should_stream = stream and queryset.count() > self.streaming_threshold
|
50
|
-
|
50
|
+
|
51
51
|
# Get fields configuration
|
52
52
|
field_config = self._get_field_config(queryset, fields, graph)
|
53
|
-
|
53
|
+
|
54
54
|
if should_stream:
|
55
|
-
return self._create_streaming_response(queryset, field_config, filename,
|
55
|
+
return self._create_streaming_response(queryset, field_config, filename,
|
56
56
|
headers, localize)
|
57
57
|
else:
|
58
|
-
return self._create_standard_response(queryset, field_config, filename,
|
58
|
+
return self._create_standard_response(queryset, field_config, filename,
|
59
59
|
headers, localize)
|
60
|
-
|
60
|
+
|
61
61
|
def serialize_data(self, data, fields=None, filename="export.csv", headers=None):
|
62
62
|
"""
|
63
63
|
Serialize list of dictionaries or objects to CSV.
|
64
|
-
|
64
|
+
|
65
65
|
:param data: List of dictionaries or objects
|
66
66
|
:param fields: Field names to include
|
67
|
-
:param filename: Output filename
|
67
|
+
:param filename: Output filename
|
68
68
|
:param headers: Custom header names
|
69
69
|
:return: HttpResponse
|
70
70
|
"""
|
71
71
|
if not data:
|
72
72
|
return self._create_empty_response(filename)
|
73
|
-
|
73
|
+
|
74
74
|
# Auto-detect fields if not provided
|
75
75
|
if not fields:
|
76
76
|
fields = self._auto_detect_fields(data[0])
|
77
|
-
|
77
|
+
|
78
78
|
# Prepare field configuration
|
79
79
|
field_config = self._prepare_field_config(fields, headers)
|
80
|
-
|
80
|
+
|
81
81
|
# Generate CSV content
|
82
82
|
output = io.StringIO()
|
83
|
-
writer = csv.writer(output, delimiter=self.delimiter,
|
83
|
+
writer = csv.writer(output, delimiter=self.delimiter,
|
84
84
|
quotechar=self.quotechar, quoting=self.quoting)
|
85
|
-
|
85
|
+
|
86
86
|
# Write header
|
87
87
|
writer.writerow(field_config['headers'])
|
88
|
-
|
88
|
+
|
89
89
|
# Write data rows
|
90
90
|
for item in data:
|
91
91
|
row = self._extract_row_data(item, field_config['field_names'])
|
92
92
|
writer.writerow(row)
|
93
|
-
|
93
|
+
|
94
94
|
# Create response
|
95
95
|
response = HttpResponse(output.getvalue(), content_type='text/csv')
|
96
|
-
response['Content-Disposition'] = f'attachment; filename=
|
96
|
+
response['Content-Disposition'] = f'attachment; filename={filename}'
|
97
97
|
return response
|
98
|
-
|
98
|
+
|
99
99
|
def _get_field_config(self, queryset, fields, graph):
|
100
100
|
"""
|
101
101
|
Get field configuration from various sources.
|
102
102
|
"""
|
103
103
|
if fields:
|
104
104
|
return self._prepare_field_config(fields)
|
105
|
-
|
105
|
+
|
106
106
|
# Try to get from RestMeta.GRAPHS
|
107
107
|
if graph and hasattr(queryset.model, 'RestMeta'):
|
108
108
|
rest_meta = queryset.model.RestMeta
|
@@ -111,18 +111,18 @@ class CsvFormatter:
|
|
111
111
|
graph_fields = graph_config.get('fields', [])
|
112
112
|
if graph_fields:
|
113
113
|
return self._prepare_field_config(graph_fields)
|
114
|
-
|
114
|
+
|
115
115
|
# Fallback to model fields
|
116
116
|
model_fields = [f.name for f in queryset.model._meta.fields]
|
117
117
|
return self._prepare_field_config(model_fields)
|
118
|
-
|
118
|
+
|
119
119
|
def _prepare_field_config(self, fields, headers=None):
|
120
120
|
"""
|
121
121
|
Prepare field configuration for CSV generation.
|
122
122
|
"""
|
123
123
|
field_names = []
|
124
124
|
field_headers = []
|
125
|
-
|
125
|
+
|
126
126
|
for i, field in enumerate(fields):
|
127
127
|
if isinstance(field, (tuple, list)):
|
128
128
|
field_name, display_name = field
|
@@ -130,33 +130,33 @@ class CsvFormatter:
|
|
130
130
|
field_headers.append(display_name)
|
131
131
|
else:
|
132
132
|
field_names.append(field)
|
133
|
-
field_headers.append(field.replace('_', ' ').title())
|
134
|
-
|
133
|
+
field_headers.append(field.replace('_', ' ').replace('.', ' ').title())
|
134
|
+
|
135
135
|
# Override with custom headers if provided
|
136
136
|
if headers:
|
137
137
|
field_headers = headers[:len(field_names)]
|
138
|
-
|
138
|
+
|
139
139
|
return {
|
140
140
|
'field_names': field_names,
|
141
141
|
'headers': field_headers
|
142
142
|
}
|
143
|
-
|
144
|
-
def _create_streaming_response(self, queryset, field_config, filename,
|
143
|
+
|
144
|
+
def _create_streaming_response(self, queryset, field_config, filename,
|
145
145
|
headers, localize):
|
146
146
|
"""
|
147
147
|
Create streaming HTTP response for large datasets.
|
148
148
|
"""
|
149
149
|
logger.info(f"Creating streaming CSV response for {queryset.count()} records")
|
150
|
-
|
150
|
+
|
151
151
|
def csv_generator():
|
152
152
|
# Create CSV writer with pseudo-buffer
|
153
|
-
pseudo_buffer =
|
153
|
+
pseudo_buffer = PseudoBuffer()
|
154
154
|
writer = csv.writer(pseudo_buffer, delimiter=self.delimiter,
|
155
155
|
quotechar=self.quotechar, quoting=self.quoting)
|
156
|
-
|
156
|
+
|
157
157
|
# Yield header row
|
158
158
|
yield writer.writerow(field_config['headers'])
|
159
|
-
|
159
|
+
|
160
160
|
# Yield data rows
|
161
161
|
for obj in queryset.iterator(): # Use iterator for memory efficiency
|
162
162
|
try:
|
@@ -166,13 +166,13 @@ class CsvFormatter:
|
|
166
166
|
logger.error(f"Error processing row for object {obj.pk}: {e}")
|
167
167
|
# Continue with next row instead of failing completely
|
168
168
|
continue
|
169
|
-
|
169
|
+
|
170
170
|
response = StreamingHttpResponse(csv_generator(), content_type='text/csv')
|
171
|
-
response['Content-Disposition'] = f'attachment; filename=
|
171
|
+
response['Content-Disposition'] = f'attachment; filename={filename}'
|
172
172
|
response['Cache-Control'] = 'no-cache'
|
173
173
|
return response
|
174
|
-
|
175
|
-
def _create_standard_response(self, queryset, field_config, filename,
|
174
|
+
|
175
|
+
def _create_standard_response(self, queryset, field_config, filename,
|
176
176
|
headers, localize):
|
177
177
|
"""
|
178
178
|
Create standard HTTP response for smaller datasets.
|
@@ -180,10 +180,10 @@ class CsvFormatter:
|
|
180
180
|
output = io.StringIO()
|
181
181
|
writer = csv.writer(output, delimiter=self.delimiter,
|
182
182
|
quotechar=self.quotechar, quoting=self.quoting)
|
183
|
-
|
183
|
+
|
184
184
|
# Write header
|
185
185
|
writer.writerow(field_config['headers'])
|
186
|
-
|
186
|
+
|
187
187
|
# Write data rows
|
188
188
|
for obj in queryset:
|
189
189
|
try:
|
@@ -192,25 +192,25 @@ class CsvFormatter:
|
|
192
192
|
except Exception as e:
|
193
193
|
logger.error(f"Error processing row for object {obj.pk}: {e}")
|
194
194
|
continue
|
195
|
-
|
195
|
+
|
196
196
|
response = HttpResponse(output.getvalue(), content_type='text/csv')
|
197
|
-
response['Content-Disposition'] = f'attachment; filename=
|
197
|
+
response['Content-Disposition'] = f'attachment; filename={filename}'
|
198
198
|
return response
|
199
|
-
|
199
|
+
|
200
200
|
def _create_empty_response(self, filename):
|
201
201
|
"""
|
202
202
|
Create response for empty dataset.
|
203
203
|
"""
|
204
204
|
response = HttpResponse('', content_type='text/csv')
|
205
|
-
response['Content-Disposition'] = f'attachment; filename=
|
205
|
+
response['Content-Disposition'] = f'attachment; filename={filename}'
|
206
206
|
return response
|
207
|
-
|
207
|
+
|
208
208
|
def _extract_row_data(self, obj, field_names, localize=None):
|
209
209
|
"""
|
210
210
|
Extract row data from an object based on field names.
|
211
211
|
"""
|
212
212
|
row = []
|
213
|
-
|
213
|
+
|
214
214
|
for field_name in field_names:
|
215
215
|
try:
|
216
216
|
value = self._get_field_value(obj, field_name)
|
@@ -219,69 +219,97 @@ class CsvFormatter:
|
|
219
219
|
except Exception as e:
|
220
220
|
logger.warning(f"Error extracting field '{field_name}': {e}")
|
221
221
|
row.append("N/A")
|
222
|
-
|
222
|
+
|
223
223
|
return row
|
224
|
-
|
224
|
+
|
225
225
|
def _get_field_value(self, obj, field_name):
|
226
226
|
"""
|
227
|
-
Get field value from object, supporting nested field access.
|
227
|
+
Get field value from object, supporting nested field access, foreign keys, and JSONField traversal.
|
228
228
|
"""
|
229
|
-
# Handle nested field access (e.g., "
|
229
|
+
# Handle nested field access (e.g., "parent.id", "parent.name", "metadata.defaults.role")
|
230
230
|
if '.' in field_name:
|
231
231
|
return self._get_nested_field_value(obj, field_name)
|
232
|
-
|
233
|
-
# Handle special metadata fields
|
234
|
-
if field_name.startswith('metadata.') and hasattr(obj, 'getProperty'):
|
235
|
-
parts = field_name.split('.', 2)
|
236
|
-
if len(parts) == 3:
|
237
|
-
return obj.getProperty(parts[2], category=parts[1])
|
238
|
-
elif len(parts) == 2:
|
239
|
-
return obj.getProperty(parts[1])
|
240
|
-
|
232
|
+
|
241
233
|
# Standard field access
|
242
234
|
if hasattr(obj, field_name):
|
243
235
|
value = getattr(obj, field_name)
|
244
236
|
return value() if callable(value) else value
|
245
|
-
|
237
|
+
|
246
238
|
# Dictionary-style access
|
247
239
|
if isinstance(obj, dict):
|
248
240
|
return obj.get(field_name, None)
|
249
|
-
|
241
|
+
|
250
242
|
return None
|
251
|
-
|
243
|
+
|
252
244
|
def _get_nested_field_value(self, obj, field_path):
|
253
245
|
"""
|
254
|
-
Get value from nested field path like "
|
246
|
+
Get value from nested field path like "parent.id", "parent.name", "metadata.defaults.role".
|
247
|
+
Uses robust field type detection for ForeignKey and JSONField traversal.
|
255
248
|
"""
|
256
249
|
try:
|
250
|
+
parts = field_path.split('.')
|
257
251
|
current = obj
|
258
|
-
|
252
|
+
|
253
|
+
for i, field_part in enumerate(parts):
|
259
254
|
if current is None:
|
260
255
|
return None
|
261
|
-
|
262
|
-
if
|
263
|
-
|
264
|
-
|
265
|
-
|
256
|
+
|
257
|
+
# Check if this is a model field using get_model_field
|
258
|
+
field = None
|
259
|
+
if hasattr(current, 'get_model_field'):
|
260
|
+
field = current.get_model_field(field_part)
|
261
|
+
elif hasattr(current.__class__, 'get_model_field'):
|
262
|
+
field = current.__class__.get_model_field(field_part)
|
263
|
+
|
264
|
+
if field:
|
265
|
+
if field.get_internal_type() == "ForeignKey":
|
266
|
+
# Handle foreign key field
|
267
|
+
current = getattr(current, field_part, None)
|
268
|
+
if current is None:
|
269
|
+
return None
|
270
|
+
elif field.get_internal_type() == "JSONField":
|
271
|
+
# Handle JSONField using jsonfield_as_objict
|
272
|
+
if hasattr(current, 'jsonfield_as_objict'):
|
273
|
+
json_obj = current.jsonfield_as_objict(field_part)
|
274
|
+
# Get remaining path for JSONField traversal
|
275
|
+
remaining_path = '.'.join(parts[i+1:])
|
276
|
+
if remaining_path:
|
277
|
+
return json_obj.get(remaining_path, "N/A")
|
278
|
+
else:
|
279
|
+
return json_obj
|
280
|
+
else:
|
281
|
+
# Fallback to direct access
|
282
|
+
current = getattr(current, field_part, {})
|
283
|
+
if not isinstance(current, dict):
|
284
|
+
return current
|
285
|
+
else:
|
286
|
+
# Regular field
|
287
|
+
current = getattr(current, field_part, None)
|
266
288
|
else:
|
267
|
-
|
268
|
-
|
289
|
+
# No model field found, try direct access
|
290
|
+
if hasattr(current, field_part):
|
291
|
+
current = getattr(current, field_part)
|
292
|
+
elif isinstance(current, dict):
|
293
|
+
current = current.get(field_part)
|
294
|
+
else:
|
295
|
+
return None
|
296
|
+
|
269
297
|
# Handle callable attributes
|
270
298
|
if callable(current):
|
271
299
|
current = current()
|
272
|
-
|
300
|
+
|
273
301
|
return current
|
274
302
|
except Exception as e:
|
275
303
|
logger.warning(f"Error accessing nested field '{field_path}': {e}")
|
276
304
|
return None
|
277
|
-
|
305
|
+
|
278
306
|
def _process_field_value(self, value, field_name, localize=None):
|
279
307
|
"""
|
280
308
|
Process field value with localization and special handling.
|
281
309
|
"""
|
282
310
|
if value is None:
|
283
311
|
return "N/A"
|
284
|
-
|
312
|
+
|
285
313
|
# Apply localization if configured
|
286
314
|
if localize and field_name in localize:
|
287
315
|
try:
|
@@ -290,7 +318,7 @@ class CsvFormatter:
|
|
290
318
|
localizer_name, extra = localizer_config.split('|', 1)
|
291
319
|
else:
|
292
320
|
localizer_name, extra = localizer_config, None
|
293
|
-
|
321
|
+
|
294
322
|
# Import and apply localizer
|
295
323
|
from mojo.serializers.formats.localizers import get_localizer
|
296
324
|
localizer = get_localizer(localizer_name)
|
@@ -298,42 +326,42 @@ class CsvFormatter:
|
|
298
326
|
return localizer(value, extra)
|
299
327
|
except Exception as e:
|
300
328
|
logger.warning(f"Localization failed for field '{field_name}': {e}")
|
301
|
-
|
329
|
+
|
302
330
|
return value
|
303
|
-
|
331
|
+
|
304
332
|
def _format_csv_value(self, value):
|
305
333
|
"""
|
306
334
|
Format value for CSV output.
|
307
335
|
"""
|
308
336
|
if value is None:
|
309
337
|
return ""
|
310
|
-
|
338
|
+
|
311
339
|
# Handle model instances
|
312
340
|
if hasattr(value, 'pk'):
|
313
341
|
return str(value.pk)
|
314
|
-
|
342
|
+
|
315
343
|
# Handle datetime objects
|
316
344
|
elif isinstance(value, datetime):
|
317
345
|
return value.strftime('%Y-%m-%d %H:%M:%S')
|
318
346
|
elif isinstance(value, date):
|
319
347
|
return value.strftime('%Y-%m-%d')
|
320
|
-
|
348
|
+
|
321
349
|
# Handle numeric types
|
322
350
|
elif isinstance(value, Decimal):
|
323
351
|
return str(float(value)) if not value.is_nan() else "0"
|
324
352
|
elif isinstance(value, (int, float)):
|
325
353
|
return str(value)
|
326
|
-
|
354
|
+
|
327
355
|
# Handle collections
|
328
356
|
elif isinstance(value, (list, tuple)):
|
329
357
|
return '; '.join(str(item) for item in value)
|
330
358
|
elif isinstance(value, dict):
|
331
359
|
return str(value) # Could be enhanced with better dict formatting
|
332
|
-
|
360
|
+
|
333
361
|
# Default string conversion
|
334
362
|
else:
|
335
363
|
return str(value)
|
336
|
-
|
364
|
+
|
337
365
|
def _auto_detect_fields(self, sample_item):
|
338
366
|
"""
|
339
367
|
Auto-detect fields from a sample data item.
|
@@ -348,69 +376,18 @@ class CsvFormatter:
|
|
348
376
|
return ['value'] # Fallback for primitive types
|
349
377
|
|
350
378
|
|
351
|
-
class
|
379
|
+
class PseudoBuffer:
|
352
380
|
"""
|
353
|
-
A
|
381
|
+
A buffer for streaming CSV generation.
|
354
382
|
"""
|
355
|
-
|
383
|
+
|
356
384
|
def writerow(self, row):
|
357
385
|
"""Write the row by returning it as a CSV line."""
|
358
386
|
output = io.StringIO()
|
359
387
|
writer = csv.writer(output)
|
360
388
|
writer.writerow(row)
|
361
389
|
return output.getvalue()
|
362
|
-
|
390
|
+
|
363
391
|
def write(self, value):
|
364
392
|
"""Write the value by returning it directly."""
|
365
393
|
return value
|
366
|
-
|
367
|
-
|
368
|
-
# Convenience functions for backwards compatibility
|
369
|
-
def generate_csv(queryset, fields, filename, headers=None, localize=None, stream=True):
|
370
|
-
"""
|
371
|
-
Generate CSV from queryset.
|
372
|
-
|
373
|
-
:param queryset: Django QuerySet
|
374
|
-
:param fields: List of field names
|
375
|
-
:param filename: Output filename
|
376
|
-
:param headers: Custom header names
|
377
|
-
:param localize: Localization config
|
378
|
-
:param stream: Enable streaming for large datasets
|
379
|
-
:return: HttpResponse or StreamingHttpResponse
|
380
|
-
"""
|
381
|
-
formatter = CsvFormatter()
|
382
|
-
return formatter.serialize_queryset(
|
383
|
-
queryset=queryset,
|
384
|
-
fields=fields,
|
385
|
-
filename=filename,
|
386
|
-
headers=headers,
|
387
|
-
localize=localize,
|
388
|
-
stream=stream
|
389
|
-
)
|
390
|
-
|
391
|
-
|
392
|
-
def generate_csv_stream(queryset, fields, filename, localize=None):
|
393
|
-
"""
|
394
|
-
Generate streaming CSV response.
|
395
|
-
"""
|
396
|
-
formatter = CsvFormatter()
|
397
|
-
return formatter.serialize_queryset(
|
398
|
-
queryset=queryset,
|
399
|
-
fields=fields,
|
400
|
-
filename=filename,
|
401
|
-
localize=localize,
|
402
|
-
stream=True
|
403
|
-
)
|
404
|
-
|
405
|
-
|
406
|
-
def serialize_to_csv(data, fields=None, filename="export.csv", headers=None):
|
407
|
-
"""
|
408
|
-
Serialize list of data to CSV.
|
409
|
-
"""
|
410
|
-
formatter = CsvFormatter()
|
411
|
-
return formatter.serialize_data(
|
412
|
-
data=data,
|
413
|
-
fields=fields,
|
414
|
-
filename=filename,
|
415
|
-
headers=headers
|
416
|
-
)
|