django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +279 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -1,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)
|