django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__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.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- 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 +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- 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 +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- 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 +7 -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/s3.py +64 -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/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- 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 +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- 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/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- 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 +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -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 +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- 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/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- 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/bounce.py +0 -0
- 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 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,393 @@
|
|
1
|
+
import csv
|
2
|
+
import io
|
3
|
+
from decimal import Decimal
|
4
|
+
from datetime import datetime, date
|
5
|
+
from django.http import StreamingHttpResponse, HttpResponse
|
6
|
+
from django.db.models import QuerySet
|
7
|
+
from mojo.helpers import logit
|
8
|
+
|
9
|
+
logger = logit.get_logger("csv_formatter", "csv_formatter.log")
|
10
|
+
|
11
|
+
|
12
|
+
class CsvFormatter:
|
13
|
+
"""
|
14
|
+
Advanced CSV formatter with streaming support and RestMeta.GRAPHS integration.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(self, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL,
|
18
|
+
encoding='utf-8', streaming_threshold=1000):
|
19
|
+
"""
|
20
|
+
Initialize CSV formatter.
|
21
|
+
|
22
|
+
:param delimiter: Field delimiter (default comma)
|
23
|
+
:param quotechar: Quote character for fields containing special chars
|
24
|
+
:param quoting: Quoting behavior (csv.QUOTE_MINIMAL, etc.)
|
25
|
+
:param encoding: Character encoding for output
|
26
|
+
:param streaming_threshold: Minimum rows to trigger streaming response
|
27
|
+
"""
|
28
|
+
self.delimiter = delimiter
|
29
|
+
self.quotechar = quotechar
|
30
|
+
self.quoting = quoting
|
31
|
+
self.encoding = encoding
|
32
|
+
self.streaming_threshold = streaming_threshold
|
33
|
+
|
34
|
+
def serialize_queryset(self, queryset, fields=None, graph=None, filename="export.csv",
|
35
|
+
headers=None, localize=None, stream=True):
|
36
|
+
"""
|
37
|
+
Serialize a Django QuerySet to CSV format.
|
38
|
+
|
39
|
+
:param queryset: Django QuerySet to serialize
|
40
|
+
:param fields: List of field names or tuples (field_name, display_name)
|
41
|
+
:param graph: RestMeta graph name to use for field configuration
|
42
|
+
:param filename: Output filename
|
43
|
+
:param headers: Custom header names (overrides field names)
|
44
|
+
:param localize: Localization configuration
|
45
|
+
:param stream: Enable streaming for large datasets
|
46
|
+
:return: HttpResponse or StreamingHttpResponse
|
47
|
+
"""
|
48
|
+
# Determine if we should stream based on queryset size
|
49
|
+
should_stream = stream and queryset.count() > self.streaming_threshold
|
50
|
+
|
51
|
+
# Get fields configuration
|
52
|
+
field_config = self._get_field_config(queryset, fields, graph)
|
53
|
+
|
54
|
+
if should_stream:
|
55
|
+
return self._create_streaming_response(queryset, field_config, filename,
|
56
|
+
headers, localize)
|
57
|
+
else:
|
58
|
+
return self._create_standard_response(queryset, field_config, filename,
|
59
|
+
headers, localize)
|
60
|
+
|
61
|
+
def serialize_data(self, data, fields=None, filename="export.csv", headers=None):
|
62
|
+
"""
|
63
|
+
Serialize list of dictionaries or objects to CSV.
|
64
|
+
|
65
|
+
:param data: List of dictionaries or objects
|
66
|
+
:param fields: Field names to include
|
67
|
+
:param filename: Output filename
|
68
|
+
:param headers: Custom header names
|
69
|
+
:return: HttpResponse
|
70
|
+
"""
|
71
|
+
if not data:
|
72
|
+
return self._create_empty_response(filename)
|
73
|
+
|
74
|
+
# Auto-detect fields if not provided
|
75
|
+
if not fields:
|
76
|
+
fields = self._auto_detect_fields(data[0])
|
77
|
+
|
78
|
+
# Prepare field configuration
|
79
|
+
field_config = self._prepare_field_config(fields, headers)
|
80
|
+
|
81
|
+
# Generate CSV content
|
82
|
+
output = io.StringIO()
|
83
|
+
writer = csv.writer(output, delimiter=self.delimiter,
|
84
|
+
quotechar=self.quotechar, quoting=self.quoting)
|
85
|
+
|
86
|
+
# Write header
|
87
|
+
writer.writerow(field_config['headers'])
|
88
|
+
|
89
|
+
# Write data rows
|
90
|
+
for item in data:
|
91
|
+
row = self._extract_row_data(item, field_config['field_names'])
|
92
|
+
writer.writerow(row)
|
93
|
+
|
94
|
+
# Create response
|
95
|
+
response = HttpResponse(output.getvalue(), content_type='text/csv')
|
96
|
+
response['Content-Disposition'] = f'attachment; filename={filename}'
|
97
|
+
return response
|
98
|
+
|
99
|
+
def _get_field_config(self, queryset, fields, graph):
|
100
|
+
"""
|
101
|
+
Get field configuration from various sources.
|
102
|
+
"""
|
103
|
+
if fields:
|
104
|
+
return self._prepare_field_config(fields)
|
105
|
+
|
106
|
+
# Try to get from RestMeta.GRAPHS
|
107
|
+
if graph and hasattr(queryset.model, 'RestMeta'):
|
108
|
+
rest_meta = queryset.model.RestMeta
|
109
|
+
if hasattr(rest_meta, 'GRAPHS') and graph in rest_meta.GRAPHS:
|
110
|
+
graph_config = rest_meta.GRAPHS[graph]
|
111
|
+
graph_fields = graph_config.get('fields', [])
|
112
|
+
if graph_fields:
|
113
|
+
return self._prepare_field_config(graph_fields)
|
114
|
+
|
115
|
+
# Fallback to model fields
|
116
|
+
model_fields = [f.name for f in queryset.model._meta.fields]
|
117
|
+
return self._prepare_field_config(model_fields)
|
118
|
+
|
119
|
+
def _prepare_field_config(self, fields, headers=None):
|
120
|
+
"""
|
121
|
+
Prepare field configuration for CSV generation.
|
122
|
+
"""
|
123
|
+
field_names = []
|
124
|
+
field_headers = []
|
125
|
+
|
126
|
+
for i, field in enumerate(fields):
|
127
|
+
if isinstance(field, (tuple, list)):
|
128
|
+
field_name, display_name = field
|
129
|
+
field_names.append(field_name)
|
130
|
+
field_headers.append(display_name)
|
131
|
+
else:
|
132
|
+
field_names.append(field)
|
133
|
+
field_headers.append(field.replace('_', ' ').replace('.', ' ').title())
|
134
|
+
|
135
|
+
# Override with custom headers if provided
|
136
|
+
if headers:
|
137
|
+
field_headers = headers[:len(field_names)]
|
138
|
+
|
139
|
+
return {
|
140
|
+
'field_names': field_names,
|
141
|
+
'headers': field_headers
|
142
|
+
}
|
143
|
+
|
144
|
+
def _create_streaming_response(self, queryset, field_config, filename,
|
145
|
+
headers, localize):
|
146
|
+
"""
|
147
|
+
Create streaming HTTP response for large datasets.
|
148
|
+
"""
|
149
|
+
logger.info(f"Creating streaming CSV response for {queryset.count()} records")
|
150
|
+
|
151
|
+
def csv_generator():
|
152
|
+
# Create CSV writer with pseudo-buffer
|
153
|
+
pseudo_buffer = PseudoBuffer()
|
154
|
+
writer = csv.writer(pseudo_buffer, delimiter=self.delimiter,
|
155
|
+
quotechar=self.quotechar, quoting=self.quoting)
|
156
|
+
|
157
|
+
# Yield header row
|
158
|
+
yield writer.writerow(field_config['headers'])
|
159
|
+
|
160
|
+
# Yield data rows
|
161
|
+
for obj in queryset.iterator(): # Use iterator for memory efficiency
|
162
|
+
try:
|
163
|
+
row = self._extract_row_data(obj, field_config['field_names'], localize)
|
164
|
+
yield writer.writerow(row)
|
165
|
+
except Exception as e:
|
166
|
+
logger.error(f"Error processing row for object {obj.pk}: {e}")
|
167
|
+
# Continue with next row instead of failing completely
|
168
|
+
continue
|
169
|
+
|
170
|
+
response = StreamingHttpResponse(csv_generator(), content_type='text/csv')
|
171
|
+
response['Content-Disposition'] = f'attachment; filename={filename}'
|
172
|
+
response['Cache-Control'] = 'no-cache'
|
173
|
+
return response
|
174
|
+
|
175
|
+
def _create_standard_response(self, queryset, field_config, filename,
|
176
|
+
headers, localize):
|
177
|
+
"""
|
178
|
+
Create standard HTTP response for smaller datasets.
|
179
|
+
"""
|
180
|
+
output = io.StringIO()
|
181
|
+
writer = csv.writer(output, delimiter=self.delimiter,
|
182
|
+
quotechar=self.quotechar, quoting=self.quoting)
|
183
|
+
|
184
|
+
# Write header
|
185
|
+
writer.writerow(field_config['headers'])
|
186
|
+
|
187
|
+
# Write data rows
|
188
|
+
for obj in queryset:
|
189
|
+
try:
|
190
|
+
row = self._extract_row_data(obj, field_config['field_names'], localize)
|
191
|
+
writer.writerow(row)
|
192
|
+
except Exception as e:
|
193
|
+
logger.error(f"Error processing row for object {obj.pk}: {e}")
|
194
|
+
continue
|
195
|
+
|
196
|
+
response = HttpResponse(output.getvalue(), content_type='text/csv')
|
197
|
+
response['Content-Disposition'] = f'attachment; filename={filename}'
|
198
|
+
return response
|
199
|
+
|
200
|
+
def _create_empty_response(self, filename):
|
201
|
+
"""
|
202
|
+
Create response for empty dataset.
|
203
|
+
"""
|
204
|
+
response = HttpResponse('', content_type='text/csv')
|
205
|
+
response['Content-Disposition'] = f'attachment; filename={filename}'
|
206
|
+
return response
|
207
|
+
|
208
|
+
def _extract_row_data(self, obj, field_names, localize=None):
|
209
|
+
"""
|
210
|
+
Extract row data from an object based on field names.
|
211
|
+
"""
|
212
|
+
row = []
|
213
|
+
|
214
|
+
for field_name in field_names:
|
215
|
+
try:
|
216
|
+
value = self._get_field_value(obj, field_name)
|
217
|
+
value = self._process_field_value(value, field_name, localize)
|
218
|
+
row.append(self._format_csv_value(value))
|
219
|
+
except Exception as e:
|
220
|
+
logger.warning(f"Error extracting field '{field_name}': {e}")
|
221
|
+
row.append("N/A")
|
222
|
+
|
223
|
+
return row
|
224
|
+
|
225
|
+
def _get_field_value(self, obj, field_name):
|
226
|
+
"""
|
227
|
+
Get field value from object, supporting nested field access, foreign keys, and JSONField traversal.
|
228
|
+
"""
|
229
|
+
# Handle nested field access (e.g., "parent.id", "parent.name", "metadata.defaults.role")
|
230
|
+
if '.' in field_name:
|
231
|
+
return self._get_nested_field_value(obj, field_name)
|
232
|
+
|
233
|
+
# Standard field access
|
234
|
+
if hasattr(obj, field_name):
|
235
|
+
value = getattr(obj, field_name)
|
236
|
+
return value() if callable(value) else value
|
237
|
+
|
238
|
+
# Dictionary-style access
|
239
|
+
if isinstance(obj, dict):
|
240
|
+
return obj.get(field_name, None)
|
241
|
+
|
242
|
+
return None
|
243
|
+
|
244
|
+
def _get_nested_field_value(self, obj, field_path):
|
245
|
+
"""
|
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.
|
248
|
+
"""
|
249
|
+
try:
|
250
|
+
parts = field_path.split('.')
|
251
|
+
current = obj
|
252
|
+
|
253
|
+
for i, field_part in enumerate(parts):
|
254
|
+
if current is None:
|
255
|
+
return None
|
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)
|
288
|
+
else:
|
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
|
+
|
297
|
+
# Handle callable attributes
|
298
|
+
if callable(current):
|
299
|
+
current = current()
|
300
|
+
|
301
|
+
return current
|
302
|
+
except Exception as e:
|
303
|
+
logger.warning(f"Error accessing nested field '{field_path}': {e}")
|
304
|
+
return None
|
305
|
+
|
306
|
+
def _process_field_value(self, value, field_name, localize=None):
|
307
|
+
"""
|
308
|
+
Process field value with localization and special handling.
|
309
|
+
"""
|
310
|
+
if value is None:
|
311
|
+
return "N/A"
|
312
|
+
|
313
|
+
# Apply localization if configured
|
314
|
+
if localize and field_name in localize:
|
315
|
+
try:
|
316
|
+
localizer_config = localize[field_name]
|
317
|
+
if '|' in localizer_config:
|
318
|
+
localizer_name, extra = localizer_config.split('|', 1)
|
319
|
+
else:
|
320
|
+
localizer_name, extra = localizer_config, None
|
321
|
+
|
322
|
+
# Import and apply localizer
|
323
|
+
from mojo.serializers.formats.localizers import get_localizer
|
324
|
+
localizer = get_localizer(localizer_name)
|
325
|
+
if localizer:
|
326
|
+
return localizer(value, extra)
|
327
|
+
except Exception as e:
|
328
|
+
logger.warning(f"Localization failed for field '{field_name}': {e}")
|
329
|
+
|
330
|
+
return value
|
331
|
+
|
332
|
+
def _format_csv_value(self, value):
|
333
|
+
"""
|
334
|
+
Format value for CSV output.
|
335
|
+
"""
|
336
|
+
if value is None:
|
337
|
+
return ""
|
338
|
+
|
339
|
+
# Handle model instances
|
340
|
+
if hasattr(value, 'pk'):
|
341
|
+
return str(value.pk)
|
342
|
+
|
343
|
+
# Handle datetime objects
|
344
|
+
elif isinstance(value, datetime):
|
345
|
+
return value.strftime('%Y-%m-%d %H:%M:%S')
|
346
|
+
elif isinstance(value, date):
|
347
|
+
return value.strftime('%Y-%m-%d')
|
348
|
+
|
349
|
+
# Handle numeric types
|
350
|
+
elif isinstance(value, Decimal):
|
351
|
+
return str(float(value)) if not value.is_nan() else "0"
|
352
|
+
elif isinstance(value, (int, float)):
|
353
|
+
return str(value)
|
354
|
+
|
355
|
+
# Handle collections
|
356
|
+
elif isinstance(value, (list, tuple)):
|
357
|
+
return '; '.join(str(item) for item in value)
|
358
|
+
elif isinstance(value, dict):
|
359
|
+
return str(value) # Could be enhanced with better dict formatting
|
360
|
+
|
361
|
+
# Default string conversion
|
362
|
+
else:
|
363
|
+
return str(value)
|
364
|
+
|
365
|
+
def _auto_detect_fields(self, sample_item):
|
366
|
+
"""
|
367
|
+
Auto-detect fields from a sample data item.
|
368
|
+
"""
|
369
|
+
if isinstance(sample_item, dict):
|
370
|
+
return list(sample_item.keys())
|
371
|
+
elif hasattr(sample_item, '_meta'):
|
372
|
+
return [f.name for f in sample_item._meta.fields]
|
373
|
+
elif hasattr(sample_item, '__dict__'):
|
374
|
+
return list(sample_item.__dict__.keys())
|
375
|
+
else:
|
376
|
+
return ['value'] # Fallback for primitive types
|
377
|
+
|
378
|
+
|
379
|
+
class PseudoBuffer:
|
380
|
+
"""
|
381
|
+
A buffer for streaming CSV generation.
|
382
|
+
"""
|
383
|
+
|
384
|
+
def writerow(self, row):
|
385
|
+
"""Write the row by returning it as a CSV line."""
|
386
|
+
output = io.StringIO()
|
387
|
+
writer = csv.writer(output)
|
388
|
+
writer.writerow(row)
|
389
|
+
return output.getvalue()
|
390
|
+
|
391
|
+
def write(self, value):
|
392
|
+
"""Write the value by returning it directly."""
|
393
|
+
return value
|