django-nativemojo 0.1.15__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.
Files changed (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +281 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /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="{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 = EchoWriter()
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="{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="{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="{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., "user.email", "profile.address.city")
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 "user.profile.name".
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
- for field_part in field_path.split('.'):
252
+
253
+ for i, field_part in enumerate(parts):
259
254
  if current is None:
260
255
  return None
261
-
262
- if hasattr(current, field_part):
263
- current = getattr(current, field_part)
264
- elif isinstance(current, dict):
265
- current = current.get(field_part)
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
- return None
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 EchoWriter:
379
+ class PseudoBuffer:
352
380
  """
353
- A writer that implements just the write method for streaming CSV generation.
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
- )