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,319 @@
1
+ from django.db import models
2
+ from django.utils.text import slugify
3
+ from mojo.models import MojoModel
4
+ from mojo.helpers import logit
5
+
6
+
7
+ class Page(models.Model, MojoModel):
8
+ """
9
+ Individual documentation page within a book
10
+
11
+ Pages can be organized hierarchically with parent-child relationships
12
+ and support version control through PageRevision records.
13
+ """
14
+
15
+ # Relationships
16
+ book = models.ForeignKey(
17
+ 'docit.Book',
18
+ on_delete=models.CASCADE,
19
+ related_name='pages',
20
+ help_text="Book this page belongs to"
21
+ )
22
+ parent = models.ForeignKey(
23
+ 'self',
24
+ null=True,
25
+ blank=True,
26
+ on_delete=models.CASCADE,
27
+ related_name='children',
28
+ help_text="Parent page for hierarchical organization"
29
+ )
30
+
31
+ # Basic fields
32
+ title = models.CharField(
33
+ max_length=200,
34
+ help_text="Page title"
35
+ )
36
+ slug = models.SlugField(
37
+ max_length=200,
38
+ db_index=True,
39
+ help_text="URL-friendly identifier (unique within book)"
40
+ )
41
+ content = models.TextField(
42
+ help_text="Raw markdown content"
43
+ )
44
+
45
+ # Ordering and metadata
46
+ order_priority = models.IntegerField(
47
+ default=0,
48
+ db_index=True,
49
+ help_text="Higher values appear first in listings"
50
+ )
51
+ metadata = models.JSONField(
52
+ default=dict,
53
+ help_text="Frontmatter and additional page metadata"
54
+ )
55
+
56
+ # Status
57
+ is_published = models.BooleanField(
58
+ default=True,
59
+ db_index=True,
60
+ help_text="Whether this page is published and visible"
61
+ )
62
+
63
+ # Ownership and tracking (inherits from book permissions)
64
+ user = models.ForeignKey(
65
+ 'account.User',
66
+ on_delete=models.PROTECT,
67
+ help_text="Page owner (inherited from book for permissions)"
68
+ )
69
+ created_by = models.ForeignKey(
70
+ 'account.User',
71
+ on_delete=models.PROTECT,
72
+ related_name='created_pages',
73
+ help_text="User who created this page"
74
+ )
75
+ modified_by = models.ForeignKey(
76
+ 'account.User',
77
+ on_delete=models.PROTECT,
78
+ related_name='modified_pages',
79
+ null=True,
80
+ default=None,
81
+ help_text="User who last modified this page"
82
+ )
83
+
84
+ # Standard MOJO timestamps
85
+ created = models.DateTimeField(
86
+ auto_now_add=True,
87
+ editable=False,
88
+ db_index=True
89
+ )
90
+ modified = models.DateTimeField(
91
+ auto_now=True,
92
+ db_index=True
93
+ )
94
+
95
+ class Meta:
96
+ ordering = ['-order_priority', 'title']
97
+ verbose_name = 'Page'
98
+ verbose_name_plural = 'Pages'
99
+ # Ensure slug is unique within a book
100
+ unique_together = ['book', 'slug']
101
+
102
+ class RestMeta:
103
+ VIEW_PERMS = ['all']
104
+ SAVE_PERMS = ['manage_docit', 'owner']
105
+ DELETE_PERMS = ['manage_docit', 'owner']
106
+ CREATED_BY_OWNER_FIELD = 'created_by'
107
+ UPDATED_BY_OWNER_FIELD = 'modified_by'
108
+ GRAPHS = {
109
+ 'default': {
110
+ "graphs": {
111
+ "user": "basic",
112
+ "book": "default",
113
+ "created_by": "basic",
114
+ "modified_by": "basic"
115
+ }
116
+ },
117
+ 'detail': {
118
+ "fields": [
119
+ 'id', 'title', 'slug', 'content', 'order_priority',
120
+ 'metadata', 'is_published', 'created', 'modified',
121
+ 'book', 'parent'
122
+ ],
123
+ "graphs": {
124
+ "user": "basic",
125
+ "book": "default",
126
+ "created_by": "basic",
127
+ "modified_by": "basic"
128
+ }
129
+ },
130
+ 'list': {
131
+ "fields": [
132
+ 'id', 'title', 'slug', 'is_published', 'order_priority', "metadata"
133
+ ],
134
+ "graphs": {
135
+ # "user": "basic",
136
+ "book": "default",
137
+ # "created_by": "basic",
138
+ "modified_by": "basic"
139
+ }
140
+ },
141
+ 'content_only': {
142
+ "fields": [
143
+ 'id', 'title', 'content'
144
+ ],
145
+ },
146
+ 'html': {
147
+ "fields": [
148
+ 'id', 'title', 'slug', 'order_priority',
149
+ 'metadata', 'is_published', 'created', 'modified',
150
+ ],
151
+ "extra": ["html"],
152
+ "graphs": {
153
+ "modified_by": "basic"
154
+ }
155
+ },
156
+ 'tree': {
157
+ "fields": [
158
+ 'id', 'title', 'slug', 'order_priority', 'parent', 'children'
159
+ ],
160
+ }
161
+ }
162
+
163
+ def __str__(self):
164
+ return f"{self.book.title} / {self.title}"
165
+
166
+ def save(self, *args, **kwargs):
167
+ """Override save to auto-generate slug, validate hierarchy, and log operations"""
168
+
169
+ # Auto-generate slug from title if not provided
170
+ if not self.slug:
171
+ self.slug = slugify(self.title.replace('_', '-'))
172
+
173
+ # Handle duplicate slugs within the same book
174
+ counter = 1
175
+ original_slug = self.slug
176
+ while Page.objects.filter(
177
+ book=self.book,
178
+ slug=self.slug
179
+ ).exclude(pk=self.pk).exists():
180
+ self.slug = f"{original_slug}-{counter}"
181
+ counter += 1
182
+
183
+ if (not hasattr(self, "user") or self.user is None) and self.created_by:
184
+ self.user = self.created_by
185
+
186
+ # Validate parent relationship (prevent circular references)
187
+ if self.parent:
188
+ if self.parent == self:
189
+ raise ValueError("A page cannot be its own parent")
190
+
191
+ if self.pk and self._would_create_cycle(self.parent):
192
+ raise ValueError("Parent relationship would create a circular reference")
193
+
194
+ # Ensure parent belongs to same book
195
+ if self.parent.book != self.book:
196
+ raise ValueError("Parent page must belong to the same book")
197
+
198
+ # Inherit user from book if not set
199
+ if not self.user_id and self.book_id:
200
+ self.user = self.book.user
201
+
202
+ # Log the operation
203
+ if self.pk:
204
+ logit.info(f"Updating page: {self.title} in book {self.book.title} (ID: {self.pk})")
205
+ else:
206
+ logit.info(f"Creating new page: {self.title} in book {self.book.title}")
207
+
208
+ super().save(*args, **kwargs)
209
+
210
+ def delete(self, *args, **kwargs):
211
+ """Override delete to log the operation"""
212
+ logit.info(f"Deleting page: {self.title} from book {self.book.title} (ID: {self.pk})")
213
+ super().delete(*args, **kwargs)
214
+
215
+ def _would_create_cycle(self, potential_parent):
216
+ """Check if setting potential_parent as parent would create a cycle"""
217
+ current = potential_parent
218
+ while current:
219
+ if current == self:
220
+ return True
221
+ current = current.parent
222
+ return False
223
+
224
+ @property
225
+ def full_path(self):
226
+ """Return hierarchical path like: parent/child/grandchild"""
227
+ if self.parent:
228
+ return f"{self.parent.full_path}/{self.slug}"
229
+ return self.slug
230
+
231
+ @property
232
+ def html(self):
233
+ """
234
+ Renders the Markdown content of the page to HTML.
235
+ """
236
+ from mojo.apps.docit.services.markdown import MarkdownRenderer
237
+ renderer = MarkdownRenderer()
238
+ rendered_html = renderer.render(self.content)
239
+ return rendered_html
240
+
241
+ @property
242
+ def ast(self):
243
+ """
244
+ Return AST representation of page content
245
+
246
+ This will be implemented in Phase 2 with markdown processing
247
+ """
248
+ return None
249
+
250
+ def get_children(self):
251
+ """Get direct child pages (published only by default)"""
252
+ return self.children.filter(is_published=True).order_by('-order_priority', 'title')
253
+
254
+ def get_all_children(self, include_unpublished=False):
255
+ """Get direct child pages with option to include unpublished"""
256
+ queryset = self.children.all()
257
+ if not include_unpublished:
258
+ queryset = queryset.filter(is_published=True)
259
+ return queryset.order_by('-order_priority', 'title')
260
+
261
+ def get_descendants(self):
262
+ """Get all descendant pages (recursive, published only)"""
263
+ descendants = []
264
+ for child in self.get_children():
265
+ descendants.append(child)
266
+ descendants.extend(child.get_descendants())
267
+ return descendants
268
+
269
+ def get_ancestors(self):
270
+ """Get all ancestor pages from root to immediate parent"""
271
+ ancestors = []
272
+ current = self.parent
273
+ while current:
274
+ ancestors.insert(0, current) # Insert at beginning for correct order
275
+ current = current.parent
276
+ return ancestors
277
+
278
+ def get_breadcrumbs(self):
279
+ """Get breadcrumb trail including this page"""
280
+ breadcrumbs = self.get_ancestors()
281
+ breadcrumbs.append(self)
282
+ return breadcrumbs
283
+
284
+ def get_depth(self):
285
+ """Get the depth level in the hierarchy (0 for root pages)"""
286
+ depth = 0
287
+ current = self.parent
288
+ while current:
289
+ depth += 1
290
+ current = current.parent
291
+ return depth
292
+
293
+ def create_revision(self, user, change_summary=""):
294
+ """Create a new revision record for this page"""
295
+ from .page_revision import PageRevision
296
+
297
+ # Get the next version number
298
+ last_revision = self.revisions.order_by('-version').first()
299
+ next_version = (last_revision.version + 1) if last_revision else 1
300
+
301
+ revision = PageRevision.objects.create(
302
+ page=self,
303
+ content=self.content,
304
+ version=next_version,
305
+ change_summary=change_summary,
306
+ created_by=user,
307
+ user=self.user
308
+ )
309
+
310
+ logit.info(f"Created revision v{next_version} for page: {self.title}")
311
+ return revision
312
+
313
+ def get_latest_revision(self):
314
+ """Get the most recent revision"""
315
+ return self.revisions.order_by('-version').first()
316
+
317
+ def get_revision_count(self):
318
+ """Get total number of revisions for this page"""
319
+ return self.revisions.count()
@@ -0,0 +1,203 @@
1
+ from django.db import models
2
+ from mojo.models import MojoModel
3
+ from mojo.helpers import logit
4
+
5
+
6
+ class PageRevision(models.Model, MojoModel):
7
+ """
8
+ Version history for pages
9
+
10
+ Each revision captures a snapshot of page content at a point in time,
11
+ providing a complete audit trail and version control system.
12
+ """
13
+
14
+ # Relationships
15
+ page = models.ForeignKey(
16
+ 'docit.Page',
17
+ on_delete=models.CASCADE,
18
+ related_name='revisions',
19
+ help_text="Page this revision belongs to"
20
+ )
21
+
22
+ # Content snapshot
23
+ content = models.TextField(
24
+ help_text="Markdown content snapshot at time of revision"
25
+ )
26
+
27
+ # Version tracking
28
+ version = models.IntegerField(
29
+ db_index=True,
30
+ help_text="Sequential version number for this page"
31
+ )
32
+
33
+ # Optional metadata
34
+ change_summary = models.CharField(
35
+ max_length=200,
36
+ blank=True,
37
+ help_text="Brief description of changes made in this revision"
38
+ )
39
+
40
+ # Ownership and tracking (inherits from page/book permissions)
41
+ user = models.ForeignKey(
42
+ 'account.User',
43
+ on_delete=models.PROTECT,
44
+ help_text="User for permission inheritance (from page/book)"
45
+ )
46
+ created_by = models.ForeignKey(
47
+ 'account.User',
48
+ on_delete=models.PROTECT,
49
+ related_name='created_revisions',
50
+ help_text="User who created this revision"
51
+ )
52
+
53
+ # Standard MOJO timestamps
54
+ created = models.DateTimeField(
55
+ auto_now_add=True,
56
+ editable=False,
57
+ db_index=True
58
+ )
59
+ modified = models.DateTimeField(
60
+ auto_now=True,
61
+ db_index=True
62
+ )
63
+
64
+ class Meta:
65
+ ordering = ['-version']
66
+ verbose_name = 'Page Revision'
67
+ verbose_name_plural = 'Page Revisions'
68
+ # Ensure version is unique within a page
69
+ unique_together = ['page', 'version']
70
+
71
+ class RestMeta:
72
+ VIEW_PERMS = ['all']
73
+ SAVE_PERMS = ['manage_docit', 'owner']
74
+ DELETE_PERMS = ['manage_docit', 'owner']
75
+
76
+ GRAPHS = {
77
+ 'default': {
78
+ "fields": [
79
+ 'id', 'version', 'change_summary', 'created'
80
+ ],
81
+ },
82
+ 'detail': {
83
+ "fields": [
84
+ 'id', 'content', 'version', 'change_summary',
85
+ 'created', 'page', 'created_by'
86
+ ],
87
+ },
88
+ 'list': {
89
+ "fields": [
90
+ 'id', 'version', 'change_summary', 'created'
91
+ ],
92
+ },
93
+ 'content_only': {
94
+ "fields": [
95
+ 'id', 'version', 'content'
96
+ ],
97
+ }
98
+ }
99
+
100
+ def __str__(self):
101
+ return f"{self.page.title} v{self.version}"
102
+
103
+ def save(self, *args, **kwargs):
104
+ """Override save to validate version and log operations"""
105
+
106
+ # Auto-assign version number if not provided
107
+ if not self.version:
108
+ last_revision = PageRevision.objects.filter(
109
+ page=self.page
110
+ ).order_by('-version').first()
111
+
112
+ self.version = (last_revision.version + 1) if last_revision else 1
113
+
114
+ # Inherit user from page if not set
115
+ if not self.user_id and self.page_id:
116
+ self.user = self.page.user
117
+
118
+ # Log the operation
119
+ if self.pk:
120
+ logit.info(f"Updating revision v{self.version} for page: {self.page.title} (ID: {self.pk})")
121
+ else:
122
+ logit.info(f"Creating revision v{self.version} for page: {self.page.title}")
123
+
124
+ super().save(*args, **kwargs)
125
+
126
+ def delete(self, *args, **kwargs):
127
+ """Override delete to log the operation"""
128
+ logit.warn(f"Deleting revision v{self.version} for page: {self.page.title} (ID: {self.pk})")
129
+ super().delete(*args, **kwargs)
130
+
131
+ @property
132
+ def is_latest(self):
133
+ """Check if this is the latest revision for the page"""
134
+ latest = PageRevision.objects.filter(
135
+ page=self.page
136
+ ).order_by('-version').first()
137
+
138
+ return latest and latest.id == self.id
139
+
140
+ def get_content_diff(self, other_revision=None):
141
+ """
142
+ Get content difference between this revision and another
143
+
144
+ This will be implemented in Phase 2 with proper diff functionality
145
+ """
146
+ return None
147
+
148
+ def get_previous_revision(self):
149
+ """Get the revision immediately before this one"""
150
+ return PageRevision.objects.filter(
151
+ page=self.page,
152
+ version__lt=self.version
153
+ ).order_by('-version').first()
154
+
155
+ def get_next_revision(self):
156
+ """Get the revision immediately after this one"""
157
+ return PageRevision.objects.filter(
158
+ page=self.page,
159
+ version__gt=self.version
160
+ ).order_by('version').first()
161
+
162
+ def restore_to_page(self, user):
163
+ """
164
+ Restore this revision's content to the current page
165
+
166
+ This creates a new revision with the restored content.
167
+ """
168
+ # Update the page content
169
+ self.page.content = self.content
170
+ self.page.modified_by = user
171
+ self.page.save()
172
+
173
+ # Create a new revision to track the restoration
174
+ new_revision = self.page.create_revision(
175
+ user=user,
176
+ change_summary=f"Restored from v{self.version}"
177
+ )
178
+
179
+ logit.info(f"Restored page '{self.page.title}' to revision v{self.version}")
180
+ return new_revision
181
+
182
+ def get_age_since_created(self):
183
+ """Get time elapsed since this revision was created"""
184
+ from django.utils import timezone
185
+ return timezone.now() - self.created
186
+
187
+ @classmethod
188
+ def cleanup_old_revisions(cls, page, keep_count=50):
189
+ """
190
+ Clean up old revisions, keeping only the most recent ones
191
+
192
+ This is a utility method for revision management
193
+ """
194
+ revisions = cls.objects.filter(page=page).order_by('-version')
195
+
196
+ if revisions.count() > keep_count:
197
+ old_revisions = revisions[keep_count:]
198
+ count = len(old_revisions)
199
+
200
+ for revision in old_revisions:
201
+ revision.delete()
202
+
203
+ logit.info(f"Cleaned up {count} old revisions for page: {page.title}")
@@ -0,0 +1,10 @@
1
+ """
2
+ DocIt REST Handlers
3
+
4
+ Import all REST endpoint handlers to register them with Django-MOJO
5
+ """
6
+
7
+ from .book import *
8
+ from .page import *
9
+ from .page_revision import *
10
+ from .asset import *
@@ -0,0 +1,17 @@
1
+ import mojo.decorators as md
2
+ from ..models import Asset
3
+
4
+
5
+ @md.URL('book/asset')
6
+ @md.URL('book/asset/<int:pk>')
7
+ def on_book_asset(request, pk=None):
8
+ """
9
+ Standard CRUD endpoints for Asset model
10
+
11
+ GET /api/docit/book/asset - List book assets
12
+ POST /api/docit/book/asset - Create new book asset
13
+ GET /api/docit/book/asset/<id> - Get single book asset
14
+ PUT /api/docit/book/asset/<id> - Update book asset
15
+ DELETE /api/docit/book/asset/<id> - Delete book asset
16
+ """
17
+ return Asset.on_rest_request(request, pk)
@@ -0,0 +1,22 @@
1
+ import mojo.decorators as md
2
+ from ..models import Book
3
+
4
+
5
+ @md.URL('book')
6
+ @md.URL('book/<int:pk>')
7
+ def on_book(request, pk=None):
8
+ """
9
+ Standard CRUD endpoints for Book model
10
+
11
+ GET /api/docit/book - List books
12
+ POST /api/docit/book - Create new book
13
+ GET /api/docit/book/<id> - Get single book
14
+ PUT /api/docit/book/<id> - Update book
15
+ DELETE /api/docit/book/<id> - Delete book
16
+ """
17
+ return Book.on_rest_request(request, pk)
18
+
19
+
20
+ @md.URL('book/slug/<str:slug>')
21
+ def on_book_by_slug(request, slug=None):
22
+ return Book.objects.get(slug=slug).on_rest_get(request)
@@ -0,0 +1,22 @@
1
+ import mojo.decorators as md
2
+ from ..models import Page
3
+
4
+
5
+ @md.URL('page')
6
+ @md.URL('page/<int:pk>')
7
+ def on_page(request, pk=None):
8
+ """
9
+ Standard CRUD endpoints for Page model
10
+
11
+ GET /api/docit/page - List pages
12
+ POST /api/docit/page - Create new page
13
+ GET /api/docit/page/<id> - Get single page
14
+ PUT /api/docit/page/<id> - Update page
15
+ DELETE /api/docit/page/<id> - Delete page
16
+ """
17
+ return Page.on_rest_request(request, pk)
18
+
19
+
20
+ @md.URL('page/slug/<str:slug>')
21
+ def on_page_by_slug(request, slug=None):
22
+ return Page.objects.get(slug=slug).on_rest_get(request)
@@ -0,0 +1,17 @@
1
+ import mojo.decorators as md
2
+ from ..models import PageRevision
3
+
4
+
5
+ @md.URL('page/revision')
6
+ @md.URL('page/revision/<int:pk>')
7
+ def on_page_revision(request, pk=None):
8
+ """
9
+ Standard CRUD endpoints for PageRevision model
10
+
11
+ GET /api/docit/page/revision - List page revisions
12
+ POST /api/docit/page/revision - Create new page revision
13
+ GET /api/docit/page/revision/<id> - Get single page revision
14
+ PUT /api/docit/page/revision/<id> - Update page revision
15
+ DELETE /api/docit/page/revision/<id> - Delete page revision
16
+ """
17
+ return PageRevision.on_rest_request(request, pk)
@@ -0,0 +1,11 @@
1
+ """
2
+ DocIt Services
3
+
4
+ Export business logic services for clean imports
5
+ """
6
+
7
+ from .docit import DocItService
8
+
9
+ __all__ = [
10
+ 'DocItService'
11
+ ]