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,315 @@
1
+ from mojo.helpers import logit
2
+ from ..models import Book, Page, PageRevision, Asset
3
+
4
+
5
+ class DocItService:
6
+ """
7
+ Business logic service for DocIt operations
8
+
9
+ Handles complex operations that span multiple models or contain
10
+ business logic that doesn't belong in individual model methods.
11
+ """
12
+
13
+ @staticmethod
14
+ def create_book_with_homepage(title, description, group, user, homepage_title="Home"):
15
+ """
16
+ Create a new book with an initial homepage
17
+
18
+ This is a common pattern where new books should have at least one page
19
+ """
20
+ try:
21
+ # Create the book
22
+ book = Book.objects.create(
23
+ title=title,
24
+ description=description,
25
+ group=group,
26
+ user=user,
27
+ created_by=user,
28
+ modified_by=user
29
+ )
30
+
31
+ # Create the homepage
32
+ homepage = Page.objects.create(
33
+ book=book,
34
+ title=homepage_title,
35
+ content=f"# {homepage_title}\n\nWelcome to {title}.",
36
+ order_priority=1000, # High priority to appear first
37
+ user=user,
38
+ created_by=user,
39
+ modified_by=user
40
+ )
41
+
42
+ # Create initial revision
43
+ homepage.create_revision(
44
+ user=user,
45
+ change_summary="Initial page creation"
46
+ )
47
+
48
+ logit.info(f"Created new book '{title}' with homepage for user {user.username}")
49
+
50
+ return book, homepage
51
+
52
+ except Exception as e:
53
+ logit.error(f"Failed to create book '{title}': {str(e)}")
54
+ raise
55
+
56
+ @staticmethod
57
+ def move_page(page, new_parent=None, new_position=None):
58
+ """
59
+ Move a page to a new location in the hierarchy
60
+
61
+ Handles validation and maintains data integrity
62
+ """
63
+ try:
64
+ old_parent = page.parent
65
+ old_path = page.full_path
66
+
67
+ # Validate the move
68
+ if new_parent and new_parent.book != page.book:
69
+ raise ValueError(f"Cannot move page to a different book: from '{page.book.id}' to '{new_parent.book.id}'")
70
+
71
+ if new_parent and page._would_create_cycle(new_parent):
72
+ raise ValueError("Move would create circular reference")
73
+
74
+ # Update the page
75
+ page.parent = new_parent
76
+
77
+ if new_position is not None:
78
+ page.order_priority = new_position
79
+
80
+ page.save()
81
+
82
+ new_path = page.full_path
83
+ logit.info(f"Moved page '{page.title}' from '{old_path}' to '{new_path}'")
84
+
85
+ return page
86
+
87
+ except Exception as e:
88
+ logit.error(f"Failed to move page '{page.title}': {str(e)}")
89
+ raise
90
+
91
+ @staticmethod
92
+ def duplicate_page(page, new_title, new_parent=None, include_children=False, user=None):
93
+ """
94
+ Create a duplicate of a page, optionally with its children
95
+ """
96
+ try:
97
+ # Create the duplicate
98
+ duplicate = Page.objects.create(
99
+ book=page.book,
100
+ parent=new_parent or page.parent,
101
+ title=new_title,
102
+ content=page.content,
103
+ order_priority=page.order_priority,
104
+ metadata=page.metadata.copy(),
105
+ is_published=False, # Start as draft
106
+ user=page.user,
107
+ created_by=user or page.created_by,
108
+ modified_by=user or page.modified_by
109
+ )
110
+
111
+ # Create initial revision for the duplicate
112
+ duplicate.create_revision(
113
+ user=user or page.created_by,
114
+ change_summary=f"Duplicated from '{page.title}'"
115
+ )
116
+
117
+ # Duplicate children if requested
118
+ if include_children:
119
+ for child in page.get_all_children(include_unpublished=True):
120
+ DocItService.duplicate_page(
121
+ page=child,
122
+ new_title=child.title,
123
+ new_parent=duplicate,
124
+ include_children=True, # Recursive
125
+ user=user
126
+ )
127
+
128
+ logit.info(f"Duplicated page '{page.title}' as '{new_title}' (children: {include_children})")
129
+
130
+ return duplicate
131
+
132
+ except Exception as e:
133
+ logit.error(f"Failed to duplicate page '{page.title}': {str(e)}")
134
+ raise
135
+
136
+ @staticmethod
137
+ def bulk_update_page_status(pages, is_published, user):
138
+ """
139
+ Bulk update publication status for multiple pages
140
+ """
141
+ try:
142
+ updated_count = 0
143
+
144
+ for page in pages:
145
+ if page.is_published != is_published:
146
+ page.is_published = is_published
147
+ page.modified_by = user
148
+ page.save()
149
+ updated_count += 1
150
+
151
+ status = "published" if is_published else "unpublished"
152
+ logit.info(f"Bulk updated {updated_count} pages to {status} by {user.username}")
153
+
154
+ return updated_count
155
+
156
+ except Exception as e:
157
+ logit.error(f"Failed to bulk update page status: {str(e)}")
158
+ raise
159
+
160
+ @staticmethod
161
+ def get_book_structure(book, include_unpublished=False):
162
+ """
163
+ Get the complete hierarchical structure of a book
164
+
165
+ Returns a nested dictionary representing the page tree
166
+ """
167
+ def build_tree(pages, parent_id=None):
168
+ tree = []
169
+ for page in pages:
170
+ if page.parent_id == parent_id:
171
+ page_data = {
172
+ 'id': page.id,
173
+ 'title': page.title,
174
+ 'slug': page.slug,
175
+ 'is_published': page.is_published,
176
+ 'order_priority': page.order_priority,
177
+ 'children': build_tree(pages, page.id)
178
+ }
179
+ tree.append(page_data)
180
+ return tree
181
+
182
+ try:
183
+ queryset = book.pages.all()
184
+ if not include_unpublished:
185
+ queryset = queryset.filter(is_published=True)
186
+
187
+ pages = list(queryset.order_by('-order_priority', 'title'))
188
+ structure = build_tree(pages)
189
+
190
+ logit.debug(f"Generated structure for book '{book.title}' with {len(pages)} pages")
191
+
192
+ return structure
193
+
194
+ except Exception as e:
195
+ logit.error(f"Failed to get book structure for '{book.title}': {str(e)}")
196
+ raise
197
+
198
+ @staticmethod
199
+ def organize_assets(book, asset_ids_in_order):
200
+ """
201
+ Reorder assets within a book based on provided ID list
202
+ """
203
+ try:
204
+ assets = Asset.objects.filter(book=book, id__in=asset_ids_in_order)
205
+
206
+ updated_count = 0
207
+ for index, asset_id in enumerate(asset_ids_in_order):
208
+ asset = assets.filter(id=asset_id).first()
209
+ if asset:
210
+ new_priority = len(asset_ids_in_order) - index # Higher index = higher priority
211
+ if asset.order_priority != new_priority:
212
+ asset.order_priority = new_priority
213
+ asset.save()
214
+ updated_count += 1
215
+
216
+ logit.info(f"Reorganized {updated_count} assets in book '{book.title}'")
217
+
218
+ return updated_count
219
+
220
+ except Exception as e:
221
+ logit.error(f"Failed to organize assets for book '{book.title}': {str(e)}")
222
+ raise
223
+
224
+ @staticmethod
225
+ def cleanup_orphaned_revisions(max_revisions_per_page=50):
226
+ """
227
+ Clean up old revisions across all pages to prevent database bloat
228
+ """
229
+ try:
230
+ total_cleaned = 0
231
+
232
+ # Get all pages that have more than the max revisions
233
+ for page in Page.objects.all():
234
+ revision_count = page.revisions.count()
235
+
236
+ if revision_count > max_revisions_per_page:
237
+ cleaned = PageRevision.cleanup_old_revisions(page, max_revisions_per_page)
238
+ total_cleaned += cleaned
239
+
240
+ if total_cleaned > 0:
241
+ logit.info(f"Cleaned up {total_cleaned} old page revisions")
242
+
243
+ return total_cleaned
244
+
245
+ except Exception as e:
246
+ logit.error(f"Failed to cleanup orphaned revisions: {str(e)}")
247
+ raise
248
+
249
+ @staticmethod
250
+ def get_book_statistics(book):
251
+ """
252
+ Get comprehensive statistics for a book
253
+ """
254
+ try:
255
+ stats = {
256
+ 'total_pages': book.get_page_count(),
257
+ 'published_pages': book.pages.filter(is_published=True).count(),
258
+ 'draft_pages': book.pages.filter(is_published=False).count(),
259
+ 'total_assets': book.get_asset_count(),
260
+ 'image_assets': book.assets.filter(file__category='image').count(),
261
+ 'document_assets': book.assets.filter(file__category='document').count(),
262
+ 'total_revisions': PageRevision.objects.filter(page__book=book).count(),
263
+ 'root_pages': book.get_root_pages(published_only=False).count(),
264
+ 'max_depth': 0
265
+ }
266
+
267
+ # Calculate maximum page depth
268
+ for page in book.pages.all():
269
+ depth = page.get_depth()
270
+ if depth > stats['max_depth']:
271
+ stats['max_depth'] = depth
272
+
273
+ logit.debug(f"Generated statistics for book '{book.title}'")
274
+
275
+ return stats
276
+
277
+ except Exception as e:
278
+ logit.error(f"Failed to get statistics for book '{book.title}': {str(e)}")
279
+ raise
280
+
281
+ @staticmethod
282
+ def validate_book_integrity(book):
283
+ """
284
+ Validate the integrity of a book and its pages
285
+
286
+ Returns a list of issues found
287
+ """
288
+ issues = []
289
+
290
+ try:
291
+ # Check for circular references in page hierarchy
292
+ for page in book.pages.all():
293
+ try:
294
+ _ = page.full_path # This will fail if there's a cycle
295
+ except RecursionError:
296
+ issues.append(f"Circular reference detected in page hierarchy: {page.title}")
297
+
298
+ # Check for orphaned assets (assets without files)
299
+ orphaned_assets = book.assets.filter(file__isnull=True)
300
+ if orphaned_assets.exists():
301
+ issues.append(f"Found {orphaned_assets.count()} orphaned assets without files")
302
+
303
+ # Check for pages with same slug
304
+ slugs = book.pages.values_list('slug', flat=True)
305
+ duplicate_slugs = [slug for slug in set(slugs) if slugs.count(slug) > 1]
306
+ if duplicate_slugs:
307
+ issues.append(f"Duplicate page slugs found: {duplicate_slugs}")
308
+
309
+ logit.info(f"Book integrity check for '{book.title}' found {len(issues)} issues")
310
+
311
+ return issues
312
+
313
+ except Exception as e:
314
+ logit.error(f"Failed to validate book integrity for '{book.title}': {str(e)}")
315
+ raise
@@ -0,0 +1,44 @@
1
+ import mistune
2
+ from pygments import highlight
3
+ from pygments.lexers import get_lexer_by_name
4
+ from pygments.styles import get_style_by_name
5
+ from pygments.formatters import HtmlFormatter
6
+
7
+
8
+ class HighlightRenderer(mistune.HTMLRenderer):
9
+ def block_code(self, code, info=None):
10
+ if not info:
11
+ return f'\n<pre>{mistune.escape(code)}</pre>\n'
12
+ lexer = get_lexer_by_name(info, stripall=True)
13
+ formatter = HtmlFormatter(
14
+ linenos=False,
15
+ cssclass="highlight",
16
+ style=get_style_by_name("monokai")
17
+ )
18
+ return highlight(code, lexer, formatter)
19
+
20
+ class MarkdownRenderer:
21
+ _renderer = None
22
+
23
+ def __init__(self):
24
+ if not self._renderer:
25
+ self._initialize_renderer()
26
+
27
+ def _initialize_renderer(self):
28
+ # plugins = self._discover_plugins()
29
+ self._renderer = mistune.create_markdown(
30
+ renderer=HighlightRenderer(escape=False),
31
+ escape=False,
32
+ hard_wrap=True,
33
+ plugins=[]
34
+ )
35
+
36
+ def _discover_plugins(self):
37
+ # from mojo.apps.docit.markdown_plugins import syntax_highlight
38
+ plugins = [
39
+ 'table', 'url', 'task_list',
40
+ 'footnotes', 'abbr', 'mark', 'math']
41
+ return plugins
42
+
43
+ def render(self, markdown_text):
44
+ return self._renderer(markdown_text)
@@ -103,8 +103,8 @@ const data = await response.json();
103
103
  "files": [
104
104
  {
105
105
  "id": 123,
106
- "filename": "document_20231201_abc12345.pdf",
107
- "original_filename": "document.pdf",
106
+ "storage_filename": "document_20231201_abc12345.pdf",
107
+ "filename": "document.pdf",
108
108
  "upload_token": "a1b2c3d4e5f6...",
109
109
  "upload_url": "https://s3.amazonaws.com/my-bucket/...",
110
110
  "method": "POST",
@@ -297,12 +297,12 @@ class FileUploader {
297
297
 
298
298
  async uploadFile(file, uploadData) {
299
299
  const formData = new FormData();
300
-
300
+
301
301
  // Add fields for S3 or other backends
302
302
  Object.entries(uploadData.fields || {}).forEach(([key, value]) => {
303
303
  formData.append(key, value);
304
304
  });
305
-
305
+
306
306
  formData.append('file', file);
307
307
 
308
308
  const response = await fetch(uploadData.upload_url, {
@@ -411,7 +411,7 @@ file_manager = FileManager.objects.create(
411
411
  allowed_extensions=["jpg", "jpeg", "png", "gif", "webp"],
412
412
  allowed_mime_types=[
413
413
  "image/jpeg",
414
- "image/png",
414
+ "image/png",
415
415
  "image/gif",
416
416
  "image/webp"
417
417
  ],
@@ -429,13 +429,13 @@ def validate_upload(request, file_data):
429
429
  # Custom business logic
430
430
  if file_data['filename'].startswith('temp_'):
431
431
  raise ValidationError('Temporary files not allowed')
432
-
432
+
433
433
  # Check file size against user's quota
434
434
  user_files_size = File.objects.filter(
435
435
  uploaded_by=request.user,
436
436
  upload_status=File.COMPLETED
437
437
  ).aggregate(total=Sum('file_size'))['total'] or 0
438
-
438
+
439
439
  if user_files_size + file_data['size'] > USER_QUOTA:
440
440
  raise ValidationError('Upload would exceed user quota')
441
441
  ```
@@ -546,4 +546,4 @@ print(f"Backend valid: {is_valid}, Errors: {errors}")
546
546
 
547
547
  ## License
548
548
 
549
- This project is licensed under the MIT License - see the LICENSE file for details.
549
+ This project is licensed under the MIT License - see the LICENSE file for details.