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.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- 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 +319 -15
- mojo/apps/account/models/member.py +29 -5
- 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 +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- 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 +7 -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/s3.py +64 -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/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- 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 +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- 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/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- 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 +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -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 +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- 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/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- 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/bounce.py +0 -0
- 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 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
import os
|
2
|
+
import logging
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from typing import Dict, List, Optional, Tuple, Any, Union
|
5
|
+
from django.conf import settings
|
6
|
+
from mojo.apps.fileman.models import File, FileRendition
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
class RenditionRole:
|
11
|
+
"""
|
12
|
+
Predefined roles for file renditions
|
13
|
+
"""
|
14
|
+
# Common roles
|
15
|
+
ORIGINAL = 'original'
|
16
|
+
THUMBNAIL = 'thumbnail'
|
17
|
+
PREVIEW = 'preview'
|
18
|
+
|
19
|
+
# Image-specific roles
|
20
|
+
THUMBNAIL_SM = 'thumbnail_sm'
|
21
|
+
THUMBNAIL_MD = 'thumbnail_md'
|
22
|
+
THUMBNAIL_LG = 'thumbnail_lg'
|
23
|
+
SQUARE_SM = 'square_sm'
|
24
|
+
SQUARE_MD = 'square_md'
|
25
|
+
SQUARE_LG = 'square_lg'
|
26
|
+
|
27
|
+
# Video-specific roles
|
28
|
+
VIDEO_THUMBNAIL = 'video_thumbnail'
|
29
|
+
VIDEO_PREVIEW = 'video_preview'
|
30
|
+
VIDEO_MP4 = 'video_mp4'
|
31
|
+
VIDEO_WEBM = 'video_webm'
|
32
|
+
|
33
|
+
# Document-specific roles
|
34
|
+
DOCUMENT_THUMBNAIL = 'document_thumbnail'
|
35
|
+
DOCUMENT_PREVIEW = 'document_preview'
|
36
|
+
DOCUMENT_PDF = 'document_pdf'
|
37
|
+
|
38
|
+
# Audio-specific roles
|
39
|
+
AUDIO_THUMBNAIL = 'audio_thumbnail'
|
40
|
+
AUDIO_PREVIEW = 'audio_preview'
|
41
|
+
AUDIO_MP3 = 'audio_mp3'
|
42
|
+
|
43
|
+
|
44
|
+
class BaseRenderer(ABC):
|
45
|
+
"""
|
46
|
+
Base class for file renderers
|
47
|
+
|
48
|
+
A renderer creates different versions (renditions) of a file based on
|
49
|
+
predefined roles. Each renderer supports specific file categories and
|
50
|
+
provides implementations for creating renditions.
|
51
|
+
"""
|
52
|
+
|
53
|
+
# The file categories this renderer supports
|
54
|
+
supported_categories = []
|
55
|
+
|
56
|
+
# Default rendition definitions:
|
57
|
+
# mapping of role -> (width, height, options)
|
58
|
+
default_renditions = {}
|
59
|
+
|
60
|
+
def __init__(self, file: File):
|
61
|
+
"""
|
62
|
+
Initialize renderer with a file
|
63
|
+
|
64
|
+
Args:
|
65
|
+
file: The original file to create renditions from
|
66
|
+
"""
|
67
|
+
self.file = file
|
68
|
+
self.renditions = {}
|
69
|
+
self._load_existing_renditions()
|
70
|
+
|
71
|
+
def _load_existing_renditions(self):
|
72
|
+
"""Load existing renditions for this file"""
|
73
|
+
for rendition in FileRendition.objects.filter(original_file=self.file):
|
74
|
+
self.renditions[rendition.role] = rendition
|
75
|
+
|
76
|
+
@classmethod
|
77
|
+
def supports_file(cls, file: File) -> bool:
|
78
|
+
"""
|
79
|
+
Check if this renderer supports the given file
|
80
|
+
|
81
|
+
Args:
|
82
|
+
file: The file to check
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
bool: True if this renderer supports the file, False otherwise
|
86
|
+
"""
|
87
|
+
return file.category in cls.supported_categories
|
88
|
+
|
89
|
+
@abstractmethod
|
90
|
+
def create_rendition(self, role: str, options: Dict = None) -> Optional[FileRendition]:
|
91
|
+
"""
|
92
|
+
Create a rendition for the specified role
|
93
|
+
|
94
|
+
Args:
|
95
|
+
role: The role of the rendition (e.g., 'thumbnail', 'preview')
|
96
|
+
options: Additional options for creating the rendition
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
FileRendition: The created rendition, or None if creation failed
|
100
|
+
"""
|
101
|
+
pass
|
102
|
+
|
103
|
+
def get_rendition(self, role: str, create_if_missing: bool = True) -> Optional[FileRendition]:
|
104
|
+
"""
|
105
|
+
Get a rendition for the specified role
|
106
|
+
|
107
|
+
Args:
|
108
|
+
role: The role of the rendition
|
109
|
+
create_if_missing: Whether to create the rendition if it doesn't exist
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
FileRendition: The rendition, or None if not found and not created
|
113
|
+
"""
|
114
|
+
if role in self.renditions:
|
115
|
+
return self.renditions[role]
|
116
|
+
|
117
|
+
if create_if_missing:
|
118
|
+
options = self.default_renditions.get(role, {})
|
119
|
+
rendition = self.create_rendition(role, options)
|
120
|
+
if rendition:
|
121
|
+
self.renditions[role] = rendition
|
122
|
+
return rendition
|
123
|
+
|
124
|
+
return None
|
125
|
+
|
126
|
+
def create_all_renditions(self) -> List[FileRendition]:
|
127
|
+
"""
|
128
|
+
Create all default renditions for this file
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
List[FileRendition]: List of created renditions
|
132
|
+
"""
|
133
|
+
results = []
|
134
|
+
for role, options in self.default_renditions.items():
|
135
|
+
rendition = self.get_rendition(role)
|
136
|
+
if rendition:
|
137
|
+
results.append(rendition)
|
138
|
+
return results
|
139
|
+
|
140
|
+
def cleanup_renditions(self):
|
141
|
+
"""
|
142
|
+
Remove all renditions for this file
|
143
|
+
"""
|
144
|
+
FileRendition.objects.filter(original_file=self.file).delete()
|
145
|
+
self.renditions = {}
|
146
|
+
|
147
|
+
def _create_rendition_object(self, role: str, filename: str, storage_path: str,
|
148
|
+
content_type: str, category: str, file_size: int = None) -> FileRendition:
|
149
|
+
"""
|
150
|
+
Create a FileRendition object in the database
|
151
|
+
|
152
|
+
Args:
|
153
|
+
role: The role of the rendition
|
154
|
+
filename: The filename of the rendition
|
155
|
+
storage_path: The storage path of the rendition
|
156
|
+
content_type: The MIME type of the rendition
|
157
|
+
category: The category of the rendition
|
158
|
+
file_size: The size of the rendition in bytes
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
FileRendition: The created rendition object
|
162
|
+
"""
|
163
|
+
rendition = FileRendition(
|
164
|
+
original_file=self.file,
|
165
|
+
role=role,
|
166
|
+
filename=filename,
|
167
|
+
storage_path=storage_path,
|
168
|
+
content_type=content_type,
|
169
|
+
category=category,
|
170
|
+
file_size=file_size,
|
171
|
+
upload_status=FileRendition.COMPLETED
|
172
|
+
)
|
173
|
+
rendition.save()
|
174
|
+
return rendition
|
175
|
+
|
176
|
+
def get_temp_path(self, suffix: str = '') -> str:
|
177
|
+
"""
|
178
|
+
Get a temporary file path for processing
|
179
|
+
|
180
|
+
Args:
|
181
|
+
suffix: Optional suffix for the temp file (e.g., '.jpg')
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
str: Path to a temporary file
|
185
|
+
"""
|
186
|
+
import tempfile
|
187
|
+
temp_dir = getattr(settings, 'MOJO_TEMP_DIR', None)
|
188
|
+
if temp_dir:
|
189
|
+
os.makedirs(temp_dir, exist_ok=True)
|
190
|
+
return os.path.join(temp_dir, f"{self.file.id}_{suffix}")
|
191
|
+
return tempfile.mktemp(suffix=suffix)
|
192
|
+
|
193
|
+
@staticmethod
|
194
|
+
def get_renderer_for_file(file: File) -> Optional['BaseRenderer']:
|
195
|
+
"""
|
196
|
+
Get the appropriate renderer for a file
|
197
|
+
|
198
|
+
Args:
|
199
|
+
file: The file to get a renderer for
|
200
|
+
|
201
|
+
Returns:
|
202
|
+
BaseRenderer: The renderer instance, or None if no renderer supports the file
|
203
|
+
"""
|
204
|
+
from mojo.apps.fileman.renderer import get_renderer_for_file
|
205
|
+
return get_renderer_for_file(file)
|
@@ -0,0 +1,404 @@
|
|
1
|
+
import os
|
2
|
+
import io
|
3
|
+
import subprocess
|
4
|
+
import logging
|
5
|
+
import mimetypes
|
6
|
+
import tempfile
|
7
|
+
from typing import Dict, Optional, Tuple, Union, BinaryIO, List
|
8
|
+
import shutil
|
9
|
+
|
10
|
+
from mojo.apps.fileman.models import File, FileRendition
|
11
|
+
from mojo.apps.fileman.renderer.base import BaseRenderer, RenditionRole
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
class DocumentRenderer(BaseRenderer):
|
16
|
+
"""
|
17
|
+
Renderer for document files
|
18
|
+
|
19
|
+
Creates various renditions like thumbnails and previews for document files
|
20
|
+
such as PDFs, Word documents, Excel spreadsheets, etc.
|
21
|
+
"""
|
22
|
+
|
23
|
+
# Document file categories
|
24
|
+
supported_categories = ['document', 'pdf', 'spreadsheet', 'presentation']
|
25
|
+
|
26
|
+
# Default rendition definitions with options
|
27
|
+
default_renditions = {
|
28
|
+
RenditionRole.DOCUMENT_THUMBNAIL: {
|
29
|
+
'width': 300,
|
30
|
+
'height': 424, # Roughly A4 proportions
|
31
|
+
'format': 'jpg',
|
32
|
+
'page': 1
|
33
|
+
},
|
34
|
+
RenditionRole.THUMBNAIL: {
|
35
|
+
'width': 200,
|
36
|
+
'height': 283, # Roughly A4 proportions
|
37
|
+
'format': 'jpg',
|
38
|
+
'page': 1
|
39
|
+
},
|
40
|
+
RenditionRole.DOCUMENT_PREVIEW: {
|
41
|
+
'format': 'pdf',
|
42
|
+
'quality': 'medium',
|
43
|
+
'max_pages': 20, # Limit preview to first 20 pages
|
44
|
+
},
|
45
|
+
RenditionRole.DOCUMENT_PDF: {
|
46
|
+
'format': 'pdf',
|
47
|
+
'quality': 'high',
|
48
|
+
},
|
49
|
+
}
|
50
|
+
|
51
|
+
# Document format conversions supported
|
52
|
+
conversion_map = {
|
53
|
+
# Office formats
|
54
|
+
'.doc': 'pdf',
|
55
|
+
'.docx': 'pdf',
|
56
|
+
'.xls': 'pdf',
|
57
|
+
'.xlsx': 'pdf',
|
58
|
+
'.ppt': 'pdf',
|
59
|
+
'.pptx': 'pdf',
|
60
|
+
'.odt': 'pdf',
|
61
|
+
'.ods': 'pdf',
|
62
|
+
'.odp': 'pdf',
|
63
|
+
# Text formats
|
64
|
+
'.txt': 'pdf',
|
65
|
+
'.rtf': 'pdf',
|
66
|
+
'.md': 'pdf',
|
67
|
+
# Other formats
|
68
|
+
'.epub': 'pdf',
|
69
|
+
}
|
70
|
+
|
71
|
+
def __init__(self, file: File):
|
72
|
+
super().__init__(file)
|
73
|
+
# Check if required tools are available
|
74
|
+
self._check_dependencies()
|
75
|
+
|
76
|
+
def _check_dependencies(self):
|
77
|
+
"""Check if required tools are available in the system"""
|
78
|
+
# Check for pdftoppm (for PDF thumbnails)
|
79
|
+
try:
|
80
|
+
subprocess.run(["pdftoppm", "-v"],
|
81
|
+
stdout=subprocess.PIPE,
|
82
|
+
stderr=subprocess.PIPE,
|
83
|
+
check=True)
|
84
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
85
|
+
logger.warning("pdftoppm is not available. PDF thumbnail generation may not work properly.")
|
86
|
+
|
87
|
+
# Check for LibreOffice (for document conversion)
|
88
|
+
try:
|
89
|
+
subprocess.run(["libreoffice", "--version"],
|
90
|
+
stdout=subprocess.PIPE,
|
91
|
+
stderr=subprocess.PIPE,
|
92
|
+
check=True)
|
93
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
94
|
+
logger.warning("LibreOffice is not available. Document conversion may not work properly.")
|
95
|
+
|
96
|
+
def _download_original(self) -> Union[str, None]:
|
97
|
+
"""
|
98
|
+
Download the original file to a temporary location
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
str: Path to the downloaded file, or None if download failed
|
102
|
+
"""
|
103
|
+
try:
|
104
|
+
file_manager = self.file.file_manager
|
105
|
+
backend = file_manager.backend
|
106
|
+
|
107
|
+
# Get file extension
|
108
|
+
_, ext = os.path.splitext(self.file.filename)
|
109
|
+
temp_path = self.get_temp_path(ext)
|
110
|
+
|
111
|
+
# Download file from storage
|
112
|
+
with open(temp_path, 'wb') as f:
|
113
|
+
backend.download(self.file.storage_file_path, f)
|
114
|
+
|
115
|
+
return temp_path
|
116
|
+
except Exception as e:
|
117
|
+
logger.error(f"Failed to download original document file: {str(e)}")
|
118
|
+
return None
|
119
|
+
|
120
|
+
def _convert_to_pdf(self, source_path: str) -> Tuple[str, bool]:
|
121
|
+
"""
|
122
|
+
Convert document to PDF using LibreOffice
|
123
|
+
|
124
|
+
Args:
|
125
|
+
source_path: Path to the source document
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
Tuple[str, bool]: (Output PDF path, success status)
|
129
|
+
"""
|
130
|
+
_, ext = os.path.splitext(source_path.lower())
|
131
|
+
|
132
|
+
# If already PDF, just return the path
|
133
|
+
if ext == '.pdf':
|
134
|
+
return source_path, True
|
135
|
+
|
136
|
+
# Check if we support converting this format
|
137
|
+
if ext not in self.conversion_map:
|
138
|
+
logger.warning(f"Unsupported document format for conversion: {ext}")
|
139
|
+
return None, False
|
140
|
+
|
141
|
+
# Create a temporary directory for the conversion
|
142
|
+
temp_dir = tempfile.mkdtemp()
|
143
|
+
try:
|
144
|
+
# Copy the source file to the temp directory
|
145
|
+
temp_input = os.path.join(temp_dir, os.path.basename(source_path))
|
146
|
+
shutil.copy2(source_path, temp_input)
|
147
|
+
|
148
|
+
# Use LibreOffice to convert to PDF
|
149
|
+
cmd = [
|
150
|
+
"libreoffice",
|
151
|
+
"--headless",
|
152
|
+
"--convert-to", "pdf",
|
153
|
+
"--outdir", temp_dir,
|
154
|
+
temp_input
|
155
|
+
]
|
156
|
+
|
157
|
+
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
158
|
+
|
159
|
+
# Find the output PDF
|
160
|
+
base_name = os.path.splitext(os.path.basename(source_path))[0]
|
161
|
+
output_pdf = os.path.join(temp_dir, f"{base_name}.pdf")
|
162
|
+
|
163
|
+
if not os.path.exists(output_pdf):
|
164
|
+
logger.error("PDF conversion failed - output file not found")
|
165
|
+
return None, False
|
166
|
+
|
167
|
+
# Copy to a location outside the temp dir
|
168
|
+
final_pdf = self.get_temp_path(".pdf")
|
169
|
+
shutil.copy2(output_pdf, final_pdf)
|
170
|
+
|
171
|
+
return final_pdf, True
|
172
|
+
|
173
|
+
except subprocess.SubprocessError as e:
|
174
|
+
logger.error(f"Document conversion failed: {str(e)}")
|
175
|
+
return None, False
|
176
|
+
finally:
|
177
|
+
# Clean up temp directory
|
178
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
179
|
+
|
180
|
+
def _create_pdf_thumbnail(self, pdf_path: str, width: int, height: int,
|
181
|
+
page: int, output_format: str) -> Tuple[str, str, int]:
|
182
|
+
"""
|
183
|
+
Create a thumbnail from a PDF
|
184
|
+
|
185
|
+
Args:
|
186
|
+
pdf_path: Path to the PDF
|
187
|
+
width: Target width
|
188
|
+
height: Target height
|
189
|
+
page: Page number to use (1-based)
|
190
|
+
output_format: Output format (jpg, png)
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
Tuple[str, str, int]: (Output path, mime type, file size)
|
194
|
+
"""
|
195
|
+
temp_prefix = self.get_temp_path("")
|
196
|
+
|
197
|
+
try:
|
198
|
+
# Use pdftoppm to extract page as image
|
199
|
+
cmd = [
|
200
|
+
"pdftoppm",
|
201
|
+
"-f", str(page), # First page
|
202
|
+
"-l", str(page), # Last page (same as first)
|
203
|
+
"-scale-to-x", str(width),
|
204
|
+
"-scale-to-y", str(height),
|
205
|
+
"-singlefile", # Output a single file
|
206
|
+
]
|
207
|
+
|
208
|
+
# Set format
|
209
|
+
if output_format.lower() == 'jpg':
|
210
|
+
cmd.append("-jpeg")
|
211
|
+
elif output_format.lower() == 'png':
|
212
|
+
cmd.append("-png")
|
213
|
+
|
214
|
+
# Add input and output paths
|
215
|
+
cmd.extend([pdf_path, temp_prefix])
|
216
|
+
|
217
|
+
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
218
|
+
|
219
|
+
# pdftoppm adds the format as suffix
|
220
|
+
output_file = f"{temp_prefix}.{output_format}"
|
221
|
+
|
222
|
+
if not os.path.exists(output_file):
|
223
|
+
logger.error("PDF thumbnail generation failed - output file not found")
|
224
|
+
return None, None, 0
|
225
|
+
|
226
|
+
# Get file size
|
227
|
+
file_size = os.path.getsize(output_file)
|
228
|
+
|
229
|
+
# Get mime type
|
230
|
+
mime_type = mimetypes.guess_type(f"file.{output_format}")[0]
|
231
|
+
|
232
|
+
return output_file, mime_type, file_size
|
233
|
+
|
234
|
+
except subprocess.SubprocessError as e:
|
235
|
+
logger.error(f"Failed to create PDF thumbnail: {str(e)}")
|
236
|
+
return None, None, 0
|
237
|
+
|
238
|
+
def _optimize_pdf(self, pdf_path: str, quality: str = 'medium') -> Tuple[str, int]:
|
239
|
+
"""
|
240
|
+
Optimize a PDF file to reduce size
|
241
|
+
|
242
|
+
Args:
|
243
|
+
pdf_path: Path to the PDF
|
244
|
+
quality: Quality level ('low', 'medium', 'high')
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
Tuple[str, int]: (Output path, file size)
|
248
|
+
"""
|
249
|
+
output_path = self.get_temp_path(".pdf")
|
250
|
+
|
251
|
+
try:
|
252
|
+
# Set Ghostscript parameters based on quality
|
253
|
+
if quality == 'low':
|
254
|
+
params = ["-dPDFSETTINGS=/screen"] # lowest quality, smallest size
|
255
|
+
elif quality == 'medium':
|
256
|
+
params = ["-dPDFSETTINGS=/ebook"] # medium quality, medium size
|
257
|
+
else: # high
|
258
|
+
params = ["-dPDFSETTINGS=/prepress"] # high quality, larger size
|
259
|
+
|
260
|
+
# Use Ghostscript to optimize
|
261
|
+
cmd = [
|
262
|
+
"gs",
|
263
|
+
"-sDEVICE=pdfwrite",
|
264
|
+
"-dCompatibilityLevel=1.4",
|
265
|
+
"-dNOPAUSE",
|
266
|
+
"-dQUIET",
|
267
|
+
"-dBATCH",
|
268
|
+
] + params + [
|
269
|
+
f"-sOutputFile={output_path}",
|
270
|
+
pdf_path
|
271
|
+
]
|
272
|
+
|
273
|
+
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
274
|
+
|
275
|
+
if not os.path.exists(output_path):
|
276
|
+
logger.error("PDF optimization failed - output file not found")
|
277
|
+
return pdf_path, os.path.getsize(pdf_path)
|
278
|
+
|
279
|
+
# Get file size
|
280
|
+
file_size = os.path.getsize(output_path)
|
281
|
+
|
282
|
+
return output_path, file_size
|
283
|
+
|
284
|
+
except subprocess.SubprocessError as e:
|
285
|
+
logger.error(f"Failed to optimize PDF: {str(e)}")
|
286
|
+
return pdf_path, os.path.getsize(pdf_path)
|
287
|
+
|
288
|
+
def create_rendition(self, role: str, options: Dict = None) -> Optional[FileRendition]:
|
289
|
+
"""
|
290
|
+
Create a document rendition for the specified role
|
291
|
+
|
292
|
+
Args:
|
293
|
+
role: The role of the rendition
|
294
|
+
options: Additional options for creating the rendition
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
FileRendition: The created rendition, or None if creation failed
|
298
|
+
"""
|
299
|
+
try:
|
300
|
+
# Get rendition settings
|
301
|
+
settings = self.default_renditions.get(role, {})
|
302
|
+
if options:
|
303
|
+
settings.update(options)
|
304
|
+
|
305
|
+
# Download the original file
|
306
|
+
source_path = self._download_original()
|
307
|
+
if not source_path:
|
308
|
+
return None
|
309
|
+
|
310
|
+
temp_files = [source_path] # Track temporary files to clean up
|
311
|
+
|
312
|
+
try:
|
313
|
+
# First convert to PDF if needed
|
314
|
+
if role in [RenditionRole.DOCUMENT_PDF, RenditionRole.DOCUMENT_PREVIEW]:
|
315
|
+
pdf_path, success = self._convert_to_pdf(source_path)
|
316
|
+
if not success:
|
317
|
+
return None
|
318
|
+
|
319
|
+
temp_files.append(pdf_path)
|
320
|
+
|
321
|
+
# For preview or PDF rendition
|
322
|
+
quality = settings.get('quality', 'medium')
|
323
|
+
|
324
|
+
# Optimize the PDF
|
325
|
+
optimized_pdf, file_size = self._optimize_pdf(pdf_path, quality)
|
326
|
+
temp_files.append(optimized_pdf)
|
327
|
+
|
328
|
+
# Set output details
|
329
|
+
temp_output = optimized_pdf
|
330
|
+
mime_type = "application/pdf"
|
331
|
+
|
332
|
+
# Set filename
|
333
|
+
name, _ = os.path.splitext(self.file.filename)
|
334
|
+
filename = f"{name}_{role}.pdf"
|
335
|
+
category = 'document'
|
336
|
+
|
337
|
+
elif role in [RenditionRole.THUMBNAIL, RenditionRole.DOCUMENT_THUMBNAIL]:
|
338
|
+
# First make sure we have a PDF
|
339
|
+
pdf_path, success = self._convert_to_pdf(source_path)
|
340
|
+
if not success:
|
341
|
+
return None
|
342
|
+
|
343
|
+
temp_files.append(pdf_path)
|
344
|
+
|
345
|
+
# Create thumbnail image
|
346
|
+
width = settings.get('width', 200)
|
347
|
+
height = settings.get('height', 283)
|
348
|
+
page = settings.get('page', 1)
|
349
|
+
output_format = settings.get('format', 'jpg')
|
350
|
+
|
351
|
+
temp_output, mime_type, file_size = self._create_pdf_thumbnail(
|
352
|
+
pdf_path, width, height, page, output_format
|
353
|
+
)
|
354
|
+
|
355
|
+
if not temp_output:
|
356
|
+
return None
|
357
|
+
|
358
|
+
temp_files.append(temp_output)
|
359
|
+
|
360
|
+
# Set filename
|
361
|
+
name, _ = os.path.splitext(self.file.filename)
|
362
|
+
filename = f"{name}_{role}.{output_format}"
|
363
|
+
category = 'image' # Thumbnails are images
|
364
|
+
|
365
|
+
else:
|
366
|
+
logger.warning(f"Unsupported rendition role for documents: {role}")
|
367
|
+
return None
|
368
|
+
|
369
|
+
# Save to storage
|
370
|
+
file_manager = self.file.file_manager
|
371
|
+
backend = file_manager.backend
|
372
|
+
storage_path = os.path.join(
|
373
|
+
os.path.dirname(self.file.storage_file_path),
|
374
|
+
filename
|
375
|
+
)
|
376
|
+
|
377
|
+
# Upload to storage
|
378
|
+
with open(temp_output, 'rb') as f:
|
379
|
+
backend.save(f, storage_path, mime_type)
|
380
|
+
|
381
|
+
# Create rendition record
|
382
|
+
rendition = self._create_rendition_object(
|
383
|
+
role=role,
|
384
|
+
filename=filename,
|
385
|
+
storage_path=storage_path,
|
386
|
+
content_type=mime_type,
|
387
|
+
category=category,
|
388
|
+
file_size=file_size
|
389
|
+
)
|
390
|
+
|
391
|
+
return rendition
|
392
|
+
|
393
|
+
finally:
|
394
|
+
# Clean up temporary files
|
395
|
+
for temp_file in temp_files:
|
396
|
+
if temp_file and os.path.exists(temp_file):
|
397
|
+
try:
|
398
|
+
os.unlink(temp_file)
|
399
|
+
except:
|
400
|
+
pass
|
401
|
+
|
402
|
+
except Exception as e:
|
403
|
+
logger.error(f"Failed to create document rendition '{role}': {str(e)}")
|
404
|
+
return None
|