django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. django_nativemojo-0.1.15.dist-info/METADATA +136 -0
  2. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +531 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/models/group.py +25 -7
  10. mojo/apps/account/models/member.py +15 -4
  11. mojo/apps/account/models/user.py +197 -20
  12. mojo/apps/account/rest/group.py +1 -0
  13. mojo/apps/account/rest/user.py +6 -2
  14. mojo/apps/aws/rest/__init__.py +1 -0
  15. mojo/apps/aws/rest/s3.py +64 -0
  16. mojo/apps/fileman/README.md +8 -8
  17. mojo/apps/fileman/backends/base.py +76 -70
  18. mojo/apps/fileman/backends/filesystem.py +86 -86
  19. mojo/apps/fileman/backends/s3.py +200 -108
  20. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  21. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  22. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  23. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  24. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  25. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  26. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  27. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  28. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  29. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  30. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  31. mojo/apps/fileman/models/__init__.py +1 -5
  32. mojo/apps/fileman/models/file.py +204 -58
  33. mojo/apps/fileman/models/manager.py +161 -31
  34. mojo/apps/fileman/models/rendition.py +118 -0
  35. mojo/apps/fileman/renderer/__init__.py +111 -0
  36. mojo/apps/fileman/renderer/audio.py +403 -0
  37. mojo/apps/fileman/renderer/base.py +205 -0
  38. mojo/apps/fileman/renderer/document.py +404 -0
  39. mojo/apps/fileman/renderer/image.py +222 -0
  40. mojo/apps/fileman/renderer/utils.py +297 -0
  41. mojo/apps/fileman/renderer/video.py +304 -0
  42. mojo/apps/fileman/rest/__init__.py +1 -18
  43. mojo/apps/fileman/rest/upload.py +22 -32
  44. mojo/apps/fileman/signals.py +58 -0
  45. mojo/apps/fileman/tasks.py +254 -0
  46. mojo/apps/fileman/utils/__init__.py +40 -16
  47. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  48. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  49. mojo/apps/incident/models/__init__.py +1 -0
  50. mojo/apps/incident/models/history.py +36 -0
  51. mojo/apps/incident/models/incident.py +1 -1
  52. mojo/apps/incident/reporter.py +3 -1
  53. mojo/apps/incident/rest/event.py +7 -1
  54. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  55. mojo/apps/logit/models/log.py +4 -1
  56. mojo/apps/metrics/utils.py +2 -2
  57. mojo/apps/notify/handlers/ses/message.py +1 -1
  58. mojo/apps/notify/providers/aws.py +2 -2
  59. mojo/apps/tasks/__init__.py +34 -1
  60. mojo/apps/tasks/manager.py +200 -45
  61. mojo/apps/tasks/rest/tasks.py +24 -10
  62. mojo/apps/tasks/runner.py +283 -18
  63. mojo/apps/tasks/task.py +99 -0
  64. mojo/apps/tasks/tq_handlers.py +118 -0
  65. mojo/decorators/auth.py +6 -1
  66. mojo/decorators/http.py +7 -2
  67. mojo/helpers/aws/__init__.py +41 -0
  68. mojo/helpers/aws/ec2.py +804 -0
  69. mojo/helpers/aws/iam.py +748 -0
  70. mojo/helpers/aws/s3.py +451 -11
  71. mojo/helpers/aws/ses.py +483 -0
  72. mojo/helpers/aws/sns.py +461 -0
  73. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  74. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  75. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  76. mojo/helpers/dates.py +18 -0
  77. mojo/helpers/response.py +6 -2
  78. mojo/helpers/settings/__init__.py +2 -0
  79. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  80. mojo/helpers/settings/parser.py +132 -0
  81. mojo/middleware/logging.py +1 -1
  82. mojo/middleware/mojo.py +5 -0
  83. mojo/models/rest.py +261 -46
  84. mojo/models/secrets.py +13 -4
  85. mojo/serializers/__init__.py +100 -0
  86. mojo/serializers/advanced/README.md +363 -0
  87. mojo/serializers/advanced/__init__.py +247 -0
  88. mojo/serializers/advanced/formats/__init__.py +28 -0
  89. mojo/serializers/advanced/formats/csv.py +416 -0
  90. mojo/serializers/advanced/formats/excel.py +516 -0
  91. mojo/serializers/advanced/formats/json.py +239 -0
  92. mojo/serializers/advanced/formats/localizers.py +509 -0
  93. mojo/serializers/advanced/formats/response.py +485 -0
  94. mojo/serializers/advanced/serializer.py +568 -0
  95. mojo/serializers/manager.py +501 -0
  96. mojo/serializers/optimized.py +618 -0
  97. mojo/serializers/settings_example.py +322 -0
  98. mojo/serializers/{models.py → simple.py} +38 -15
  99. testit/helpers.py +21 -4
  100. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  101. mojo/apps/metrics/rest/db.py +0 -0
  102. mojo/helpers/aws/setup_email.py +0 -0
  103. mojo/ws4redis/README.md +0 -174
  104. mojo/ws4redis/__init__.py +0 -2
  105. mojo/ws4redis/client.py +0 -283
  106. mojo/ws4redis/connection.py +0 -327
  107. mojo/ws4redis/exceptions.py +0 -32
  108. mojo/ws4redis/redis.py +0 -183
  109. mojo/ws4redis/servers/base.py +0 -86
  110. mojo/ws4redis/servers/django.py +0 -171
  111. mojo/ws4redis/servers/uwsgi.py +0 -63
  112. mojo/ws4redis/settings.py +0 -45
  113. mojo/ws4redis/utf8validator.py +0 -128
  114. mojo/ws4redis/websocket.py +0 -403
  115. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
  116. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
  117. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
  118. /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
  119. /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
  120. /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -0,0 +1,531 @@
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
+ )
42
+
43
+
44
+ class Command(BaseCommand):
45
+ help = 'Manage MOJO serializers: benchmark, configure, and monitor performance'
46
+
47
+ def add_arguments(self, parser):
48
+ """Add command line arguments."""
49
+
50
+ subparsers = parser.add_subparsers(dest='action', help='Available actions')
51
+
52
+ # List serializers
53
+ list_parser = subparsers.add_parser('list', help='List all registered serializers')
54
+ list_parser.add_argument(
55
+ '--detail',
56
+ action='store_true',
57
+ help='Show detailed information about each serializer'
58
+ )
59
+
60
+ # Benchmark serializers
61
+ bench_parser = subparsers.add_parser('benchmark', help='Benchmark serializer performance')
62
+ bench_parser.add_argument(
63
+ '--model',
64
+ type=str,
65
+ required=True,
66
+ help='Model name to benchmark (format: app.ModelName or ModelName)'
67
+ )
68
+ bench_parser.add_argument(
69
+ '--count',
70
+ type=int,
71
+ default=100,
72
+ help='Number of objects to serialize (default: 100)'
73
+ )
74
+ bench_parser.add_argument(
75
+ '--graph',
76
+ type=str,
77
+ default='default',
78
+ help='Graph configuration to use (default: "default")'
79
+ )
80
+ bench_parser.add_argument(
81
+ '--iterations',
82
+ type=int,
83
+ default=5,
84
+ help='Number of benchmark iterations (default: 5)'
85
+ )
86
+ bench_parser.add_argument(
87
+ '--serializers',
88
+ nargs='+',
89
+ help='Specific serializers to benchmark (default: all)'
90
+ )
91
+ bench_parser.add_argument(
92
+ '--output-json',
93
+ type=str,
94
+ help='Save results to JSON file'
95
+ )
96
+
97
+ # Set default serializer
98
+ default_parser = subparsers.add_parser('set-default', help='Set default serializer')
99
+ default_parser.add_argument(
100
+ 'serializer_name',
101
+ type=str,
102
+ help='Name of serializer to set as default'
103
+ )
104
+
105
+ # Performance statistics
106
+ stats_parser = subparsers.add_parser('stats', help='Show performance statistics')
107
+ stats_parser.add_argument(
108
+ '--clear',
109
+ action='store_true',
110
+ help='Clear statistics after showing them'
111
+ )
112
+ stats_parser.add_argument(
113
+ '--json',
114
+ action='store_true',
115
+ help='Output statistics as JSON'
116
+ )
117
+
118
+ # Cache management
119
+ cache_parser = subparsers.add_parser('clear-cache', help='Clear serializer caches')
120
+ cache_parser.add_argument(
121
+ '--serializer',
122
+ type=str,
123
+ help='Clear cache for specific serializer (default: all)'
124
+ )
125
+
126
+ # Health check
127
+ health_parser = subparsers.add_parser('health', help='Run serializer health checks')
128
+ health_parser.add_argument(
129
+ '--model',
130
+ type=str,
131
+ help='Test specific model (format: app.ModelName or ModelName)'
132
+ )
133
+
134
+ # Test serializers
135
+ test_parser = subparsers.add_parser('test', help='Test serializer functionality')
136
+ test_parser.add_argument(
137
+ '--model',
138
+ type=str,
139
+ required=True,
140
+ help='Model to test (format: app.ModelName or ModelName)'
141
+ )
142
+ test_parser.add_argument(
143
+ '--graph',
144
+ type=str,
145
+ default='default',
146
+ help='Graph configuration to test'
147
+ )
148
+
149
+ def handle(self, *args, **options):
150
+ """Handle command execution."""
151
+ action = options.get('action')
152
+
153
+ if not action:
154
+ self.print_help()
155
+ return
156
+
157
+ # Route to appropriate handler
158
+ handler_map = {
159
+ 'list': self.handle_list,
160
+ 'benchmark': self.handle_benchmark,
161
+ 'set-default': self.handle_set_default,
162
+ 'stats': self.handle_stats,
163
+ 'clear-cache': self.handle_clear_cache,
164
+ 'health': self.handle_health,
165
+ 'test': self.handle_test,
166
+ }
167
+
168
+ handler = handler_map.get(action)
169
+ if handler:
170
+ try:
171
+ handler(options)
172
+ except Exception as e:
173
+ raise CommandError(f"Error executing {action}: {str(e)}")
174
+ else:
175
+ raise CommandError(f"Unknown action: {action}")
176
+
177
+ def handle_list(self, options):
178
+ """List all registered serializers."""
179
+ serializers = list_serializers()
180
+
181
+ if not serializers:
182
+ self.stdout.write(
183
+ self.style.WARNING('No serializers registered')
184
+ )
185
+ return
186
+
187
+ self.stdout.write(
188
+ self.style.SUCCESS('Registered Serializers:')
189
+ )
190
+
191
+ for name, info in serializers.items():
192
+ status = " (DEFAULT)" if info['is_default'] else ""
193
+ self.stdout.write(f" • {name}{status}")
194
+
195
+ if options.get('detail'):
196
+ self.stdout.write(f" Class: {info['class_name']}")
197
+ self.stdout.write(f" Description: {info['description']}")
198
+
199
+ def handle_benchmark(self, options):
200
+ """Benchmark serializer performance."""
201
+ model_class = self.get_model_class(options['model'])
202
+ count = options['count']
203
+ graph = options['graph']
204
+ iterations = options['iterations']
205
+ serializer_types = options.get('serializers')
206
+
207
+ # Check if model has enough instances
208
+ total_instances = model_class.objects.count()
209
+ if total_instances < count:
210
+ self.stdout.write(
211
+ self.style.WARNING(
212
+ f"Model {model_class.__name__} only has {total_instances} instances, "
213
+ f"but {count} requested. Using available instances."
214
+ )
215
+ )
216
+ count = min(count, total_instances)
217
+
218
+ if count == 0:
219
+ raise CommandError(f"No instances found for model {model_class.__name__}")
220
+
221
+ # Get test queryset
222
+ test_queryset = model_class.objects.all()[:count]
223
+
224
+ self.stdout.write(
225
+ self.style.SUCCESS(
226
+ f"Benchmarking serializers with {count} {model_class.__name__} instances"
227
+ )
228
+ )
229
+ self.stdout.write(f"Graph: {graph}")
230
+ self.stdout.write(f"Iterations: {iterations}")
231
+
232
+ # Run benchmark
233
+ try:
234
+ results = benchmark_serializers(
235
+ instance=test_queryset,
236
+ graph=graph,
237
+ serializer_types=serializer_types,
238
+ iterations=iterations
239
+ )
240
+
241
+ # Display results
242
+ self.display_benchmark_results(results)
243
+
244
+ # Save to JSON if requested
245
+ if options.get('output_json'):
246
+ self.save_benchmark_results(results, options['output_json'])
247
+
248
+ except Exception as e:
249
+ raise CommandError(f"Benchmark failed: {str(e)}")
250
+
251
+ def handle_set_default(self, options):
252
+ """Set default serializer."""
253
+ serializer_name = options['serializer_name']
254
+
255
+ if set_default_serializer(serializer_name):
256
+ self.stdout.write(
257
+ self.style.SUCCESS(
258
+ f"Default serializer set to: {serializer_name}"
259
+ )
260
+ )
261
+ else:
262
+ raise CommandError(f"Failed to set default serializer to {serializer_name}")
263
+
264
+ def handle_stats(self, options):
265
+ """Show performance statistics."""
266
+ stats = get_performance_stats()
267
+
268
+ if options.get('json'):
269
+ self.stdout.write(json.dumps(stats, indent=2))
270
+ else:
271
+ self.display_stats(stats)
272
+
273
+ if options.get('clear'):
274
+ # Clear statistics if requested
275
+ manager = get_serializer_manager()
276
+ if hasattr(manager, 'reset_performance_stats'):
277
+ manager.reset_performance_stats()
278
+ self.stdout.write(
279
+ self.style.SUCCESS("Performance statistics cleared")
280
+ )
281
+
282
+ def handle_clear_cache(self, options):
283
+ """Clear serializer caches."""
284
+ serializer_name = options.get('serializer')
285
+
286
+ if serializer_name:
287
+ clear_serializer_caches(serializer_name)
288
+ self.stdout.write(
289
+ self.style.SUCCESS(f"Cache cleared for {serializer_name}")
290
+ )
291
+ else:
292
+ clear_serializer_caches()
293
+ self.stdout.write(
294
+ self.style.SUCCESS("All serializer caches cleared")
295
+ )
296
+
297
+ def handle_health(self, options):
298
+ """Run serializer health checks."""
299
+ model_name = options.get('model')
300
+
301
+ if model_name:
302
+ # Test specific model
303
+ model_class = self.get_model_class(model_name)
304
+ self.run_model_health_check(model_class)
305
+ else:
306
+ # Test all MojoModel subclasses
307
+ self.run_full_health_check()
308
+
309
+ def handle_test(self, options):
310
+ """Test serializer functionality."""
311
+ model_class = self.get_model_class(options['model'])
312
+ graph = options['graph']
313
+
314
+ # Get a test instance
315
+ instance = model_class.objects.first()
316
+ if not instance:
317
+ raise CommandError(f"No instances found for model {model_class.__name__}")
318
+
319
+ self.stdout.write(
320
+ self.style.SUCCESS(f"Testing {model_class.__name__} serialization")
321
+ )
322
+
323
+ # Test each registered serializer
324
+ serializers = list_serializers()
325
+ manager = get_serializer_manager()
326
+
327
+ for serializer_name in serializers.keys():
328
+ self.stdout.write(f"\nTesting {serializer_name}:")
329
+
330
+ try:
331
+ # Test single instance
332
+ serializer = manager.get_serializer(instance, graph=graph, serializer_type=serializer_name)
333
+ data = serializer.serialize()
334
+ json_output = serializer.to_json()
335
+
336
+ self.stdout.write(
337
+ self.style.SUCCESS(f" ✓ Single instance: {len(str(data))} chars")
338
+ )
339
+
340
+ # Test queryset
341
+ queryset = model_class.objects.all()[:5] # Test with 5 instances
342
+ serializer = manager.get_serializer(queryset, graph=graph, serializer_type=serializer_name, many=True)
343
+ list_data = serializer.serialize()
344
+ list_json = serializer.to_json()
345
+
346
+ self.stdout.write(
347
+ self.style.SUCCESS(f" ✓ QuerySet ({len(list_data)} items): {len(str(list_json))} chars")
348
+ )
349
+
350
+ except Exception as e:
351
+ self.stdout.write(
352
+ self.style.ERROR(f" ✗ Failed: {str(e)}")
353
+ )
354
+
355
+ def get_model_class(self, model_string):
356
+ """Get model class from string."""
357
+ try:
358
+ if '.' in model_string:
359
+ app_label, model_name = model_string.split('.', 1)
360
+ return apps.get_model(app_label, model_name)
361
+ else:
362
+ # Try to find model in any app
363
+ for app_config in apps.get_app_configs():
364
+ try:
365
+ return app_config.get_model(model_string)
366
+ except LookupError:
367
+ continue
368
+ raise CommandError(f"Model '{model_string}' not found")
369
+ except Exception as e:
370
+ raise CommandError(f"Invalid model specification '{model_string}': {str(e)}")
371
+
372
+ def display_benchmark_results(self, results):
373
+ """Display benchmark results in a formatted table."""
374
+ if not results:
375
+ self.stdout.write(self.style.WARNING("No benchmark results"))
376
+ return
377
+
378
+ self.stdout.write("\nBenchmark Results:")
379
+ self.stdout.write("=" * 80)
380
+
381
+ # Table header
382
+ header = f"{'Serializer':<15} {'Avg Time':<12} {'Min Time':<12} {'Max Time':<12} {'Obj/Sec':<10}"
383
+ self.stdout.write(header)
384
+ self.stdout.write("-" * 80)
385
+
386
+ # Sort by average time
387
+ sorted_results = sorted(
388
+ results.items(),
389
+ key=lambda x: x[1].get('avg_time', float('inf'))
390
+ )
391
+
392
+ for name, stats in sorted_results:
393
+ if 'error' in stats:
394
+ row = f"{name:<15} {stats['error']:<50}"
395
+ self.stdout.write(self.style.ERROR(row))
396
+ else:
397
+ avg_time = f"{stats['avg_time']:.4f}s"
398
+ min_time = f"{stats['min_time']:.4f}s"
399
+ max_time = f"{stats['max_time']:.4f}s"
400
+ obj_per_sec = f"{stats.get('objects_per_second', 0):.1f}"
401
+
402
+ row = f"{name:<15} {avg_time:<12} {min_time:<12} {max_time:<12} {obj_per_sec:<10}"
403
+ self.stdout.write(row)
404
+
405
+ if stats.get('errors', 0) > 0:
406
+ self.stdout.write(
407
+ self.style.WARNING(f" └─ {stats['errors']} errors occurred")
408
+ )
409
+
410
+ def display_stats(self, stats):
411
+ """Display performance statistics."""
412
+ self.stdout.write(self.style.SUCCESS("Serializer Performance Statistics:"))
413
+
414
+ # Default serializer
415
+ default_serializer = stats.get('default_serializer')
416
+ if default_serializer:
417
+ self.stdout.write(f"Default Serializer: {default_serializer}")
418
+
419
+ # Registered serializers
420
+ registered = stats.get('registered_serializers', {})
421
+ if registered:
422
+ self.stdout.write(f"Registered Serializers: {len(registered)}")
423
+ for name, info in registered.items():
424
+ status = " (default)" if info['is_default'] else ""
425
+ self.stdout.write(f" • {name}{status}")
426
+
427
+ # Usage statistics
428
+ usage_stats = stats.get('usage_stats', {})
429
+ if usage_stats:
430
+ self.stdout.write("\nUsage Statistics:")
431
+ for serializer, data in usage_stats.items():
432
+ self.stdout.write(
433
+ f" {serializer}: {data['count']} uses, "
434
+ f"{data['total_objects']} objects serialized"
435
+ )
436
+
437
+ # Individual serializer stats
438
+ for key, value in stats.items():
439
+ if key.endswith('_stats') and isinstance(value, dict):
440
+ serializer_name = key.replace('_stats', '')
441
+ self.stdout.write(f"\n{serializer_name.title()} Stats:")
442
+ for stat_key, stat_value in value.items():
443
+ self.stdout.write(f" {stat_key}: {stat_value}")
444
+
445
+ def save_benchmark_results(self, results, filename):
446
+ """Save benchmark results to JSON file."""
447
+ try:
448
+ with open(filename, 'w') as f:
449
+ json.dump({
450
+ 'timestamp': time.time(),
451
+ 'results': results
452
+ }, f, indent=2)
453
+ self.stdout.write(
454
+ self.style.SUCCESS(f"Results saved to {filename}")
455
+ )
456
+ except Exception as e:
457
+ self.stdout.write(
458
+ self.style.ERROR(f"Failed to save results: {str(e)}")
459
+ )
460
+
461
+ def run_model_health_check(self, model_class):
462
+ """Run health check for specific model."""
463
+ self.stdout.write(f"Health check for {model_class.__name__}:")
464
+
465
+ # Check if model has RestMeta
466
+ if hasattr(model_class, 'RestMeta'):
467
+ self.stdout.write(self.style.SUCCESS(" ✓ RestMeta found"))
468
+
469
+ # Check graphs
470
+ if hasattr(model_class.RestMeta, 'GRAPHS'):
471
+ graphs = model_class.RestMeta.GRAPHS
472
+ self.stdout.write(f" ✓ {len(graphs)} graphs configured: {list(graphs.keys())}")
473
+ else:
474
+ self.stdout.write(self.style.WARNING(" ⚠ No GRAPHS configured"))
475
+ else:
476
+ self.stdout.write(self.style.WARNING(" ⚠ No RestMeta found"))
477
+
478
+ # Test serialization if instances exist
479
+ if model_class.objects.exists():
480
+ instance = model_class.objects.first()
481
+ manager = get_serializer_manager()
482
+
483
+ try:
484
+ serializer = manager.get_serializer(instance)
485
+ data = serializer.serialize()
486
+ self.stdout.write(self.style.SUCCESS(" ✓ Serialization successful"))
487
+ except Exception as e:
488
+ self.stdout.write(self.style.ERROR(f" ✗ Serialization failed: {str(e)}"))
489
+ else:
490
+ self.stdout.write(self.style.WARNING(" ⚠ No instances available for testing"))
491
+
492
+ def run_full_health_check(self):
493
+ """Run health check for all models."""
494
+ self.stdout.write("Running full serializer health check...")
495
+
496
+ # Import MojoModel to check for subclasses
497
+ try:
498
+ from mojo.models.rest import MojoModel
499
+
500
+ # Find all MojoModel subclasses
501
+ mojo_models = []
502
+ for app_config in apps.get_app_configs():
503
+ for model in app_config.get_models():
504
+ if hasattr(model, 'RestMeta') or 'MojoModel' in [c.__name__ for c in model.__mro__]:
505
+ mojo_models.append(model)
506
+
507
+ if not mojo_models:
508
+ self.stdout.write(self.style.WARNING("No MojoModel subclasses found"))
509
+ return
510
+
511
+ self.stdout.write(f"Found {len(mojo_models)} models to check\n")
512
+
513
+ for model in mojo_models:
514
+ self.run_model_health_check(model)
515
+ self.stdout.write("") # Empty line
516
+
517
+ except ImportError:
518
+ self.stdout.write(self.style.ERROR("Could not import MojoModel"))
519
+
520
+ def print_help(self):
521
+ """Print command help."""
522
+ self.stdout.write("MOJO Serializer Administration Command")
523
+ self.stdout.write("\nAvailable actions:")
524
+ self.stdout.write(" list - List registered serializers")
525
+ self.stdout.write(" benchmark - Benchmark serializer performance")
526
+ self.stdout.write(" set-default - Set default serializer")
527
+ self.stdout.write(" stats - Show performance statistics")
528
+ self.stdout.write(" clear-cache - Clear serializer caches")
529
+ self.stdout.write(" health - Run health checks")
530
+ self.stdout.write(" test - Test serializer functionality")
531
+ 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
+ ]
@@ -1,7 +1,11 @@
1
1
  from django.db import models
2
2
  from mojo.models import MojoModel, MojoSecrets
3
3
  from mojo.helpers import dates
4
+ from mojo.apps import metrics
5
+ from mojo.helpers.settings import settings
4
6
 
7
+ GROUP_LAST_ACTIVITY_FREQ = settings.get("GROUP_LAST_ACTIVITY_FREQ", 300)
8
+ METRICS_TIMEZONE = settings.get("METRICS_TIMEZONE", "America/Los_Angeles")
5
9
 
6
10
 
7
11
  class Group(MojoSecrets, MojoModel):
@@ -10,6 +14,7 @@ class Group(MojoSecrets, MojoModel):
10
14
  """
11
15
  created = models.DateTimeField(auto_now_add=True, editable=False)
12
16
  modified = models.DateTimeField(auto_now=True, db_index=True)
17
+ last_activity = models.DateTimeField(default=None, null=True, db_index=True)
13
18
 
14
19
  name = models.CharField(max_length=200)
15
20
  uuid = models.CharField(max_length=200, null=True, default=None, db_index=True)
@@ -36,6 +41,7 @@ class Group(MojoSecrets, MojoModel):
36
41
  'name',
37
42
  'created',
38
43
  'modified',
44
+ 'last_activity',
39
45
  'is_active',
40
46
  'kind',
41
47
  ]
@@ -46,15 +52,17 @@ class Group(MojoSecrets, MojoModel):
46
52
  'name',
47
53
  'created',
48
54
  'modified',
55
+ 'last_activity',
49
56
  'is_active',
50
57
  'kind',
51
58
  'parent',
52
59
  'metadata'
53
- ]
60
+ ],
61
+ "graphs": {
62
+ "parent": "basic"
63
+ }
54
64
  },
55
- "graphs": {
56
- "parent": "basic"
57
- }
65
+
58
66
  }
59
67
 
60
68
  @property
@@ -68,7 +76,7 @@ class Group(MojoSecrets, MojoModel):
68
76
  return dates.get_local_time(self.timezone, dt_utc)
69
77
 
70
78
  def __str__(self):
71
- return self.name
79
+ return str(self.name)
72
80
 
73
81
  def has_permission(self, user):
74
82
  from mojo.account.models.member import GroupMember
@@ -82,6 +90,14 @@ class Group(MojoSecrets, MojoModel):
82
90
  return ms.has_permission(perms)
83
91
  return False
84
92
 
93
+ def touch(self):
94
+ # can't subtract offset-naive and offset-aware datetimes
95
+ if self.last_activity and not dates.is_today(self.last_activity, METRICS_TIMEZONE):
96
+ metrics.record("group_activity_day", category="group", min_granularity="days")
97
+ if self.last_activity is None or dates.has_time_elsapsed(self.last_activity, seconds=GROUP_LAST_ACTIVITY_FREQ):
98
+ self.last_activity = dates.utcnow()
99
+ self.atomic_save()
100
+
85
101
  def get_metadata(self):
86
102
  # converts our local metadata into an objict
87
103
  self.metadata = self.jsonfield_as_objict("metadata")
@@ -94,5 +110,7 @@ class Group(MojoSecrets, MojoModel):
94
110
  def on_rest_handle_list(cls, request):
95
111
  if cls.rest_check_permission(request, "VIEW_PERMS"):
96
112
  return cls.on_rest_list(request)
97
- group_ids = request.user.members.values_list('group__id', flat=True)
98
- return cls.on_rest_list(request, cls.objects.filter(id__in=group_ids))
113
+ if getattr(request.user, 'members') is not None:
114
+ group_ids = request.user.members.values_list('group__id', flat=True)
115
+ return cls.on_rest_list(request, cls.objects.filter(id__in=group_ids))
116
+ return cls.on_rest_list(request, cls.objects.none())