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