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,651 @@
|
|
1
|
+
"""
|
2
|
+
Django management command for serializer administration and benchmarking.
|
3
|
+
|
4
|
+
Provides comprehensive serializer management capabilities including:
|
5
|
+
- Performance benchmarking and comparison
|
6
|
+
- Serializer registration and configuration
|
7
|
+
- Cache management and statistics
|
8
|
+
- Health checks and diagnostics
|
9
|
+
|
10
|
+
Usage:
|
11
|
+
# List all registered serializers
|
12
|
+
python manage.py serializer_admin list
|
13
|
+
|
14
|
+
# Benchmark serializers
|
15
|
+
python manage.py serializer_admin benchmark --model MyModel --count 1000
|
16
|
+
|
17
|
+
# Set default serializer
|
18
|
+
python manage.py serializer_admin set-default optimized
|
19
|
+
|
20
|
+
# Get performance statistics
|
21
|
+
python manage.py serializer_admin stats
|
22
|
+
|
23
|
+
# Clear caches
|
24
|
+
python manage.py serializer_admin clear-cache
|
25
|
+
"""
|
26
|
+
|
27
|
+
import time
|
28
|
+
import json
|
29
|
+
from django.core.management.base import BaseCommand, CommandError
|
30
|
+
from django.apps import apps
|
31
|
+
from django.db import models
|
32
|
+
from django.core.management import color
|
33
|
+
|
34
|
+
from mojo.serializers import (
|
35
|
+
get_serializer_manager,
|
36
|
+
get_performance_stats,
|
37
|
+
clear_serializer_caches,
|
38
|
+
benchmark_serializers,
|
39
|
+
list_serializers,
|
40
|
+
set_default_serializer,
|
41
|
+
HAS_UJSON,
|
42
|
+
UJSON_VERSION
|
43
|
+
)
|
44
|
+
|
45
|
+
# Import cache system for enhanced functionality
|
46
|
+
try:
|
47
|
+
from mojo.serializers.core.cache import (
|
48
|
+
get_cache_backend,
|
49
|
+
get_cache_stats,
|
50
|
+
get_available_backends,
|
51
|
+
test_backend_connectivity,
|
52
|
+
get_cache_health
|
53
|
+
)
|
54
|
+
HAS_CACHE_SYSTEM = True
|
55
|
+
except ImportError:
|
56
|
+
HAS_CACHE_SYSTEM = False
|
57
|
+
|
58
|
+
|
59
|
+
class Command(BaseCommand):
|
60
|
+
help = 'Manage MOJO serializers: benchmark, configure, and monitor performance'
|
61
|
+
|
62
|
+
def add_arguments(self, parser):
|
63
|
+
"""Add command line arguments."""
|
64
|
+
|
65
|
+
subparsers = parser.add_subparsers(dest='action', help='Available actions')
|
66
|
+
|
67
|
+
# List serializers
|
68
|
+
list_parser = subparsers.add_parser('list', help='List all registered serializers')
|
69
|
+
list_parser.add_argument(
|
70
|
+
'--detail',
|
71
|
+
action='store_true',
|
72
|
+
help='Show detailed information about each serializer'
|
73
|
+
)
|
74
|
+
|
75
|
+
# Benchmark serializers
|
76
|
+
bench_parser = subparsers.add_parser('benchmark', help='Benchmark serializer performance')
|
77
|
+
bench_parser.add_argument(
|
78
|
+
'--model',
|
79
|
+
type=str,
|
80
|
+
required=True,
|
81
|
+
help='Model name to benchmark (format: app.ModelName or ModelName)'
|
82
|
+
)
|
83
|
+
bench_parser.add_argument(
|
84
|
+
'--count',
|
85
|
+
type=int,
|
86
|
+
default=100,
|
87
|
+
help='Number of objects to serialize (default: 100)'
|
88
|
+
)
|
89
|
+
bench_parser.add_argument(
|
90
|
+
'--graph',
|
91
|
+
type=str,
|
92
|
+
default='default',
|
93
|
+
help='Graph configuration to use (default: "default")'
|
94
|
+
)
|
95
|
+
bench_parser.add_argument(
|
96
|
+
'--iterations',
|
97
|
+
type=int,
|
98
|
+
default=5,
|
99
|
+
help='Number of benchmark iterations (default: 5)'
|
100
|
+
)
|
101
|
+
bench_parser.add_argument(
|
102
|
+
'--serializers',
|
103
|
+
nargs='+',
|
104
|
+
help='Specific serializers to benchmark (default: all)'
|
105
|
+
)
|
106
|
+
bench_parser.add_argument(
|
107
|
+
'--output-json',
|
108
|
+
type=str,
|
109
|
+
help='Save results to JSON file'
|
110
|
+
)
|
111
|
+
|
112
|
+
# Set default serializer
|
113
|
+
default_parser = subparsers.add_parser('set-default', help='Set default serializer')
|
114
|
+
default_parser.add_argument(
|
115
|
+
'serializer_name',
|
116
|
+
type=str,
|
117
|
+
help='Name of serializer to set as default'
|
118
|
+
)
|
119
|
+
|
120
|
+
# Performance statistics
|
121
|
+
stats_parser = subparsers.add_parser('stats', help='Show performance statistics')
|
122
|
+
stats_parser.add_argument(
|
123
|
+
'--clear',
|
124
|
+
action='store_true',
|
125
|
+
help='Clear statistics after showing them'
|
126
|
+
)
|
127
|
+
stats_parser.add_argument(
|
128
|
+
'--json',
|
129
|
+
action='store_true',
|
130
|
+
help='Output statistics as JSON'
|
131
|
+
)
|
132
|
+
|
133
|
+
# Cache management
|
134
|
+
cache_parser = subparsers.add_parser('clear-cache', help='Clear serializer caches')
|
135
|
+
cache_parser.add_argument(
|
136
|
+
'--serializer',
|
137
|
+
type=str,
|
138
|
+
help='Clear cache for specific serializer (default: all)'
|
139
|
+
)
|
140
|
+
|
141
|
+
# Health check
|
142
|
+
health_parser = subparsers.add_parser('health', help='Run serializer health checks')
|
143
|
+
health_parser.add_argument(
|
144
|
+
'--model',
|
145
|
+
type=str,
|
146
|
+
help='Test specific model (format: app.ModelName or ModelName)'
|
147
|
+
)
|
148
|
+
|
149
|
+
# Test serializers
|
150
|
+
test_parser = subparsers.add_parser('test', help='Test serializer functionality')
|
151
|
+
test_parser.add_argument(
|
152
|
+
'--model',
|
153
|
+
type=str,
|
154
|
+
required=True,
|
155
|
+
help='Model to test (format: app.ModelName or ModelName)'
|
156
|
+
)
|
157
|
+
test_parser.add_argument(
|
158
|
+
'--graph',
|
159
|
+
type=str,
|
160
|
+
default='default',
|
161
|
+
help='Graph configuration to test'
|
162
|
+
)
|
163
|
+
|
164
|
+
# Cache information
|
165
|
+
cache_parser = subparsers.add_parser('cache-info', help='Show detailed cache information')
|
166
|
+
cache_parser.add_argument(
|
167
|
+
'--test-connectivity',
|
168
|
+
action='store_true',
|
169
|
+
help='Test cache backend connectivity'
|
170
|
+
)
|
171
|
+
|
172
|
+
def handle(self, *args, **options):
|
173
|
+
"""Handle command execution."""
|
174
|
+
action = options.get('action')
|
175
|
+
|
176
|
+
if not action:
|
177
|
+
self.print_help()
|
178
|
+
return
|
179
|
+
|
180
|
+
# Route to appropriate handler
|
181
|
+
handler_map = {
|
182
|
+
'list': self.handle_list,
|
183
|
+
'benchmark': self.handle_benchmark,
|
184
|
+
'set-default': self.handle_set_default,
|
185
|
+
'stats': self.handle_stats,
|
186
|
+
'clear-cache': self.handle_clear_cache,
|
187
|
+
'health': self.handle_health,
|
188
|
+
'test': self.handle_test,
|
189
|
+
'cache-info': self.handle_cache_info,
|
190
|
+
}
|
191
|
+
|
192
|
+
handler = handler_map.get(action)
|
193
|
+
if handler:
|
194
|
+
try:
|
195
|
+
handler(options)
|
196
|
+
except Exception as e:
|
197
|
+
raise CommandError(f"Error executing {action}: {str(e)}")
|
198
|
+
else:
|
199
|
+
raise CommandError(f"Unknown action: {action}")
|
200
|
+
|
201
|
+
def handle_list(self, options):
|
202
|
+
"""List all registered serializers."""
|
203
|
+
serializers = list_serializers()
|
204
|
+
|
205
|
+
if not serializers:
|
206
|
+
self.stdout.write(
|
207
|
+
self.style.WARNING('No serializers registered')
|
208
|
+
)
|
209
|
+
return
|
210
|
+
|
211
|
+
self.stdout.write(
|
212
|
+
self.style.SUCCESS('Registered Serializers:')
|
213
|
+
)
|
214
|
+
|
215
|
+
for name, info in serializers.items():
|
216
|
+
status = " (DEFAULT)" if info['is_default'] else ""
|
217
|
+
self.stdout.write(f" • {name}{status}")
|
218
|
+
|
219
|
+
if options.get('detail'):
|
220
|
+
self.stdout.write(f" Class: {info['class_name']}")
|
221
|
+
self.stdout.write(f" Description: {info['description']}")
|
222
|
+
|
223
|
+
# Show ujson information
|
224
|
+
self.stdout.write(f"\nPerformance Information:")
|
225
|
+
if HAS_UJSON:
|
226
|
+
self.stdout.write(f" ✓ ujson {UJSON_VERSION} available - optimal JSON performance")
|
227
|
+
else:
|
228
|
+
self.stdout.write(f" ⚠ ujson not available - using standard json (slower)")
|
229
|
+
self.stdout.write(f" Install with: pip install ujson")
|
230
|
+
|
231
|
+
# Show cache backend information
|
232
|
+
if HAS_CACHE_SYSTEM:
|
233
|
+
try:
|
234
|
+
cache_backend = get_cache_backend()
|
235
|
+
cache_stats = cache_backend.stats()
|
236
|
+
backend_type = cache_stats.get('backend', 'unknown')
|
237
|
+
self.stdout.write(f" ✓ Cache backend: {backend_type}")
|
238
|
+
except Exception as e:
|
239
|
+
self.stdout.write(f" ⚠ Cache backend error: {e}")
|
240
|
+
|
241
|
+
def handle_benchmark(self, options):
|
242
|
+
"""Benchmark serializer performance."""
|
243
|
+
model_class = self.get_model_class(options['model'])
|
244
|
+
count = options['count']
|
245
|
+
graph = options['graph']
|
246
|
+
iterations = options['iterations']
|
247
|
+
serializer_types = options.get('serializers')
|
248
|
+
|
249
|
+
# Check if model has enough instances
|
250
|
+
total_instances = model_class.objects.count()
|
251
|
+
if total_instances < count:
|
252
|
+
self.stdout.write(
|
253
|
+
self.style.WARNING(
|
254
|
+
f"Model {model_class.__name__} only has {total_instances} instances, "
|
255
|
+
f"but {count} requested. Using available instances."
|
256
|
+
)
|
257
|
+
)
|
258
|
+
count = min(count, total_instances)
|
259
|
+
|
260
|
+
if count == 0:
|
261
|
+
raise CommandError(f"No instances found for model {model_class.__name__}")
|
262
|
+
|
263
|
+
# Get test queryset
|
264
|
+
test_queryset = model_class.objects.all()[:count]
|
265
|
+
|
266
|
+
self.stdout.write(
|
267
|
+
self.style.SUCCESS(
|
268
|
+
f"Benchmarking serializers with {count} {model_class.__name__} instances"
|
269
|
+
)
|
270
|
+
)
|
271
|
+
self.stdout.write(f"Graph: {graph}")
|
272
|
+
self.stdout.write(f"Iterations: {iterations}")
|
273
|
+
|
274
|
+
# Run benchmark
|
275
|
+
try:
|
276
|
+
results = benchmark_serializers(
|
277
|
+
instance=test_queryset,
|
278
|
+
graph=graph,
|
279
|
+
serializer_types=serializer_types,
|
280
|
+
iterations=iterations
|
281
|
+
)
|
282
|
+
|
283
|
+
# Display results
|
284
|
+
self.display_benchmark_results(results)
|
285
|
+
|
286
|
+
# Save to JSON if requested
|
287
|
+
if options.get('output_json'):
|
288
|
+
self.save_benchmark_results(results, options['output_json'])
|
289
|
+
|
290
|
+
except Exception as e:
|
291
|
+
raise CommandError(f"Benchmark failed: {str(e)}")
|
292
|
+
|
293
|
+
def handle_set_default(self, options):
|
294
|
+
"""Set default serializer."""
|
295
|
+
serializer_name = options['serializer_name']
|
296
|
+
|
297
|
+
if set_default_serializer(serializer_name):
|
298
|
+
self.stdout.write(
|
299
|
+
self.style.SUCCESS(
|
300
|
+
f"Default serializer set to: {serializer_name}"
|
301
|
+
)
|
302
|
+
)
|
303
|
+
else:
|
304
|
+
raise CommandError(f"Failed to set default serializer to {serializer_name}")
|
305
|
+
|
306
|
+
def handle_stats(self, options):
|
307
|
+
"""Show performance statistics."""
|
308
|
+
stats = get_performance_stats()
|
309
|
+
|
310
|
+
if options.get('json'):
|
311
|
+
self.stdout.write(json.dumps(stats, indent=2))
|
312
|
+
else:
|
313
|
+
self.display_stats(stats)
|
314
|
+
|
315
|
+
if options.get('clear'):
|
316
|
+
# Clear statistics if requested
|
317
|
+
manager = get_serializer_manager()
|
318
|
+
if hasattr(manager, 'reset_performance_stats'):
|
319
|
+
manager.reset_performance_stats()
|
320
|
+
self.stdout.write(
|
321
|
+
self.style.SUCCESS("Performance statistics cleared")
|
322
|
+
)
|
323
|
+
|
324
|
+
def handle_cache_info(self, options):
|
325
|
+
"""Show detailed cache information."""
|
326
|
+
if not HAS_CACHE_SYSTEM:
|
327
|
+
self.stdout.write(
|
328
|
+
self.style.ERROR("Cache system not available")
|
329
|
+
)
|
330
|
+
return
|
331
|
+
|
332
|
+
self.stdout.write(self.style.SUCCESS("Cache System Information:"))
|
333
|
+
|
334
|
+
try:
|
335
|
+
# Show available backends
|
336
|
+
backends = get_available_backends()
|
337
|
+
self.stdout.write(f"\nAvailable Backends:")
|
338
|
+
for name, info in backends.items():
|
339
|
+
status = "✓" if info['available'] else "✗"
|
340
|
+
self.stdout.write(f" {status} {name}: {info['description']}")
|
341
|
+
if info.get('ujson_available'):
|
342
|
+
self.stdout.write(f" ujson: {info.get('ujson_version', 'available')}")
|
343
|
+
if info.get('error'):
|
344
|
+
self.stdout.write(f" Error: {info['error']}")
|
345
|
+
|
346
|
+
# Show current backend health
|
347
|
+
health = get_cache_health()
|
348
|
+
self.stdout.write(f"\nCache Health: {health['status'].upper()}")
|
349
|
+
if health.get('issues'):
|
350
|
+
for issue in health['issues']:
|
351
|
+
self.stdout.write(f" ⚠ {issue}")
|
352
|
+
|
353
|
+
# Show recommendations
|
354
|
+
if health.get('recommendations'):
|
355
|
+
self.stdout.write(f"\nRecommendations:")
|
356
|
+
for rec in health['recommendations']:
|
357
|
+
self.stdout.write(f" • {rec}")
|
358
|
+
|
359
|
+
# Test connectivity if requested
|
360
|
+
if options.get('test_connectivity'):
|
361
|
+
self.stdout.write(f"\nTesting Cache Connectivity:")
|
362
|
+
backend_type = health.get('backend_type', 'unknown')
|
363
|
+
test_result = test_backend_connectivity(backend_type)
|
364
|
+
|
365
|
+
if test_result['connectivity']:
|
366
|
+
self.stdout.write(f" ✓ {backend_type} backend connectivity OK")
|
367
|
+
if test_result['functionality']:
|
368
|
+
perf = test_result.get('performance', {})
|
369
|
+
self.stdout.write(f" ✓ Functionality test passed")
|
370
|
+
if perf:
|
371
|
+
self.stdout.write(f" Set time: {perf.get('set_time_ms', 0)}ms")
|
372
|
+
self.stdout.write(f" Get time: {perf.get('get_time_ms', 0)}ms")
|
373
|
+
else:
|
374
|
+
self.stdout.write(f" ✗ Functionality test failed")
|
375
|
+
else:
|
376
|
+
self.stdout.write(f" ✗ {backend_type} backend connectivity failed")
|
377
|
+
|
378
|
+
for error in test_result.get('errors', []):
|
379
|
+
self.stdout.write(f" Error: {error}")
|
380
|
+
|
381
|
+
except Exception as e:
|
382
|
+
self.stdout.write(
|
383
|
+
self.style.ERROR(f"Error getting cache information: {e}")
|
384
|
+
)
|
385
|
+
|
386
|
+
def handle_clear_cache(self, options):
|
387
|
+
"""Clear serializer caches."""
|
388
|
+
serializer_name = options.get('serializer')
|
389
|
+
|
390
|
+
if serializer_name:
|
391
|
+
clear_serializer_caches(serializer_name)
|
392
|
+
self.stdout.write(
|
393
|
+
self.style.SUCCESS(f"Cache cleared for {serializer_name}")
|
394
|
+
)
|
395
|
+
else:
|
396
|
+
clear_serializer_caches()
|
397
|
+
self.stdout.write(
|
398
|
+
self.style.SUCCESS("All serializer caches cleared")
|
399
|
+
)
|
400
|
+
|
401
|
+
def handle_health(self, options):
|
402
|
+
"""Run serializer health checks."""
|
403
|
+
model_name = options.get('model')
|
404
|
+
|
405
|
+
if model_name:
|
406
|
+
# Test specific model
|
407
|
+
model_class = self.get_model_class(model_name)
|
408
|
+
self.run_model_health_check(model_class)
|
409
|
+
else:
|
410
|
+
# Test all MojoModel subclasses
|
411
|
+
self.run_full_health_check()
|
412
|
+
|
413
|
+
def handle_test(self, options):
|
414
|
+
"""Test serializer functionality."""
|
415
|
+
model_class = self.get_model_class(options['model'])
|
416
|
+
graph = options['graph']
|
417
|
+
|
418
|
+
# Get a test instance
|
419
|
+
instance = model_class.objects.first()
|
420
|
+
if not instance:
|
421
|
+
raise CommandError(f"No instances found for model {model_class.__name__}")
|
422
|
+
|
423
|
+
self.stdout.write(
|
424
|
+
self.style.SUCCESS(f"Testing {model_class.__name__} serialization")
|
425
|
+
)
|
426
|
+
|
427
|
+
# Test each registered serializer
|
428
|
+
serializers = list_serializers()
|
429
|
+
manager = get_serializer_manager()
|
430
|
+
|
431
|
+
for serializer_name in serializers.keys():
|
432
|
+
self.stdout.write(f"\nTesting {serializer_name}:")
|
433
|
+
|
434
|
+
try:
|
435
|
+
# Test single instance
|
436
|
+
serializer = manager.get_serializer(instance, graph=graph, serializer_type=serializer_name)
|
437
|
+
data = serializer.serialize()
|
438
|
+
json_output = serializer.to_json()
|
439
|
+
|
440
|
+
self.stdout.write(
|
441
|
+
self.style.SUCCESS(f" ✓ Single instance: {len(str(data))} chars")
|
442
|
+
)
|
443
|
+
|
444
|
+
# Test queryset
|
445
|
+
queryset = model_class.objects.all()[:5] # Test with 5 instances
|
446
|
+
serializer = manager.get_serializer(queryset, graph=graph, serializer_type=serializer_name, many=True)
|
447
|
+
list_data = serializer.serialize()
|
448
|
+
list_json = serializer.to_json()
|
449
|
+
|
450
|
+
self.stdout.write(
|
451
|
+
self.style.SUCCESS(f" ✓ QuerySet ({len(list_data)} items): {len(str(list_json))} chars")
|
452
|
+
)
|
453
|
+
|
454
|
+
except Exception as e:
|
455
|
+
self.stdout.write(
|
456
|
+
self.style.ERROR(f" ✗ Failed: {str(e)}")
|
457
|
+
)
|
458
|
+
|
459
|
+
def get_model_class(self, model_string):
|
460
|
+
"""Get model class from string."""
|
461
|
+
try:
|
462
|
+
if '.' in model_string:
|
463
|
+
app_label, model_name = model_string.split('.', 1)
|
464
|
+
return apps.get_model(app_label, model_name)
|
465
|
+
else:
|
466
|
+
# Try to find model in any app
|
467
|
+
for app_config in apps.get_app_configs():
|
468
|
+
try:
|
469
|
+
return app_config.get_model(model_string)
|
470
|
+
except LookupError:
|
471
|
+
continue
|
472
|
+
raise CommandError(f"Model '{model_string}' not found")
|
473
|
+
except Exception as e:
|
474
|
+
raise CommandError(f"Invalid model specification '{model_string}': {str(e)}")
|
475
|
+
|
476
|
+
def display_benchmark_results(self, results):
|
477
|
+
"""Display benchmark results in a formatted table."""
|
478
|
+
if not results:
|
479
|
+
self.stdout.write(self.style.WARNING("No benchmark results"))
|
480
|
+
return
|
481
|
+
|
482
|
+
self.stdout.write("\nBenchmark Results:")
|
483
|
+
self.stdout.write("=" * 80)
|
484
|
+
|
485
|
+
# Table header
|
486
|
+
header = f"{'Serializer':<15} {'Avg Time':<12} {'Min Time':<12} {'Max Time':<12} {'Obj/Sec':<10}"
|
487
|
+
self.stdout.write(header)
|
488
|
+
self.stdout.write("-" * 80)
|
489
|
+
|
490
|
+
# Sort by average time
|
491
|
+
sorted_results = sorted(
|
492
|
+
results.items(),
|
493
|
+
key=lambda x: x[1].get('avg_time', float('inf'))
|
494
|
+
)
|
495
|
+
|
496
|
+
for name, stats in sorted_results:
|
497
|
+
if 'error' in stats:
|
498
|
+
row = f"{name:<15} {stats['error']:<50}"
|
499
|
+
self.stdout.write(self.style.ERROR(row))
|
500
|
+
else:
|
501
|
+
avg_time = f"{stats['avg_time']:.4f}s"
|
502
|
+
min_time = f"{stats['min_time']:.4f}s"
|
503
|
+
max_time = f"{stats['max_time']:.4f}s"
|
504
|
+
obj_per_sec = f"{stats.get('objects_per_second', 0):.1f}"
|
505
|
+
|
506
|
+
row = f"{name:<15} {avg_time:<12} {min_time:<12} {max_time:<12} {obj_per_sec:<10}"
|
507
|
+
self.stdout.write(row)
|
508
|
+
|
509
|
+
if stats.get('errors', 0) > 0:
|
510
|
+
self.stdout.write(
|
511
|
+
self.style.WARNING(f" └─ {stats['errors']} errors occurred")
|
512
|
+
)
|
513
|
+
|
514
|
+
def display_stats(self, stats):
|
515
|
+
"""Display performance statistics."""
|
516
|
+
self.stdout.write(self.style.SUCCESS("Serializer Performance Statistics:"))
|
517
|
+
|
518
|
+
# Default serializer
|
519
|
+
default_serializer = stats.get('default_serializer')
|
520
|
+
if default_serializer:
|
521
|
+
self.stdout.write(f"Default Serializer: {default_serializer}")
|
522
|
+
|
523
|
+
# Registered serializers
|
524
|
+
registered = stats.get('registered_serializers', {})
|
525
|
+
if registered:
|
526
|
+
self.stdout.write(f"Registered Serializers: {len(registered)}")
|
527
|
+
for name, info in registered.items():
|
528
|
+
status = " (default)" if info['is_default'] else ""
|
529
|
+
self.stdout.write(f" • {name}{status}")
|
530
|
+
|
531
|
+
# Usage statistics
|
532
|
+
usage_stats = stats.get('usage_stats', {})
|
533
|
+
if usage_stats:
|
534
|
+
self.stdout.write("\nUsage Statistics:")
|
535
|
+
for serializer, data in usage_stats.items():
|
536
|
+
self.stdout.write(
|
537
|
+
f" {serializer}: {data['count']} uses, "
|
538
|
+
f"{data['total_objects']} objects serialized"
|
539
|
+
)
|
540
|
+
|
541
|
+
# Cache statistics
|
542
|
+
if HAS_CACHE_SYSTEM:
|
543
|
+
try:
|
544
|
+
cache_stats = get_cache_stats()
|
545
|
+
self.stdout.write(f"\nCache Statistics:")
|
546
|
+
self.stdout.write(f" Backend: {cache_stats.get('backend', 'unknown')}")
|
547
|
+
self.stdout.write(f" Hit Rate: {cache_stats.get('hit_rate', 0):.1%}")
|
548
|
+
self.stdout.write(f" Total Requests: {cache_stats.get('total_requests', 0)}")
|
549
|
+
self.stdout.write(f" Cache Size: {cache_stats.get('current_size', 0)}")
|
550
|
+
if cache_stats.get('max_size'):
|
551
|
+
utilization = cache_stats.get('current_size', 0) / cache_stats.get('max_size', 1)
|
552
|
+
self.stdout.write(f" Utilization: {utilization:.1%}")
|
553
|
+
except Exception as e:
|
554
|
+
self.stdout.write(f"\nCache Stats Error: {e}")
|
555
|
+
|
556
|
+
# Individual serializer stats
|
557
|
+
for key, value in stats.items():
|
558
|
+
if key.endswith('_stats') and isinstance(value, dict):
|
559
|
+
serializer_name = key.replace('_stats', '')
|
560
|
+
self.stdout.write(f"\n{serializer_name.title()} Stats:")
|
561
|
+
for stat_key, stat_value in value.items():
|
562
|
+
self.stdout.write(f" {stat_key}: {stat_value}")
|
563
|
+
|
564
|
+
def save_benchmark_results(self, results, filename):
|
565
|
+
"""Save benchmark results to JSON file."""
|
566
|
+
try:
|
567
|
+
with open(filename, 'w') as f:
|
568
|
+
json.dump({
|
569
|
+
'timestamp': time.time(),
|
570
|
+
'results': results
|
571
|
+
}, f, indent=2)
|
572
|
+
self.stdout.write(
|
573
|
+
self.style.SUCCESS(f"Results saved to {filename}")
|
574
|
+
)
|
575
|
+
except Exception as e:
|
576
|
+
self.stdout.write(
|
577
|
+
self.style.ERROR(f"Failed to save results: {str(e)}")
|
578
|
+
)
|
579
|
+
|
580
|
+
def run_model_health_check(self, model_class):
|
581
|
+
"""Run health check for specific model."""
|
582
|
+
self.stdout.write(f"Health check for {model_class.__name__}:")
|
583
|
+
|
584
|
+
# Check if model has RestMeta
|
585
|
+
if hasattr(model_class, 'RestMeta'):
|
586
|
+
self.stdout.write(self.style.SUCCESS(" ✓ RestMeta found"))
|
587
|
+
|
588
|
+
# Check graphs
|
589
|
+
if hasattr(model_class.RestMeta, 'GRAPHS'):
|
590
|
+
graphs = model_class.RestMeta.GRAPHS
|
591
|
+
self.stdout.write(f" ✓ {len(graphs)} graphs configured: {list(graphs.keys())}")
|
592
|
+
else:
|
593
|
+
self.stdout.write(self.style.WARNING(" ⚠ No GRAPHS configured"))
|
594
|
+
else:
|
595
|
+
self.stdout.write(self.style.WARNING(" ⚠ No RestMeta found"))
|
596
|
+
|
597
|
+
# Test serialization if instances exist
|
598
|
+
if model_class.objects.exists():
|
599
|
+
instance = model_class.objects.first()
|
600
|
+
manager = get_serializer_manager()
|
601
|
+
|
602
|
+
try:
|
603
|
+
serializer = manager.get_serializer(instance)
|
604
|
+
data = serializer.serialize()
|
605
|
+
self.stdout.write(self.style.SUCCESS(" ✓ Serialization successful"))
|
606
|
+
except Exception as e:
|
607
|
+
self.stdout.write(self.style.ERROR(f" ✗ Serialization failed: {str(e)}"))
|
608
|
+
else:
|
609
|
+
self.stdout.write(self.style.WARNING(" ⚠ No instances available for testing"))
|
610
|
+
|
611
|
+
def run_full_health_check(self):
|
612
|
+
"""Run health check for all models."""
|
613
|
+
self.stdout.write("Running full serializer health check...")
|
614
|
+
|
615
|
+
# Import MojoModel to check for subclasses
|
616
|
+
try:
|
617
|
+
from mojo.models.rest import MojoModel
|
618
|
+
|
619
|
+
# Find all MojoModel subclasses
|
620
|
+
mojo_models = []
|
621
|
+
for app_config in apps.get_app_configs():
|
622
|
+
for model in app_config.get_models():
|
623
|
+
if hasattr(model, 'RestMeta') or 'MojoModel' in [c.__name__ for c in model.__mro__]:
|
624
|
+
mojo_models.append(model)
|
625
|
+
|
626
|
+
if not mojo_models:
|
627
|
+
self.stdout.write(self.style.WARNING("No MojoModel subclasses found"))
|
628
|
+
return
|
629
|
+
|
630
|
+
self.stdout.write(f"Found {len(mojo_models)} models to check\n")
|
631
|
+
|
632
|
+
for model in mojo_models:
|
633
|
+
self.run_model_health_check(model)
|
634
|
+
self.stdout.write("") # Empty line
|
635
|
+
|
636
|
+
except ImportError:
|
637
|
+
self.stdout.write(self.style.ERROR("Could not import MojoModel"))
|
638
|
+
|
639
|
+
def print_help(self):
|
640
|
+
"""Print command help."""
|
641
|
+
self.stdout.write("MOJO Serializer Administration Command")
|
642
|
+
self.stdout.write("\nAvailable actions:")
|
643
|
+
self.stdout.write(" list - List registered serializers")
|
644
|
+
self.stdout.write(" benchmark - Benchmark serializer performance")
|
645
|
+
self.stdout.write(" set-default - Set default serializer")
|
646
|
+
self.stdout.write(" stats - Show performance statistics")
|
647
|
+
self.stdout.write(" clear-cache - Clear serializer caches")
|
648
|
+
self.stdout.write(" health - Run health checks")
|
649
|
+
self.stdout.write(" test - Test serializer functionality")
|
650
|
+
self.stdout.write(" cache-info - Show detailed cache information")
|
651
|
+
self.stdout.write("\nUse 'python manage.py serializer_admin <action> --help' for more details")
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-09 05:37
|
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
|
+
('fileman', '0011_alter_filerendition_original_file'),
|
11
|
+
('account', '0003_group_mojo_secrets_user_mojo_secrets'),
|
12
|
+
]
|
13
|
+
|
14
|
+
operations = [
|
15
|
+
migrations.AddField(
|
16
|
+
model_name='user',
|
17
|
+
name='avatar',
|
18
|
+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='fileman.file'),
|
19
|
+
),
|
20
|
+
]
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-08-26 16:05
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('account', '0004_user_avatar'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name='group',
|
15
|
+
name='last_activity',
|
16
|
+
field=models.DateTimeField(db_index=True, default=None, null=True),
|
17
|
+
),
|
18
|
+
]
|