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,113 @@
1
+ # Generated by Django 4.2.21 on 2025-08-30 18:43
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+ import mojo.models.rest
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ initial = True
12
+
13
+ dependencies = [
14
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15
+ ('account', '0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more'),
16
+ ('fileman', '0011_alter_filerendition_original_file'),
17
+ ]
18
+
19
+ operations = [
20
+ migrations.CreateModel(
21
+ name='Book',
22
+ fields=[
23
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24
+ ('title', models.CharField(help_text='Book title', max_length=200)),
25
+ ('slug', models.SlugField(help_text='URL-friendly identifier', max_length=200, unique=True)),
26
+ ('description', models.TextField(blank=True, help_text='Brief description of the book content')),
27
+ ('order_priority', models.IntegerField(db_index=True, default=0, help_text='Higher values appear first in listings')),
28
+ ('permissions', models.CharField(blank=True, help_text='Comma-separated permission strings for fine-grained access control', max_length=500)),
29
+ ('config', models.JSONField(default=dict, help_text='Book-specific settings, plugin configuration, and custom access rules')),
30
+ ('is_active', models.BooleanField(db_index=True, default=True, help_text='Whether this book is active and visible')),
31
+ ('created', models.DateTimeField(auto_now_add=True, db_index=True)),
32
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
33
+ ('created_by', models.ForeignKey(help_text='User who created this book', on_delete=django.db.models.deletion.PROTECT, related_name='created_books', to=settings.AUTH_USER_MODEL)),
34
+ ('group', models.ForeignKey(help_text='Owning group for this book', on_delete=django.db.models.deletion.CASCADE, to='account.group')),
35
+ ('modified_by', models.ForeignKey(help_text='User who last modified this book', on_delete=django.db.models.deletion.PROTECT, related_name='modified_books', to=settings.AUTH_USER_MODEL)),
36
+ ('user', models.ForeignKey(help_text='Book owner for permission checks', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
37
+ ],
38
+ options={
39
+ 'verbose_name': 'Book',
40
+ 'verbose_name_plural': 'Books',
41
+ 'ordering': ['-order_priority', 'title'],
42
+ },
43
+ bases=(models.Model, mojo.models.rest.MojoModel),
44
+ ),
45
+ migrations.CreateModel(
46
+ name='Page',
47
+ fields=[
48
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
49
+ ('title', models.CharField(help_text='Page title', max_length=200)),
50
+ ('slug', models.SlugField(help_text='URL-friendly identifier (unique within book)', max_length=200)),
51
+ ('content', models.TextField(help_text='Raw markdown content')),
52
+ ('order_priority', models.IntegerField(db_index=True, default=0, help_text='Higher values appear first in listings')),
53
+ ('metadata', models.JSONField(default=dict, help_text='Frontmatter and additional page metadata')),
54
+ ('is_published', models.BooleanField(db_index=True, default=True, help_text='Whether this page is published and visible')),
55
+ ('created', models.DateTimeField(auto_now_add=True, db_index=True)),
56
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
57
+ ('book', models.ForeignKey(help_text='Book this page belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='pages', to='docit.book')),
58
+ ('created_by', models.ForeignKey(help_text='User who created this page', on_delete=django.db.models.deletion.PROTECT, related_name='created_pages', to=settings.AUTH_USER_MODEL)),
59
+ ('modified_by', models.ForeignKey(help_text='User who last modified this page', on_delete=django.db.models.deletion.PROTECT, related_name='modified_pages', to=settings.AUTH_USER_MODEL)),
60
+ ('parent', models.ForeignKey(blank=True, help_text='Parent page for hierarchical organization', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='docit.page')),
61
+ ('user', models.ForeignKey(help_text='Page owner (inherited from book for permissions)', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
62
+ ],
63
+ options={
64
+ 'verbose_name': 'Page',
65
+ 'verbose_name_plural': 'Pages',
66
+ 'ordering': ['-order_priority', 'title'],
67
+ 'unique_together': {('book', 'slug')},
68
+ },
69
+ bases=(models.Model, mojo.models.rest.MojoModel),
70
+ ),
71
+ migrations.CreateModel(
72
+ name='Asset',
73
+ fields=[
74
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
75
+ ('order_priority', models.IntegerField(db_index=True, default=0, help_text='Higher values appear first in asset lists')),
76
+ ('alt_text', models.CharField(blank=True, help_text='Alternative text for images and accessibility', max_length=200)),
77
+ ('description', models.TextField(blank=True, help_text='Detailed description of the asset')),
78
+ ('created', models.DateTimeField(auto_now_add=True, db_index=True)),
79
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
80
+ ('book', models.ForeignKey(help_text='Book this asset belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='docit.book')),
81
+ ('created_by', models.ForeignKey(help_text='User who created this asset', on_delete=django.db.models.deletion.PROTECT, related_name='created_assets', to=settings.AUTH_USER_MODEL)),
82
+ ('file', models.ForeignKey(blank=True, help_text='Associated file from fileman', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='docit_assets', to='fileman.file')),
83
+ ('user', models.ForeignKey(help_text='Asset owner (inherited from book for permissions)', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
84
+ ],
85
+ options={
86
+ 'verbose_name': 'Asset',
87
+ 'verbose_name_plural': 'Assets',
88
+ 'ordering': ['-order_priority', 'id'],
89
+ },
90
+ bases=(models.Model, mojo.models.rest.MojoModel),
91
+ ),
92
+ migrations.CreateModel(
93
+ name='PageRevision',
94
+ fields=[
95
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
96
+ ('content', models.TextField(help_text='Markdown content snapshot at time of revision')),
97
+ ('version', models.IntegerField(db_index=True, help_text='Sequential version number for this page')),
98
+ ('change_summary', models.CharField(blank=True, help_text='Brief description of changes made in this revision', max_length=200)),
99
+ ('created', models.DateTimeField(auto_now_add=True, db_index=True)),
100
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
101
+ ('created_by', models.ForeignKey(help_text='User who created this revision', on_delete=django.db.models.deletion.PROTECT, related_name='created_revisions', to=settings.AUTH_USER_MODEL)),
102
+ ('page', models.ForeignKey(help_text='Page this revision belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='docit.page')),
103
+ ('user', models.ForeignKey(help_text='User for permission inheritance (from page/book)', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
104
+ ],
105
+ options={
106
+ 'verbose_name': 'Page Revision',
107
+ 'verbose_name_plural': 'Page Revisions',
108
+ 'ordering': ['-version'],
109
+ 'unique_together': {('page', 'version')},
110
+ },
111
+ bases=(models.Model, mojo.models.rest.MojoModel),
112
+ ),
113
+ ]
@@ -0,0 +1,26 @@
1
+ # Generated by Django 4.2.21 on 2025-08-30 19:06
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12
+ ('docit', '0001_initial'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AlterField(
17
+ model_name='book',
18
+ name='modified_by',
19
+ field=models.ForeignKey(default=None, help_text='User who last modified this book', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modified_books', to=settings.AUTH_USER_MODEL),
20
+ ),
21
+ migrations.AlterField(
22
+ model_name='page',
23
+ name='modified_by',
24
+ field=models.ForeignKey(default=None, help_text='User who last modified this page', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modified_pages', to=settings.AUTH_USER_MODEL),
25
+ ),
26
+ ]
@@ -0,0 +1,20 @@
1
+ # Generated by Django 4.2.21 on 2025-08-30 21:48
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('account', '0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more'),
11
+ ('docit', '0002_alter_book_modified_by_alter_page_modified_by'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AlterField(
16
+ model_name='book',
17
+ name='group',
18
+ field=models.ForeignKey(blank=True, default=None, help_text='Owning group for this book', null=True, on_delete=django.db.models.deletion.CASCADE, to='account.group'),
19
+ ),
20
+ ]
@@ -0,0 +1,17 @@
1
+ """
2
+ DocIt Models
3
+
4
+ Export all DocIt models for clean imports
5
+ """
6
+
7
+ from .book import Book
8
+ from .page import Page
9
+ from .page_revision import PageRevision
10
+ from .asset import Asset
11
+
12
+ __all__ = [
13
+ 'Book',
14
+ 'Page',
15
+ 'PageRevision',
16
+ 'Asset'
17
+ ]
@@ -0,0 +1,231 @@
1
+ from django.db import models
2
+ from mojo.models import MojoModel
3
+ from mojo.helpers import logit
4
+
5
+
6
+ class Asset(models.Model, MojoModel):
7
+ """
8
+ Files associated with a book (images, documents, etc.)
9
+
10
+ Assets provide a way to attach files to documentation books,
11
+ with support for organization, metadata, and access control.
12
+ """
13
+
14
+ # Relationships
15
+ book = models.ForeignKey(
16
+ 'docit.Book',
17
+ on_delete=models.CASCADE,
18
+ related_name='assets',
19
+ help_text="Book this asset belongs to"
20
+ )
21
+ file = models.ForeignKey(
22
+ 'fileman.File',
23
+ on_delete=models.SET_NULL,
24
+ null=True,
25
+ blank=True,
26
+ related_name='docit_assets',
27
+ help_text="Associated file from fileman"
28
+ )
29
+
30
+ # Organization
31
+ order_priority = models.IntegerField(
32
+ default=0,
33
+ db_index=True,
34
+ help_text="Higher values appear first in asset lists"
35
+ )
36
+
37
+ # Optional metadata
38
+ alt_text = models.CharField(
39
+ max_length=200,
40
+ blank=True,
41
+ help_text="Alternative text for images and accessibility"
42
+ )
43
+ description = models.TextField(
44
+ blank=True,
45
+ help_text="Detailed description of the asset"
46
+ )
47
+
48
+ # Ownership and tracking (inherits from book permissions)
49
+ user = models.ForeignKey(
50
+ 'account.User',
51
+ on_delete=models.PROTECT,
52
+ help_text="Asset owner (inherited from book for permissions)"
53
+ )
54
+ created_by = models.ForeignKey(
55
+ 'account.User',
56
+ on_delete=models.PROTECT,
57
+ related_name='created_assets',
58
+ help_text="User who created this asset"
59
+ )
60
+
61
+ # Standard MOJO timestamps
62
+ created = models.DateTimeField(
63
+ auto_now_add=True,
64
+ editable=False,
65
+ db_index=True
66
+ )
67
+ modified = models.DateTimeField(
68
+ auto_now=True,
69
+ db_index=True
70
+ )
71
+
72
+ class Meta:
73
+ ordering = ['-order_priority', 'id']
74
+ verbose_name = 'Asset'
75
+ verbose_name_plural = 'Assets'
76
+
77
+ class RestMeta:
78
+ VIEW_PERMS = ['all']
79
+ SAVE_PERMS = ['manage_docit', 'owner']
80
+ DELETE_PERMS = ['manage_docit', 'owner']
81
+ CREATED_BY_OWNER_FIELD = 'created_by'
82
+ GRAPHS = {
83
+ 'default': {
84
+ "fields": [
85
+ 'id', 'alt_text', 'description', 'order_priority', 'created'
86
+ ],
87
+ "graphs": {
88
+ "user": "basic",
89
+ "book": "default",
90
+ "created_by": "basic",
91
+ "file": "basic"
92
+ }
93
+ },
94
+ 'detail': {
95
+ "fields": [
96
+ 'id', 'alt_text', 'description', 'order_priority',
97
+ 'file', 'book', 'created', 'created_by'
98
+ ],
99
+ "graphs": {
100
+ "user": "basic",
101
+ "book": "default",
102
+ "created_by": "basic",
103
+ "file": "basic"
104
+ }
105
+ },
106
+ 'list': {
107
+ "fields": [
108
+ 'id', 'alt_text', 'order_priority'
109
+ ],
110
+ "graphs": {
111
+ "user": "basic",
112
+ "book": "default",
113
+ "created_by": "basic",
114
+ "file": "basic"
115
+ }
116
+ },
117
+ 'file_info': {
118
+ "fields": [
119
+ 'id', 'alt_text', 'file', 'order_priority'
120
+ ],
121
+ "graphs": {
122
+ "user": "basic",
123
+ "book": "default",
124
+ "created_by": "basic",
125
+ "file": "basic"
126
+ }
127
+ }
128
+ }
129
+
130
+ def __str__(self):
131
+ if self.file:
132
+ return f"{self.book.title} / {self.filename}"
133
+ return f"{self.book.title} / Asset #{self.id}"
134
+
135
+ def save(self, *args, **kwargs):
136
+ """Override save to inherit user from book and log operations"""
137
+
138
+ # Inherit user from book if not set
139
+ if not self.user_id and self.book_id:
140
+ self.user = self.book.user
141
+
142
+ # Log the operation
143
+ if self.pk:
144
+ logit.info(f"Updating asset in book {self.book.title} (ID: {self.pk})")
145
+ else:
146
+ logit.info(f"Creating new asset in book {self.book.title}")
147
+
148
+ super().save(*args, **kwargs)
149
+
150
+ def delete(self, *args, **kwargs):
151
+ """Override delete to log the operation"""
152
+ logit.info(f"Deleting asset from book {self.book.title} (ID: {self.pk})")
153
+ super().delete(*args, **kwargs)
154
+
155
+ @property
156
+ def filename(self):
157
+ """Get the filename from the associated file"""
158
+ if self.file:
159
+ return self.file.name
160
+ return None
161
+
162
+ @property
163
+ def file_size(self):
164
+ """Get the file size from the associated file"""
165
+ if self.file:
166
+ return self.file.size
167
+ return None
168
+
169
+ @property
170
+ def file_type(self):
171
+ """Get the file type/category from the associated file"""
172
+ if self.file:
173
+ return self.file.category
174
+ return None
175
+
176
+ @property
177
+ def is_image(self):
178
+ """Check if asset is an image"""
179
+ if self.file:
180
+ return self.file.category == 'image'
181
+ return False
182
+
183
+ @property
184
+ def is_document(self):
185
+ """Check if asset is a document"""
186
+ if self.file:
187
+ return self.file.category == 'document'
188
+ return False
189
+
190
+ @property
191
+ def file_url(self):
192
+ """Get the URL to access the file"""
193
+ if self.file:
194
+ return self.file.url
195
+ return None
196
+
197
+ @property
198
+ def thumbnail_url(self):
199
+ """Get thumbnail URL for images"""
200
+ if self.file and self.is_image:
201
+ # This assumes fileman has thumbnail support
202
+ return getattr(self.file, 'thumbnail_url', None)
203
+ return None
204
+
205
+ def get_display_name(self):
206
+ """Get the best display name for this asset"""
207
+ if self.alt_text:
208
+ return self.alt_text
209
+ if self.filename:
210
+ return self.filename
211
+ return f"Asset #{self.id}"
212
+
213
+ def can_user_access(self, user):
214
+ """
215
+ Check if user can access this asset
216
+
217
+ Assets inherit access control from their parent book
218
+ """
219
+ return self.book.can_user_view(user)
220
+
221
+ def get_file_extension(self):
222
+ """Get file extension from filename"""
223
+ if self.filename:
224
+ return self.filename.split('.')[-1].lower() if '.' in self.filename else ''
225
+ return None
226
+
227
+ def get_mime_type(self):
228
+ """Get MIME type from the file"""
229
+ if self.file:
230
+ return getattr(self.file, 'mime_type', None)
231
+ return None
@@ -0,0 +1,227 @@
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 Book(models.Model, MojoModel):
8
+ """
9
+ Top-level documentation collection
10
+
11
+ A Book represents a complete documentation collection that can contain
12
+ multiple pages organized hierarchically, along with associated assets.
13
+ """
14
+
15
+ # Basic fields
16
+ title = models.CharField(max_length=200, help_text="Book title")
17
+ slug = models.SlugField(
18
+ unique=True,
19
+ max_length=200,
20
+ help_text="URL-friendly identifier"
21
+ )
22
+ description = models.TextField(
23
+ blank=True,
24
+ help_text="Brief description of the book content"
25
+ )
26
+
27
+ # Ordering and permissions
28
+ order_priority = models.IntegerField(
29
+ default=0,
30
+ db_index=True,
31
+ help_text="Higher values appear first in listings"
32
+ )
33
+ permissions = models.CharField(
34
+ max_length=500,
35
+ blank=True,
36
+ help_text="Comma-separated permission strings for fine-grained access control"
37
+ )
38
+
39
+ # Configuration
40
+ config = models.JSONField(
41
+ default=dict,
42
+ help_text="Book-specific settings, plugin configuration, and custom access rules"
43
+ )
44
+
45
+ # Status
46
+ is_active = models.BooleanField(
47
+ default=True,
48
+ db_index=True,
49
+ help_text="Whether this book is active and visible"
50
+ )
51
+
52
+ # Ownership and tracking
53
+ group = models.ForeignKey(
54
+ 'account.Group',
55
+ on_delete=models.CASCADE,
56
+ null=True,
57
+ blank=True,
58
+ default=None,
59
+ help_text="Owning group for this book"
60
+ )
61
+ user = models.ForeignKey(
62
+ 'account.User',
63
+ on_delete=models.PROTECT,
64
+ help_text="Book owner for permission checks"
65
+ )
66
+ created_by = models.ForeignKey(
67
+ 'account.User',
68
+ on_delete=models.PROTECT,
69
+ related_name='created_books',
70
+ help_text="User who created this book"
71
+ )
72
+ modified_by = models.ForeignKey(
73
+ 'account.User',
74
+ on_delete=models.PROTECT,
75
+ related_name='modified_books',
76
+ null=True,
77
+ default=None,
78
+ help_text="User who last modified this book"
79
+ )
80
+
81
+ # Standard MOJO timestamps
82
+ created = models.DateTimeField(
83
+ auto_now_add=True,
84
+ editable=False,
85
+ db_index=True
86
+ )
87
+ modified = models.DateTimeField(
88
+ auto_now=True,
89
+ db_index=True
90
+ )
91
+
92
+ class Meta:
93
+ ordering = ['-order_priority', 'title']
94
+ verbose_name = 'Book'
95
+ verbose_name_plural = 'Books'
96
+
97
+ class RestMeta:
98
+ VIEW_PERMS = ['all']
99
+ SAVE_PERMS = ['manage_docit', 'owner']
100
+ DELETE_PERMS = ['manage_docit', 'owner']
101
+ CREATED_BY_OWNER_FIELD = 'created_by'
102
+ UPDATED_BY_OWNER_FIELD = 'modified_by'
103
+ GRAPHS = {
104
+ 'default': {
105
+ "fields": [
106
+ 'id', 'title', 'slug', 'description',
107
+ 'is_active', 'created', 'modified'
108
+ ],
109
+ "graphs": {
110
+ "user": "basic",
111
+ "group": "basic",
112
+ "created_by": "basic",
113
+ "modified_by": "basic"
114
+ }
115
+ },
116
+ 'detail': {
117
+ "fields": [
118
+ 'id', 'title', 'slug', 'description', 'order_priority',
119
+ 'config', 'is_active', 'created', 'modified',
120
+ 'created_by', 'modified_by'
121
+ ],
122
+ "graphs": {
123
+ "user": "basic",
124
+ "group": "basic",
125
+ "created_by": "basic",
126
+ "modified_by": "basic"
127
+ }
128
+ },
129
+ 'list': {
130
+ "fields": [
131
+ 'id', 'title', 'slug', 'description', 'is_active'
132
+ ],
133
+ "graphs": {
134
+ # "user": "basic",
135
+ "group": "basic",
136
+ # "created_by": "basic",
137
+ # "modified_by": "basic"
138
+ }
139
+ }
140
+ }
141
+
142
+ def __str__(self):
143
+ return self.title
144
+
145
+ def save(self, *args, **kwargs):
146
+ """Override save to auto-generate slug and log operations"""
147
+
148
+ # Auto-generate slug from title if not provided
149
+ if not self.slug:
150
+ self.slug = slugify(self.title.replace('_', '-'))
151
+
152
+ # Handle duplicate slugs by appending a counter
153
+ counter = 1
154
+ original_slug = self.slug
155
+ while Book.objects.filter(slug=self.slug).exclude(pk=self.pk).exists():
156
+ self.slug = f"{original_slug}-{counter}"
157
+ counter += 1
158
+
159
+ if (not hasattr(self, "user") or self.user is None) and self.created_by:
160
+ self.user = self.created_by
161
+
162
+ # Log the operation
163
+ if self.pk:
164
+ logit.info(f"Updating book: {self.title} (ID: {self.pk})")
165
+ else:
166
+ logit.info(f"Creating new book: {self.title}")
167
+
168
+ super().save(*args, **kwargs)
169
+
170
+ def delete(self, *args, **kwargs):
171
+ """Override delete to log the operation"""
172
+ logit.info(f"Deleting book: {self.title} (ID: {self.pk})")
173
+ super().delete(*args, **kwargs)
174
+
175
+ def get_pages(self, published_only=True):
176
+ """Get all pages in this book"""
177
+ queryset = self.pages.all()
178
+ if published_only:
179
+ queryset = queryset.filter(is_published=True)
180
+ return queryset.order_by('-order_priority', 'title')
181
+
182
+ def get_root_pages(self, published_only=True):
183
+ """Get top-level pages (no parent) in this book"""
184
+ queryset = self.pages.filter(parent__isnull=True)
185
+ if published_only:
186
+ queryset = queryset.filter(is_published=True)
187
+ return queryset.order_by('-order_priority', 'title')
188
+
189
+ def get_assets(self):
190
+ """Get all assets associated with this book"""
191
+ return self.assets.all().order_by('-order_priority', 'id')
192
+
193
+ def can_user_view(self, user):
194
+ """
195
+ Check if a user can view this book
196
+
197
+ This provides fine-grained access control beyond the basic RestMeta permissions.
198
+ The Book model handles detailed access logic here.
199
+ """
200
+ # Inactive books are not viewable
201
+ if not self.is_active:
202
+ return False
203
+
204
+ # Owner can always view
205
+ if self.user == user:
206
+ return True
207
+
208
+ # Group members can view if no specific restrictions
209
+ if user and user.groups.filter(id=self.group.id).exists():
210
+ return True
211
+
212
+ # Check custom permissions if defined
213
+ if self.permissions:
214
+ # This is where we'd implement custom permission logic
215
+ # For now, return True for basic implementation
216
+ return True
217
+
218
+ # Default to allowing view (since RestMeta has public view)
219
+ return True
220
+
221
+ def get_page_count(self):
222
+ """Get total number of published pages in this book"""
223
+ return self.pages.filter(is_published=True).count()
224
+
225
+ def get_asset_count(self):
226
+ """Get total number of assets in this book"""
227
+ return self.assets.count()