django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__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.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.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 +279 -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.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.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,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,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)
|