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.
Files changed (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +651 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,509 @@
1
+ from decimal import Decimal
2
+ from datetime import datetime, date, time
3
+ import locale
4
+ from mojo.helpers import logit
5
+
6
+ logger = logit.get_logger("localizers", "localizers.log")
7
+
8
+ # Registry of available localizers
9
+ LOCALIZER_REGISTRY = {}
10
+
11
+
12
+ def register_localizer(name, func):
13
+ """
14
+ Register a localizer function.
15
+
16
+ :param name: Name to register the localizer under
17
+ :param func: Localizer function that takes (value, extra=None)
18
+ """
19
+ LOCALIZER_REGISTRY[name] = func
20
+ logger.debug(f"Registered localizer: {name}")
21
+
22
+
23
+ def get_localizer(name):
24
+ """
25
+ Get a localizer function by name.
26
+
27
+ :param name: Localizer name
28
+ :return: Localizer function or None
29
+ """
30
+ return LOCALIZER_REGISTRY.get(name)
31
+
32
+
33
+ def list_localizers():
34
+ """
35
+ Get list of all registered localizer names.
36
+
37
+ :return: List of localizer names
38
+ """
39
+ return list(LOCALIZER_REGISTRY.keys())
40
+
41
+
42
+ # Currency localizers
43
+ def cents_to_currency(value, extra=None):
44
+ """
45
+ Convert cents to currency format.
46
+
47
+ :param value: Value in cents
48
+ :param extra: Currency symbol (default: no symbol)
49
+ :return: Formatted currency string
50
+ """
51
+ if value is None:
52
+ return "0.00"
53
+
54
+ try:
55
+ currency, cents = divmod(int(value), 100)
56
+ if extra:
57
+ return f"{extra}{currency}.{cents:02d}"
58
+ return f"{currency}.{cents:02d}"
59
+ except (ValueError, TypeError):
60
+ return str(value)
61
+
62
+
63
+ def cents_to_dollars(value, extra=None):
64
+ """
65
+ Convert cents to dollar format.
66
+
67
+ :param value: Value in cents
68
+ :param extra: Not used
69
+ :return: Dollar-formatted string
70
+ """
71
+ if value is None:
72
+ return "$0.00"
73
+
74
+ try:
75
+ dollars, cents = divmod(int(value), 100)
76
+ return f"${dollars}.{cents:02d}"
77
+ except (ValueError, TypeError):
78
+ return str(value)
79
+
80
+
81
+ def currency_format(value, extra="$"):
82
+ """
83
+ Format value as currency with specified symbol.
84
+
85
+ :param value: Numeric value
86
+ :param extra: Currency symbol
87
+ :return: Formatted currency string
88
+ """
89
+ if value is None:
90
+ return f"{extra}0.00"
91
+
92
+ try:
93
+ if isinstance(value, (int, float, Decimal)):
94
+ return f"{extra}{value:.2f}"
95
+ return f"{extra}{float(value):.2f}"
96
+ except (ValueError, TypeError):
97
+ return str(value)
98
+
99
+
100
+ # Date/time localizers
101
+ def date_format(value, extra="%Y-%m-%d"):
102
+ """
103
+ Format date with specified format.
104
+
105
+ :param value: Date value
106
+ :param extra: Date format string
107
+ :return: Formatted date string
108
+ """
109
+ if value is None:
110
+ return ""
111
+
112
+ try:
113
+ if isinstance(value, datetime):
114
+ return value.strftime(extra)
115
+ elif isinstance(value, date):
116
+ return value.strftime(extra)
117
+ elif isinstance(value, str):
118
+ # Try to parse string date
119
+ parsed_date = datetime.fromisoformat(value.replace('Z', '+00:00'))
120
+ return parsed_date.strftime(extra)
121
+ return str(value)
122
+ except (ValueError, AttributeError):
123
+ return str(value)
124
+
125
+
126
+ def datetime_format(value, extra="%Y-%m-%d %H:%M:%S"):
127
+ """
128
+ Format datetime with specified format.
129
+
130
+ :param value: Datetime value
131
+ :param extra: Datetime format string
132
+ :return: Formatted datetime string
133
+ """
134
+ if value is None:
135
+ return ""
136
+
137
+ try:
138
+ if isinstance(value, datetime):
139
+ return value.strftime(extra)
140
+ elif isinstance(value, str):
141
+ # Try to parse string datetime
142
+ parsed_datetime = datetime.fromisoformat(value.replace('Z', '+00:00'))
143
+ return parsed_datetime.strftime(extra)
144
+ return str(value)
145
+ except (ValueError, AttributeError):
146
+ return str(value)
147
+
148
+
149
+ def time_format(value, extra="%H:%M:%S"):
150
+ """
151
+ Format time with specified format.
152
+
153
+ :param value: Time value
154
+ :param extra: Time format string
155
+ :return: Formatted time string
156
+ """
157
+ if value is None:
158
+ return ""
159
+
160
+ try:
161
+ if isinstance(value, time):
162
+ return value.strftime(extra)
163
+ elif isinstance(value, datetime):
164
+ return value.time().strftime(extra)
165
+ return str(value)
166
+ except (ValueError, AttributeError):
167
+ return str(value)
168
+
169
+
170
+ def timezone_format(value, extra=None):
171
+ """
172
+ Format datetime with timezone information.
173
+
174
+ :param value: Datetime value
175
+ :param extra: Timezone name (optional)
176
+ :return: Formatted datetime with timezone
177
+ """
178
+ if value is None:
179
+ return ""
180
+
181
+ try:
182
+ # Try to import timezone helper
183
+ try:
184
+ from mojo.helpers import dates
185
+ if extra:
186
+ localized_value = dates.get_local_time(extra, value)
187
+ return localized_value.strftime("%Y-%m-%d %H:%M:%S %Z")
188
+ elif hasattr(value, 'strftime'):
189
+ return value.strftime("%Y-%m-%d %H:%M:%S %Z")
190
+ except ImportError:
191
+ logger.warning("mojo.helpers.dates not available for timezone formatting")
192
+
193
+ if hasattr(value, 'strftime'):
194
+ return value.strftime("%Y-%m-%d %H:%M:%S")
195
+ return str(value)
196
+ except Exception as e:
197
+ logger.warning(f"Timezone formatting failed: {e}")
198
+ return str(value)
199
+
200
+
201
+ # Number formatters
202
+ def number_format(value, extra="2"):
203
+ """
204
+ Format number with specified decimal places.
205
+
206
+ :param value: Numeric value
207
+ :param extra: Number of decimal places (default: 2)
208
+ :return: Formatted number string
209
+ """
210
+ if value is None:
211
+ return "0"
212
+
213
+ try:
214
+ decimal_places = int(extra) if extra else 2
215
+ if isinstance(value, (int, float, Decimal)):
216
+ return f"{float(value):.{decimal_places}f}"
217
+ return f"{float(value):.{decimal_places}f}"
218
+ except (ValueError, TypeError):
219
+ return str(value)
220
+
221
+
222
+ def percentage_format(value, extra="2"):
223
+ """
224
+ Format value as percentage.
225
+
226
+ :param value: Numeric value (0.1 = 10%)
227
+ :param extra: Number of decimal places
228
+ :return: Formatted percentage string
229
+ """
230
+ if value is None:
231
+ return "0%"
232
+
233
+ try:
234
+ decimal_places = int(extra) if extra else 2
235
+ percentage = float(value) * 100
236
+ return f"{percentage:.{decimal_places}f}%"
237
+ except (ValueError, TypeError):
238
+ return str(value)
239
+
240
+
241
+ def thousands_separator(value, extra=","):
242
+ """
243
+ Add thousands separator to number.
244
+
245
+ :param value: Numeric value
246
+ :param extra: Separator character (default: comma)
247
+ :return: Formatted number with separators
248
+ """
249
+ if value is None:
250
+ return "0"
251
+
252
+ try:
253
+ separator = extra if extra else ","
254
+ return f"{int(value):,}".replace(",", separator)
255
+ except (ValueError, TypeError):
256
+ return str(value)
257
+
258
+
259
+ # Text formatters
260
+ def title_case(value, extra=None):
261
+ """
262
+ Convert text to title case.
263
+
264
+ :param value: Text value
265
+ :param extra: Not used
266
+ :return: Title case text
267
+ """
268
+ if value is None:
269
+ return ""
270
+
271
+ return str(value).title()
272
+
273
+
274
+ def upper_case(value, extra=None):
275
+ """
276
+ Convert text to upper case.
277
+
278
+ :param value: Text value
279
+ :param extra: Not used
280
+ :return: Upper case text
281
+ """
282
+ if value is None:
283
+ return ""
284
+
285
+ return str(value).upper()
286
+
287
+
288
+ def lower_case(value, extra=None):
289
+ """
290
+ Convert text to lower case.
291
+
292
+ :param value: Text value
293
+ :param extra: Not used
294
+ :return: Lower case text
295
+ """
296
+ if value is None:
297
+ return ""
298
+
299
+ return str(value).lower()
300
+
301
+
302
+ def truncate_text(value, extra="50"):
303
+ """
304
+ Truncate text to specified length.
305
+
306
+ :param value: Text value
307
+ :param extra: Maximum length (default: 50)
308
+ :return: Truncated text
309
+ """
310
+ if value is None:
311
+ return ""
312
+
313
+ try:
314
+ max_length = int(extra) if extra else 50
315
+ text = str(value)
316
+ if len(text) <= max_length:
317
+ return text
318
+ return text[:max_length-3] + "..."
319
+ except (ValueError, TypeError):
320
+ return str(value)
321
+
322
+
323
+ # Boolean formatters
324
+ def yes_no(value, extra=None):
325
+ """
326
+ Convert boolean to Yes/No.
327
+
328
+ :param value: Boolean value
329
+ :param extra: Not used
330
+ :return: "Yes" or "No"
331
+ """
332
+ if value is None:
333
+ return "No"
334
+
335
+ return "Yes" if bool(value) else "No"
336
+
337
+
338
+ def true_false(value, extra=None):
339
+ """
340
+ Convert boolean to True/False.
341
+
342
+ :param value: Boolean value
343
+ :param extra: Not used
344
+ :return: "True" or "False"
345
+ """
346
+ if value is None:
347
+ return "False"
348
+
349
+ return "True" if bool(value) else "False"
350
+
351
+
352
+ def on_off(value, extra=None):
353
+ """
354
+ Convert boolean to On/Off.
355
+
356
+ :param value: Boolean value
357
+ :param extra: Not used
358
+ :return: "On" or "Off"
359
+ """
360
+ if value is None:
361
+ return "Off"
362
+
363
+ return "On" if bool(value) else "Off"
364
+
365
+
366
+ # List/collection formatters
367
+ def join_list(value, extra=", "):
368
+ """
369
+ Join list items with separator.
370
+
371
+ :param value: List or iterable
372
+ :param extra: Separator string (default: ", ")
373
+ :return: Joined string
374
+ """
375
+ if value is None:
376
+ return ""
377
+
378
+ try:
379
+ separator = extra if extra else ", "
380
+ if isinstance(value, (list, tuple)):
381
+ return separator.join(str(item) for item in value)
382
+ return str(value)
383
+ except Exception:
384
+ return str(value)
385
+
386
+
387
+ def list_count(value, extra=None):
388
+ """
389
+ Return count of items in list.
390
+
391
+ :param value: List or iterable
392
+ :param extra: Not used
393
+ :return: Count as string
394
+ """
395
+ if value is None:
396
+ return "0"
397
+
398
+ try:
399
+ if hasattr(value, '__len__'):
400
+ return str(len(value))
401
+ elif hasattr(value, 'count'):
402
+ return str(value.count())
403
+ return "1"
404
+ except Exception:
405
+ return "0"
406
+
407
+
408
+ # File size formatter
409
+ def file_size(value, extra="auto"):
410
+ """
411
+ Format file size in human readable format.
412
+
413
+ :param value: File size in bytes
414
+ :param extra: Unit preference ("auto", "KB", "MB", "GB")
415
+ :return: Formatted file size
416
+ """
417
+ if value is None:
418
+ return "0 B"
419
+
420
+ try:
421
+ size = float(value)
422
+ if extra != "auto":
423
+ unit = extra.upper()
424
+ if unit == "KB":
425
+ return f"{size/1024:.2f} KB"
426
+ elif unit == "MB":
427
+ return f"{size/(1024**2):.2f} MB"
428
+ elif unit == "GB":
429
+ return f"{size/(1024**3):.2f} GB"
430
+
431
+ # Auto-detect best unit
432
+ if size < 1024:
433
+ return f"{size:.0f} B"
434
+ elif size < 1024**2:
435
+ return f"{size/1024:.2f} KB"
436
+ elif size < 1024**3:
437
+ return f"{size/(1024**2):.2f} MB"
438
+ else:
439
+ return f"{size/(1024**3):.2f} GB"
440
+ except (ValueError, TypeError):
441
+ return str(value)
442
+
443
+
444
+ # Register all localizers
445
+ register_localizer('cents_to_currency', cents_to_currency)
446
+ register_localizer('cents_to_dollars', cents_to_dollars)
447
+ register_localizer('currency', currency_format)
448
+ register_localizer('date', date_format)
449
+ register_localizer('datetime', datetime_format)
450
+ register_localizer('time', time_format)
451
+ register_localizer('timezone', timezone_format)
452
+ register_localizer('number', number_format)
453
+ register_localizer('percentage', percentage_format)
454
+ register_localizer('thousands', thousands_separator)
455
+ register_localizer('title', title_case)
456
+ register_localizer('upper', upper_case)
457
+ register_localizer('lower', lower_case)
458
+ register_localizer('truncate', truncate_text)
459
+ register_localizer('yes_no', yes_no)
460
+ register_localizer('true_false', true_false)
461
+ register_localizer('on_off', on_off)
462
+ register_localizer('join', join_list)
463
+ register_localizer('count', list_count)
464
+ register_localizer('filesize', file_size)
465
+
466
+
467
+ # Custom localizer decorator
468
+ def localizer(name):
469
+ """
470
+ Decorator to register a custom localizer.
471
+
472
+ Usage:
473
+ @localizer('my_formatter')
474
+ def my_custom_formatter(value, extra=None):
475
+ return f"Custom: {value}"
476
+ """
477
+ def decorator(func):
478
+ register_localizer(name, func)
479
+ return func
480
+ return decorator
481
+
482
+
483
+ # Legacy support
484
+ def apply_localizer(value, localizer_config):
485
+ """
486
+ Apply localizer based on configuration string.
487
+
488
+ :param value: Value to localize
489
+ :param localizer_config: Configuration string like "currency|$" or "date|%Y-%m-%d"
490
+ :return: Localized value
491
+ """
492
+ if not localizer_config:
493
+ return value
494
+
495
+ try:
496
+ if '|' in localizer_config:
497
+ localizer_name, extra = localizer_config.split('|', 1)
498
+ else:
499
+ localizer_name, extra = localizer_config, None
500
+
501
+ localizer_func = get_localizer(localizer_name)
502
+ if localizer_func:
503
+ return localizer_func(value, extra)
504
+ else:
505
+ logger.warning(f"Unknown localizer: {localizer_name}")
506
+ return value
507
+ except Exception as e:
508
+ logger.error(f"Localizer error for '{localizer_config}': {e}")
509
+ return value
@@ -1,9 +1,11 @@
1
1
  import ujson
2
- from django.db.models import ForeignKey, OneToOneField
2
+ from django.db.models import ForeignKey, OneToOneField, ManyToOneRel
3
3
  from django.db.models.query import QuerySet
4
+ from django.core.exceptions import FieldDoesNotExist
4
5
  from django.http import HttpResponse
5
6
  import datetime
6
7
  from mojo.helpers import logit
8
+ from distutils.log import info
7
9
 
8
10
  logger = logit.get_logger("serializer", "serializer.log")
9
11
 
@@ -66,46 +68,64 @@ class GraphSerializer:
66
68
  method_name, alias = field
67
69
  else:
68
70
  method_name, alias = field, field
71
+ logger.info(f"Processing extra field {method_name} for {obj.__class__.__name__}")
69
72
  if hasattr(obj, method_name):
70
73
  attr = getattr(obj, method_name)
71
74
  data[alias] = attr() if callable(attr) else attr
75
+ logger.info(f"Extra field {method_name} processed successfully", data[alias])
76
+ else:
77
+ logger.warning(f"Extra field {method_name} not found for {obj.__class__.__name__}")
72
78
 
73
- # Process related model graphs (ForeignKeys, OneToOneFields)
79
+ # Process related model graphs (ForeignKeys, OneToOneFields, ManyToManyFields)
74
80
  related_graphs = graph_config.get("graphs", {})
75
81
  for related_field, sub_graph in related_graphs.items():
76
82
  related_obj = getattr(obj, related_field, None)
77
83
  if related_obj is not None:
78
- # Determine if the field is a ForeignKey or OneToOneField
84
+ # Determine if the field is a ForeignKey, OneToOneField, or ManyToManyField
79
85
  field_obj = obj._meta.get_field(related_field)
80
86
  if isinstance(field_obj, (ForeignKey, OneToOneField)):
81
87
  # Serialize related model using its corresponding graph
82
88
  logger.warning(f"graph '{sub_graph}' for {related_obj.__class__.__name__}")
83
89
  data[related_field] = GraphSerializer(related_obj, graph=sub_graph).serialize()
84
-
90
+ elif isinstance(field_obj, ManyToOneRel):
91
+ # Serialize related models in ManyToManyField
92
+ logger.warning(f"graph '{sub_graph}' for many to many relation {related_obj.model.__name__}")
93
+ m2m_qset = related_obj.all()
94
+ data[related_field] = GraphSerializer(m2m_qset, graph=sub_graph).serialize()
95
+ else:
96
+ logger.warning(f"Unsupported field type for {related_field}: {type(field_obj)}")
85
97
  return data
86
98
 
99
+ def get_model_field(self, model_class, field_name):
100
+ try:
101
+ return model_class._meta.get_field(field_name)
102
+ except FieldDoesNotExist:
103
+ return None
104
+
87
105
  def _model_to_dict_custom(self, obj, fields=None):
88
106
  """
89
107
  Custom serialization method for Django model instances.
90
108
  """
91
109
  data = {}
92
- for field in obj._meta.fields:
93
- # logger.info(field, type(field), isinstance(field, (ForeignKey, OneToOneField)))
94
- if fields and field.name not in fields:
95
- continue
96
-
97
- field_value = getattr(obj, field.name)
110
+ if fields is None:
111
+ fields = [field.name for field in obj._meta.fields]
112
+ for name in fields:
113
+ field_value = getattr(obj, name)
114
+ field = self.get_model_field(obj, name)
115
+ # Check if the field_value is callable and call it to get the value
116
+ if callable(field_value):
117
+ field_value = field_value()
98
118
 
99
119
  # Handle DateTimeField serialization to epoch
100
120
  if isinstance(field_value, datetime.datetime):
101
- data[field.name] = int(field_value.timestamp())
121
+ data[name] = int(field_value.timestamp())
102
122
  # Handle date serialization to ISO format
103
123
  elif isinstance(field_value, datetime.date):
104
- data[field.name] = field_value.isoformat()
124
+ data[name] = field_value.isoformat()
105
125
  elif field_value is not None and isinstance(field, (ForeignKey, OneToOneField)):
106
- data[field.name] = field_value.id
126
+ data[name] = field_value.id
107
127
  else:
108
- data[field.name] = field_value
128
+ data[name] = field_value
109
129
  # logger.info(data)
110
130
  return data
111
131
 
@@ -118,7 +138,10 @@ class GraphSerializer:
118
138
  else:
119
139
  data = dict(data=data, status=True, graph=self.graph)
120
140
  data.update(dict(kwargs))
121
- out = ujson.dumps(data)
141
+ try:
142
+ out = ujson.dumps(data)
143
+ except Exception as e:
144
+ logger.exception(data)
122
145
  return out
123
146
 
124
147
  def to_response(self, request, **kwargs):