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
@@ -1,568 +0,0 @@
1
- # Try to import ujson for better performance, fallback to standard json
2
- try:
3
- import ujson
4
- HAS_UJSON = True
5
- except ImportError:
6
- HAS_UJSON = False
7
-
8
- import json
9
- import time
10
- import datetime
11
- import math
12
- from decimal import Decimal
13
- from functools import wraps
14
- from itertools import chain
15
-
16
- from django.db.models import ForeignKey, OneToOneField, ManyToOneRel, ManyToManyField, F
17
- from django.db.models import QuerySet
18
- from django.core.exceptions import FieldDoesNotExist
19
- from django.http import HttpResponse
20
-
21
- from mojo.helpers import logit
22
-
23
- logger = logit.get_logger("advanced_serializer", "advanced_serializer.log")
24
-
25
- # Cache for expensive operations
26
- SERIALIZER_CACHE = {}
27
-
28
- def timeit(func):
29
- """Decorator to time function execution."""
30
- @wraps(func)
31
- def wrapper(*args, **kwargs):
32
- start_time = time.perf_counter()
33
- result = func(*args, **kwargs)
34
- end_time = time.perf_counter()
35
- total_time = end_time - start_time
36
- logger.info(f'Function {func.__name__} took {total_time:.4f} seconds')
37
- return result
38
- return wrapper
39
-
40
-
41
- class AdvancedGraphSerializer:
42
- """
43
- Advanced serializer for Django models and QuerySets with comprehensive features:
44
- - RestMeta.GRAPHS configuration support
45
- - Multiple output formats (JSON, CSV, Excel, HTML)
46
- - Caching and performance optimizations
47
- - Nested relationships and custom fields
48
- - Pagination and sorting for collections
49
- """
50
-
51
- def __init__(self, instance, graph="default", many=False, request=None, **kwargs):
52
- """
53
- :param instance: Model instance or QuerySet
54
- :param graph: The graph type to use (e.g., "default", "list", "detail")
55
- :param many: Boolean, if True, serializes a QuerySet
56
- :param request: Django request object for context
57
- :param kwargs: Additional options (cache, format, etc.)
58
- """
59
- self.graph = graph
60
- self.request = request
61
- self.cache = kwargs.get('cache', {})
62
- self.format = kwargs.get('format', 'json')
63
- self.qset = None
64
-
65
- # Handle QuerySet vs single instance
66
- if isinstance(instance, QuerySet):
67
- self.many = True
68
- self.qset = instance
69
- self.instance = instance # Keep as QuerySet for lazy evaluation
70
- else:
71
- self.many = many
72
- self.instance = instance if many else instance
73
- if many and not isinstance(instance, (list, tuple)):
74
- self.instance = [instance]
75
-
76
- @timeit
77
- def serialize(self):
78
- """
79
- Main serialization method that routes to appropriate handler.
80
- """
81
- if self.many:
82
- if isinstance(self.instance, QuerySet):
83
- return self._serialize_queryset(self.instance)
84
- else:
85
- return [self._serialize_instance(obj) for obj in self.instance]
86
- return self._serialize_instance(self.instance)
87
-
88
- def _serialize_queryset(self, qset):
89
- """
90
- Serialize a QuerySet with optimizations.
91
- """
92
- # Apply select_related optimizations if available
93
- if hasattr(qset, 'model') and hasattr(qset.model, 'RestMeta'):
94
- select_related_fields = self._get_select_related_fields(qset.model)
95
- if select_related_fields:
96
- qset = qset.select_related(*select_related_fields)
97
- logger.info(f"Applied select_related: {select_related_fields}")
98
-
99
- return [self._serialize_instance(obj) for obj in qset]
100
-
101
- def _get_select_related_fields(self, model):
102
- """
103
- Get fields that should be select_related for performance.
104
- """
105
- if not hasattr(model, 'RestMeta') or not hasattr(model.RestMeta, 'GRAPHS'):
106
- return []
107
-
108
- graph_config = model.RestMeta.GRAPHS.get(self.graph, {})
109
- fields = graph_config.get('fields', [])
110
- graphs = graph_config.get('graphs', {})
111
-
112
- select_fields = []
113
- for field_name in chain(fields, graphs.keys()):
114
- try:
115
- field = model._meta.get_field(field_name)
116
- if isinstance(field, (ForeignKey, OneToOneField)):
117
- select_fields.append(field_name)
118
- except FieldDoesNotExist:
119
- continue
120
-
121
- return select_fields
122
-
123
- def _serialize_instance(self, obj):
124
- """
125
- Serialize a single model instance using RestMeta.GRAPHS configuration.
126
- """
127
- # Check cache first
128
- cache_key = self._get_cache_key(obj)
129
- if cache_key in self.cache:
130
- return self.cache[cache_key]
131
-
132
- if not hasattr(obj, "RestMeta") or not hasattr(obj.RestMeta, "GRAPHS"):
133
- logger.warning(f"RestMeta.GRAPHS not found for {obj.__class__.__name__}")
134
- return self._fallback_serialization(obj)
135
-
136
- graph_config = obj.RestMeta.GRAPHS.get(self.graph)
137
- if graph_config is None:
138
- if self.graph != "default":
139
- logger.info(f"Graph '{self.graph}' not found, falling back to 'default'")
140
- self.graph = "default"
141
- graph_config = obj.RestMeta.GRAPHS.get("default")
142
-
143
- if graph_config is None:
144
- logger.warning(f"No graph configuration found for {obj.__class__.__name__}")
145
- return self._fallback_serialization(obj)
146
-
147
- logger.debug(f"Serializing {obj.__class__.__name__} with graph '{self.graph}': {graph_config}")
148
-
149
- # Start with basic field serialization
150
- data = self._serialize_fields(obj, graph_config.get("fields", []))
151
-
152
- # Add extra fields (methods, properties, etc.)
153
- self._add_extra_fields(obj, data, graph_config.get("extra", []))
154
-
155
- # Process related object graphs
156
- self._add_related_graphs(obj, data, graph_config.get("graphs", {}))
157
-
158
- # Cache the result
159
- if cache_key:
160
- self.cache[cache_key] = data
161
-
162
- return data
163
-
164
- def _get_cache_key(self, obj):
165
- """Generate cache key for an object."""
166
- if hasattr(obj, 'pk') and obj.pk:
167
- return f"{obj.__class__.__name__}_{obj.pk}_{self.graph}"
168
- return None
169
-
170
- def _fallback_serialization(self, obj):
171
- """Fallback when no RestMeta.GRAPHS is available."""
172
- if hasattr(obj, '_meta'):
173
- fields = [field.name for field in obj._meta.fields]
174
- return self._serialize_fields(obj, fields)
175
- return str(obj)
176
-
177
- def _serialize_fields(self, obj, fields):
178
- """
179
- Serialize basic model fields.
180
- """
181
- data = {}
182
-
183
- # If no fields specified, get all model fields
184
- if not fields:
185
- if hasattr(obj, '_meta'):
186
- fields = [field.name for field in obj._meta.fields]
187
- else:
188
- return {}
189
-
190
- for field_name in fields:
191
- try:
192
- field_value = getattr(obj, field_name)
193
- field_obj = self._get_model_field(obj, field_name)
194
-
195
- # Handle callable attributes
196
- if callable(field_value):
197
- try:
198
- field_value = field_value()
199
- except Exception as e:
200
- logger.warning(f"Error calling {field_name}: {e}")
201
- continue
202
-
203
- # Serialize the value based on type
204
- data[field_name] = self._serialize_value(field_value, field_obj)
205
-
206
- except AttributeError:
207
- logger.warning(f"Field '{field_name}' not found on {obj.__class__.__name__}")
208
- continue
209
- except Exception as e:
210
- logger.error(f"Error serializing field '{field_name}': {e}")
211
- data[field_name] = None
212
-
213
- return data
214
-
215
- def _add_extra_fields(self, obj, data, extra_fields):
216
- """
217
- Add extra fields (methods, properties, computed values).
218
- """
219
- for field in extra_fields:
220
- if isinstance(field, (tuple, list)):
221
- method_name, alias = field
222
- else:
223
- method_name, alias = field, field
224
-
225
- try:
226
- if hasattr(obj, method_name):
227
- attr = getattr(obj, method_name)
228
- value = attr() if callable(attr) else attr
229
- data[alias] = self._serialize_value(value)
230
- logger.debug(f"Added extra field '{method_name}' as '{alias}'")
231
- else:
232
- logger.warning(f"Extra field '{method_name}' not found on {obj.__class__.__name__}")
233
- except Exception as e:
234
- logger.error(f"Error processing extra field '{method_name}': {e}")
235
- data[alias] = None
236
-
237
- def _add_related_graphs(self, obj, data, related_graphs):
238
- """
239
- Process related object serialization using sub-graphs.
240
- """
241
- for field_name, sub_graph in related_graphs.items():
242
- try:
243
- related_obj = getattr(obj, field_name, None)
244
- if related_obj is None:
245
- data[field_name] = None
246
- continue
247
-
248
- field_obj = self._get_model_field(obj, field_name)
249
-
250
- if isinstance(field_obj, (ForeignKey, OneToOneField)):
251
- # Single related object
252
- logger.debug(f"Serializing related field '{field_name}' with graph '{sub_graph}'")
253
- data[field_name] = AdvancedGraphSerializer(
254
- related_obj,
255
- graph=sub_graph,
256
- cache=self.cache
257
- ).serialize()
258
-
259
- elif isinstance(field_obj, (ManyToManyField, ManyToOneRel)) or hasattr(related_obj, 'all'):
260
- # Many-to-many or reverse foreign key relationship
261
- if hasattr(related_obj, 'all'):
262
- related_qset = related_obj.all()
263
- logger.debug(f"Serializing many-to-many field '{field_name}' with graph '{sub_graph}'")
264
- data[field_name] = AdvancedGraphSerializer(
265
- related_qset,
266
- graph=sub_graph,
267
- many=True,
268
- cache=self.cache
269
- ).serialize()
270
- else:
271
- data[field_name] = []
272
- else:
273
- logger.warning(f"Unsupported field type for '{field_name}': {type(field_obj)}")
274
- data[field_name] = str(related_obj)
275
-
276
- except Exception as e:
277
- logger.error(f"Error processing related field '{field_name}': {e}")
278
- data[field_name] = None
279
-
280
- def _get_model_field(self, obj, field_name):
281
- """Get Django model field object."""
282
- try:
283
- if hasattr(obj, '_meta'):
284
- return obj._meta.get_field(field_name)
285
- except FieldDoesNotExist:
286
- pass
287
- return None
288
-
289
- def _serialize_value(self, value, field_obj=None):
290
- """
291
- Serialize individual values with type-specific handling.
292
- """
293
- if value is None:
294
- return None
295
-
296
- # Handle datetime objects
297
- if isinstance(value, datetime.datetime):
298
- return int(value.timestamp())
299
- elif isinstance(value, datetime.date):
300
- return value.isoformat()
301
-
302
- # Handle numeric types
303
- elif isinstance(value, Decimal):
304
- return 0.0 if value.is_nan() else float(value)
305
- elif isinstance(value, float):
306
- return 0.0 if math.isnan(value) else value
307
-
308
- # Handle related objects
309
- elif hasattr(value, 'pk'):
310
- if isinstance(field_obj, (ForeignKey, OneToOneField)):
311
- return value.pk
312
- return str(value)
313
-
314
- # Handle collections
315
- elif isinstance(value, (list, tuple)):
316
- return [self._serialize_value(v) for v in value]
317
- elif isinstance(value, dict):
318
- return {k: self._serialize_value(v) for k, v in value.items()}
319
-
320
- # Handle other types
321
- elif isinstance(value, (str, int, bool)):
322
- return value
323
- else:
324
- return str(value)
325
-
326
- def to_json(self, **kwargs):
327
- """
328
- Convert serialized data to JSON string.
329
- """
330
- data = self.serialize()
331
-
332
- if self.many:
333
- response_data = {
334
- 'data': data,
335
- 'status': True,
336
- 'size': len(data),
337
- 'graph': self.graph
338
- }
339
- else:
340
- response_data = {
341
- 'data': data,
342
- 'status': True,
343
- 'graph': self.graph
344
- }
345
-
346
- # Add any additional kwargs
347
- response_data.update(kwargs)
348
-
349
- try:
350
- if HAS_UJSON:
351
- return ujson.dumps(response_data)
352
- else:
353
- return json.dumps(response_data, cls=ExtendedJSONEncoder)
354
- except Exception as e:
355
- logger.error(f"JSON serialization error: {e}")
356
- # Fallback to standard json
357
- return json.dumps(response_data, cls=ExtendedJSONEncoder)
358
-
359
- def to_response(self, request=None, **kwargs):
360
- """
361
- Return appropriate HTTP response based on request headers.
362
- """
363
- request = request or self.request
364
-
365
- if not request:
366
- return HttpResponse(self.to_json(**kwargs), content_type='application/json')
367
-
368
- # Determine response format from request
369
- accept_header = request.headers.get('Accept', '')
370
-
371
- if 'application/json' in accept_header:
372
- return HttpResponse(self.to_json(**kwargs), content_type='application/json')
373
- elif 'text/html' in accept_header:
374
- return self._render_html_response(request, **kwargs)
375
- else:
376
- return HttpResponse(self.to_json(**kwargs), content_type='application/json')
377
-
378
- def _render_html_response(self, request, **kwargs):
379
- """
380
- Render HTML response for debugging/viewing.
381
- """
382
- data = self.serialize()
383
- if self.many:
384
- response_data = {'data': data, 'status': True, 'size': len(data), 'graph': self.graph}
385
- else:
386
- response_data = {'data': data, 'status': True, 'graph': self.graph}
387
-
388
- response_data.update(kwargs)
389
-
390
- # Use pretty JSON for HTML display
391
- json_output = json.dumps(response_data, cls=ExtendedJSONEncoder, indent=4, sort_keys=True)
392
-
393
- html_content = f"""
394
- <html>
395
- <head>
396
- <title>API Response</title>
397
- <style>
398
- body {{ font-family: monospace; margin: 20px; }}
399
- pre {{ background: #f5f5f5; padding: 20px; border-radius: 5px; }}
400
- </style>
401
- </head>
402
- <body>
403
- <h1>API Response</h1>
404
- <pre>{json_output}</pre>
405
- </body>
406
- </html>
407
- """
408
-
409
- return HttpResponse(html_content, content_type='text/html')
410
-
411
-
412
- class ExtendedJSONEncoder(json.JSONEncoder):
413
- """JSON encoder that handles Django model types."""
414
-
415
- def default(self, obj):
416
- if isinstance(obj, datetime.datetime):
417
- return int(obj.timestamp())
418
- elif isinstance(obj, datetime.date):
419
- return obj.isoformat()
420
- elif isinstance(obj, Decimal):
421
- return 0.0 if obj.is_nan() else float(obj)
422
- elif isinstance(obj, float) and math.isnan(obj):
423
- return 0.0
424
- elif hasattr(obj, 'pk'):
425
- return obj.pk
426
- return super().default(obj)
427
-
428
-
429
- class CollectionSerializer:
430
- """
431
- Advanced collection serializer with pagination, sorting, and filtering.
432
- """
433
-
434
- def __init__(self, queryset, graph="list", request=None, **kwargs):
435
- self.queryset = queryset
436
- self.graph = graph
437
- self.request = request
438
- self.size = kwargs.get('size', 25)
439
- self.start = kwargs.get('start', 0)
440
- self.sort = kwargs.get('sort', None)
441
- self.format = kwargs.get('format', 'json')
442
- self.cache = kwargs.get('cache', {})
443
-
444
- @timeit
445
- def serialize(self):
446
- """
447
- Serialize collection with pagination and sorting.
448
- """
449
- # Get total count for pagination
450
- total_count = self.queryset.count()
451
-
452
- # Apply sorting
453
- sorted_qset, sort_args = self._apply_sorting(self.queryset)
454
-
455
- # Apply pagination
456
- paginated_qset = sorted_qset[self.start:self.start + self.size]
457
-
458
- # Serialize items
459
- serializer = AdvancedGraphSerializer(
460
- paginated_qset,
461
- graph=self.graph,
462
- many=True,
463
- cache=self.cache
464
- )
465
- data = serializer.serialize()
466
-
467
- return {
468
- 'data': data,
469
- 'status': True,
470
- 'count': total_count,
471
- 'size': self.size,
472
- 'start': self.start,
473
- 'sort': sort_args if sort_args else None,
474
- 'graph': self.graph,
475
- 'datetime': int(time.time())
476
- }
477
-
478
- def _apply_sorting(self, qset):
479
- """
480
- Apply sorting to queryset.
481
- """
482
- if not self.sort:
483
- return qset, None
484
-
485
- sort_args = []
486
- for sort_field in self.sort.split(','):
487
- sort_field = sort_field.strip()
488
- if sort_field:
489
- # Handle reverse sorting
490
- if sort_field.startswith('-'):
491
- sort_args.append(F(sort_field[1:]).desc(nulls_last=True))
492
- else:
493
- sort_args.append(F(sort_field).asc(nulls_last=True))
494
-
495
- if sort_args:
496
- try:
497
- qset = qset.order_by(*sort_args)
498
- return qset, [str(arg) for arg in sort_args]
499
- except Exception as e:
500
- logger.error(f"Sorting error: {e}")
501
- return qset, None
502
-
503
- return qset, None
504
-
505
- def to_json(self, **kwargs):
506
- """Convert to JSON string."""
507
- data = self.serialize()
508
- data.update(kwargs)
509
-
510
- try:
511
- if HAS_UJSON:
512
- return ujson.dumps(data)
513
- else:
514
- return json.dumps(data, cls=ExtendedJSONEncoder)
515
- except Exception as e:
516
- logger.error(f"JSON serialization error: {e}")
517
- return json.dumps(data, cls=ExtendedJSONEncoder)
518
-
519
- def to_response(self, request=None, **kwargs):
520
- """Return HTTP response."""
521
- request = request or self.request
522
- return HttpResponse(self.to_json(**kwargs), content_type='application/json')
523
-
524
- def to_csv(self, fields=None, filename="export.csv"):
525
- """Export to CSV format."""
526
- if not fields:
527
- # Try to get fields from model RestMeta
528
- if hasattr(self.queryset.model, 'RestMeta') and hasattr(self.queryset.model.RestMeta, 'GRAPHS'):
529
- graph_config = self.queryset.model.RestMeta.GRAPHS.get(self.graph, {})
530
- fields = graph_config.get('fields', [])
531
-
532
- if not fields and hasattr(self.queryset.model, '_meta'):
533
- fields = [f.name for f in self.queryset.model._meta.fields]
534
-
535
- return csv.generate_csv(self.queryset, fields, filename)
536
-
537
- def to_excel(self, fields=None, filename="export.xlsx"):
538
- """Export to Excel format."""
539
- if not fields:
540
- # Try to get fields from model RestMeta
541
- if hasattr(self.queryset.model, 'RestMeta') and hasattr(self.queryset.model.RestMeta, 'GRAPHS'):
542
- graph_config = self.queryset.model.RestMeta.GRAPHS.get(self.graph, {})
543
- fields = graph_config.get('fields', [])
544
-
545
- if not fields and hasattr(self.queryset.model, '_meta'):
546
- fields = [f.name for f in self.queryset.model._meta.fields]
547
-
548
- return excel.generate_excel(self.queryset, fields, filename)
549
-
550
-
551
- # Convenience functions for backwards compatibility
552
- def serialize_model(instance, graph="default", **kwargs):
553
- """Serialize a single model instance."""
554
- return AdvancedGraphSerializer(instance, graph=graph, **kwargs).serialize()
555
-
556
- def serialize_collection(queryset, graph="list", **kwargs):
557
- """Serialize a collection/queryset."""
558
- return CollectionSerializer(queryset, graph=graph, **kwargs).serialize()
559
-
560
- def serialize_to_response(instance, graph="default", request=None, **kwargs):
561
- """Serialize and return HTTP response."""
562
- if isinstance(instance, QuerySet):
563
- serializer = CollectionSerializer(instance, graph=graph, request=request, **kwargs)
564
- else:
565
- many = kwargs.pop('many', False)
566
- serializer = AdvancedGraphSerializer(instance, graph=graph, many=many, request=request, **kwargs)
567
-
568
- return serializer.to_response(**kwargs)