django-cfg 1.2.6__py3-none-any.whl → 1.2.8__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 (53) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/agents/admin/execution_admin.py +9 -2
  3. django_cfg/apps/agents/admin/registry_admin.py +10 -2
  4. django_cfg/apps/agents/admin/toolsets_admin.py +14 -3
  5. django_cfg/apps/knowbase/admin/archive_admin.py +3 -2
  6. django_cfg/apps/knowbase/admin/chat_admin.py +3 -2
  7. django_cfg/apps/knowbase/admin/document_admin.py +11 -2
  8. django_cfg/apps/knowbase/admin/external_data_admin.py +2 -1
  9. django_cfg/apps/urls.py +2 -2
  10. django_cfg/modules/django_import_export/__init__.py +12 -5
  11. django_cfg/modules/django_unfold/callbacks/__init__.py +9 -0
  12. django_cfg/modules/django_unfold/callbacks/actions.py +50 -0
  13. django_cfg/modules/django_unfold/callbacks/base.py +98 -0
  14. django_cfg/modules/django_unfold/callbacks/charts.py +224 -0
  15. django_cfg/modules/django_unfold/callbacks/commands.py +40 -0
  16. django_cfg/modules/django_unfold/callbacks/main.py +191 -0
  17. django_cfg/modules/django_unfold/callbacks/revolution.py +76 -0
  18. django_cfg/modules/django_unfold/callbacks/statistics.py +240 -0
  19. django_cfg/modules/django_unfold/callbacks/system.py +180 -0
  20. django_cfg/modules/django_unfold/callbacks/users.py +65 -0
  21. django_cfg/modules/django_unfold/models/config.py +10 -3
  22. django_cfg/modules/django_unfold/tailwind.py +68 -0
  23. django_cfg/templates/admin/components/action_grid.html +49 -0
  24. django_cfg/templates/admin/components/card.html +50 -0
  25. django_cfg/templates/admin/components/data_table.html +67 -0
  26. django_cfg/templates/admin/components/metric_card.html +39 -0
  27. django_cfg/templates/admin/components/modal.html +58 -0
  28. django_cfg/templates/admin/components/progress_bar.html +25 -0
  29. django_cfg/templates/admin/components/section_header.html +26 -0
  30. django_cfg/templates/admin/components/stat_item.html +32 -0
  31. django_cfg/templates/admin/components/stats_grid.html +72 -0
  32. django_cfg/templates/admin/components/status_badge.html +28 -0
  33. django_cfg/templates/admin/components/user_avatar.html +27 -0
  34. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +7 -7
  35. django_cfg/templates/admin/snippets/components/activity_tracker.html +48 -11
  36. django_cfg/templates/admin/snippets/components/charts_section.html +63 -13
  37. django_cfg/templates/admin/snippets/components/django_commands.html +18 -18
  38. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -47
  39. django_cfg/templates/admin/snippets/components/recent_activity.html +28 -38
  40. django_cfg/templates/admin/snippets/components/recent_users_table.html +22 -53
  41. django_cfg/templates/admin/snippets/components/stats_cards.html +2 -66
  42. django_cfg/templates/admin/snippets/components/system_health.html +13 -63
  43. django_cfg/templates/admin/snippets/components/system_metrics.html +8 -25
  44. django_cfg/templates/admin/snippets/tabs/commands_tab.html +1 -1
  45. django_cfg/templates/admin/snippets/tabs/overview_tab.html +4 -4
  46. django_cfg/templates/admin/snippets/zones/zones_table.html +12 -33
  47. django_cfg/templatetags/django_cfg.py +2 -1
  48. {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/METADATA +2 -1
  49. {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/RECORD +52 -32
  50. django_cfg/modules/django_unfold/callbacks.py +0 -795
  51. {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/WHEEL +0 -0
  52. {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/entry_points.txt +0 -0
  53. {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.2.6"
35
+ __version__ = "1.2.8"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -17,6 +17,7 @@ from unfold.decorators import display, action
17
17
  from unfold.enums import ActionVariant
18
18
  from unfold.contrib.filters.admin import AutocompleteSelectFilter, AutocompleteSelectMultipleFilter
19
19
  from unfold.contrib.forms.widgets import WysiwygWidget
20
+ from django_cfg import ExportMixin, ExportForm
20
21
 
21
22
  from ..models.execution import AgentExecution, WorkflowExecution
22
23
 
@@ -90,9 +91,12 @@ class AgentExecutionInlineForWorkflow(TabularInline):
90
91
 
91
92
 
92
93
  @admin.register(AgentExecution)
93
- class AgentExecutionAdmin(ModelAdmin):
94
+ class AgentExecutionAdmin(ModelAdmin, ExportMixin):
94
95
  """Admin interface for AgentExecution with Unfold styling."""
95
96
 
97
+ # Export-only configuration
98
+ export_form_class = ExportForm
99
+
96
100
  list_display = [
97
101
  'id_display', 'agent_name_display', 'status_badge', 'user',
98
102
  'execution_metrics', 'cost_display', 'cached_badge', 'created_at'
@@ -261,9 +265,12 @@ class AgentExecutionAdmin(ModelAdmin):
261
265
 
262
266
 
263
267
  @admin.register(WorkflowExecution)
264
- class WorkflowExecutionAdmin(ModelAdmin):
268
+ class WorkflowExecutionAdmin(ModelAdmin, ExportMixin):
265
269
  """Admin interface for WorkflowExecution with Unfold styling."""
266
270
 
271
+ # Export-only configuration
272
+ export_form_class = ExportForm
273
+
267
274
  list_display = [
268
275
  'id_display', 'name_display', 'pattern_badge', 'status_badge', 'user',
269
276
  'progress_display', 'metrics_display', 'cost_display', 'created_at'
@@ -17,6 +17,7 @@ from unfold.decorators import display, action
17
17
  from unfold.enums import ActionVariant
18
18
  from unfold.contrib.filters.admin import AutocompleteSelectFilter, AutocompleteSelectMultipleFilter
19
19
  from unfold.contrib.forms.widgets import WysiwygWidget
20
+ from django_cfg import ImportExportModelAdmin, ExportMixin, ImportForm, ExportForm
20
21
 
21
22
  from ..models.registry import AgentDefinition, AgentTemplate
22
23
  from ..models.execution import AgentExecution
@@ -91,9 +92,13 @@ class AgentExecutionInline(TabularInline):
91
92
 
92
93
 
93
94
  @admin.register(AgentDefinition)
94
- class AgentDefinitionAdmin(ModelAdmin):
95
+ class AgentDefinitionAdmin(ModelAdmin, ImportExportModelAdmin):
95
96
  """Admin interface for AgentDefinition with Unfold styling."""
96
97
 
98
+ # Import/Export configuration
99
+ import_form_class = ImportForm
100
+ export_form_class = ExportForm
101
+
97
102
  list_display = [
98
103
  'name_display', 'display_name', 'category_badge', 'status_badges',
99
104
  'usage_stats', 'performance_indicator', 'created_by', 'created_at'
@@ -271,9 +276,12 @@ class AgentDefinitionAdmin(ModelAdmin):
271
276
 
272
277
 
273
278
  @admin.register(AgentTemplate)
274
- class AgentTemplateAdmin(ModelAdmin):
279
+ class AgentTemplateAdmin(ModelAdmin, ExportMixin):
275
280
  """Admin interface for AgentTemplate with Unfold styling."""
276
281
 
282
+ # Export-only configuration
283
+ export_form_class = ExportForm
284
+
277
285
  list_display = ['name_display', 'category_badge', 'status_badge', 'use_cases_preview', 'created_by', 'created_at']
278
286
  ordering = ['-created_at']
279
287
  list_filter = [
@@ -17,14 +17,18 @@ from unfold.decorators import display, action
17
17
  from unfold.enums import ActionVariant
18
18
  from unfold.contrib.filters.admin import AutocompleteSelectFilter, AutocompleteSelectMultipleFilter
19
19
  from unfold.contrib.forms.widgets import WysiwygWidget
20
+ from django_cfg import ExportMixin, ImportExportModelAdmin, ImportForm, ExportForm
20
21
 
21
22
  from ..models.toolsets import ToolExecution, ApprovalLog, ToolsetConfiguration
22
23
 
23
24
 
24
25
  @admin.register(ToolExecution)
25
- class ToolExecutionAdmin(ModelAdmin):
26
+ class ToolExecutionAdmin(ModelAdmin, ExportMixin):
26
27
  """Admin interface for ToolExecution with Unfold styling."""
27
28
 
29
+ # Export-only configuration
30
+ export_form_class = ExportForm
31
+
28
32
  list_display = [
29
33
  'id_display', 'tool_name_display', 'toolset_badge', 'status_badge', 'user',
30
34
  'execution_metrics', 'retry_badge', 'created_at'
@@ -197,9 +201,12 @@ class ToolExecutionAdmin(ModelAdmin):
197
201
 
198
202
 
199
203
  @admin.register(ApprovalLog)
200
- class ApprovalLogAdmin(ModelAdmin):
204
+ class ApprovalLogAdmin(ModelAdmin, ExportMixin):
201
205
  """Admin interface for ApprovalLog with Unfold styling."""
202
206
 
207
+ # Export-only configuration
208
+ export_form_class = ExportForm
209
+
203
210
  list_display = [
204
211
  'approval_id_display', 'tool_name_display', 'status_badge', 'user',
205
212
  'decision_info', 'time_metrics', 'expiry_status', 'requested_at'
@@ -371,9 +378,13 @@ class ApprovalLogAdmin(ModelAdmin):
371
378
 
372
379
 
373
380
  @admin.register(ToolsetConfiguration)
374
- class ToolsetConfigurationAdmin(ModelAdmin):
381
+ class ToolsetConfigurationAdmin(ModelAdmin, ImportExportModelAdmin):
375
382
  """Admin interface for ToolsetConfiguration with Unfold styling."""
376
383
 
384
+ # Import/Export configuration
385
+ import_form_class = ImportForm
386
+ export_form_class = ExportForm
387
+
377
388
  list_display = [
378
389
  'name_display', 'toolset_class_badge', 'status_badge', 'usage_info', 'created_by', 'created_at'
379
390
  ]
@@ -18,6 +18,7 @@ from unfold.decorators import display, action
18
18
  from unfold.enums import ActionVariant
19
19
  from unfold.contrib.filters.admin import AutocompleteSelectFilter
20
20
  from unfold.contrib.forms.widgets import WysiwygWidget
21
+ from django_cfg import ExportMixin
21
22
 
22
23
  from ..models.archive import DocumentArchive, ArchiveItem, ArchiveItemChunk
23
24
 
@@ -76,7 +77,7 @@ class ArchiveItemInline(TabularInline):
76
77
 
77
78
 
78
79
  @admin.register(DocumentArchive)
79
- class DocumentArchiveAdmin(ModelAdmin):
80
+ class DocumentArchiveAdmin(ExportMixin, ModelAdmin):
80
81
  """Admin interface for DocumentArchive."""
81
82
 
82
83
  list_display = [
@@ -483,7 +484,7 @@ class DocumentArchiveAdmin(ModelAdmin):
483
484
 
484
485
 
485
486
  @admin.register(ArchiveItem)
486
- class ArchiveItemAdmin(ModelAdmin):
487
+ class ArchiveItemAdmin(ExportMixin, ModelAdmin):
487
488
  """Admin interface for ArchiveItem."""
488
489
 
489
490
  list_display = [
@@ -10,6 +10,7 @@ from django.db.models import Count, Sum, Avg, Q
10
10
  from unfold.admin import ModelAdmin, TabularInline
11
11
  from unfold.decorators import display
12
12
  from unfold.contrib.filters.admin import AutocompleteSelectFilter
13
+ from django_cfg import ExportMixin
13
14
 
14
15
  from ..models import ChatSession, ChatMessage
15
16
 
@@ -99,7 +100,7 @@ class ChatMessageInline(TabularInline):
99
100
 
100
101
 
101
102
  @admin.register(ChatSession)
102
- class ChatSessionAdmin(ModelAdmin):
103
+ class ChatSessionAdmin(ModelAdmin, ExportMixin):
103
104
  """Admin interface for ChatSession model with Unfold styling."""
104
105
 
105
106
  list_display = [
@@ -224,7 +225,7 @@ class ChatSessionAdmin(ModelAdmin):
224
225
 
225
226
 
226
227
  @admin.register(ChatMessage)
227
- class ChatMessageAdmin(ModelAdmin):
228
+ class ChatMessageAdmin(ModelAdmin, ExportMixin):
228
229
  """Admin interface for ChatMessage model with Unfold styling."""
229
230
 
230
231
  list_display = [
@@ -15,6 +15,7 @@ from unfold.admin import ModelAdmin, TabularInline
15
15
  from unfold.decorators import display
16
16
  from unfold.contrib.filters.admin import AutocompleteSelectFilter, AutocompleteSelectMultipleFilter
17
17
  from unfold.contrib.forms.widgets import WysiwygWidget
18
+ from django_cfg import ImportExportModelAdmin, ExportMixin, ImportForm, ExportForm
18
19
 
19
20
  from ..models import Document, DocumentChunk, DocumentCategory
20
21
 
@@ -77,9 +78,13 @@ class DocumentChunkInline(TabularInline):
77
78
 
78
79
 
79
80
  @admin.register(Document)
80
- class DocumentAdmin(ModelAdmin):
81
+ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
81
82
  """Admin interface for Document model with Unfold styling."""
82
83
 
84
+ # Import/Export configuration
85
+ import_form_class = ImportForm
86
+ export_form_class = ExportForm
87
+
83
88
  list_display = [
84
89
  'title_display', 'categories_display', 'user',
85
90
  'visibility_badge', 'status_badge', 'chunks_count_display', 'vectorization_progress', 'tokens_display', 'cost_display', 'created_at'
@@ -568,9 +573,13 @@ class DocumentChunkAdmin(ModelAdmin):
568
573
 
569
574
 
570
575
  @admin.register(DocumentCategory)
571
- class DocumentCategoryAdmin(ModelAdmin):
576
+ class DocumentCategoryAdmin(ModelAdmin, ImportExportModelAdmin):
572
577
  """Admin interface for DocumentCategory model with Unfold styling."""
573
578
 
579
+ # Import/Export configuration
580
+ import_form_class = ImportForm
581
+ export_form_class = ExportForm
582
+
574
583
  list_display = [
575
584
  'short_uuid', 'name', 'visibility_badge', 'document_count', 'created_at'
576
585
  ]
@@ -18,6 +18,7 @@ from unfold.decorators import display, action
18
18
  from unfold.enums import ActionVariant
19
19
  from unfold.contrib.filters.admin import AutocompleteSelectFilter, AutocompleteSelectMultipleFilter
20
20
  from unfold.contrib.forms.widgets import WysiwygWidget
21
+ from django_cfg import ExportMixin
21
22
 
22
23
  from ..models.external_data import ExternalData, ExternalDataChunk, ExternalDataType, ExternalDataStatus
23
24
 
@@ -80,7 +81,7 @@ class ExternalDataChunkInline(TabularInline):
80
81
 
81
82
 
82
83
  @admin.register(ExternalData)
83
- class ExternalDataAdmin(ModelAdmin):
84
+ class ExternalDataAdmin(ModelAdmin, ExportMixin):
84
85
  """Admin interface for ExternalData model with Unfold styling."""
85
86
 
86
87
  list_display = [
django_cfg/apps/urls.py CHANGED
@@ -45,8 +45,8 @@ def get_django_cfg_urlpatterns() -> List[URLPattern]:
45
45
  # patterns.append(path('leads/', include('django_cfg.apps.leads.urls')))
46
46
 
47
47
  # Tasks app - enabled when knowbase or agents are enabled
48
- if base_module.is_tasks_enabled():
49
- patterns.append(path('tasks/', include('django_cfg.apps.tasks.urls')))
48
+ # if base_module.is_tasks_enabled():
49
+ # patterns.append(path('tasks/', include('django_cfg.apps.tasks.urls')))
50
50
 
51
51
  except Exception:
52
52
  # Fallback: include all URLs if config is not available
@@ -8,27 +8,34 @@ Provides seamless integration without unnecessary wrappers.
8
8
  # Re-export original classes through django-cfg registry
9
9
  from import_export.admin import ImportExportMixin as BaseImportExportMixin, ImportExportModelAdmin as BaseImportExportModelAdmin, ExportMixin as BaseExportMixin, ImportMixin as BaseImportMixin
10
10
  from import_export.resources import ModelResource as BaseResource
11
- from import_export.forms import ImportForm, ExportForm, SelectableFieldsExportForm
11
+ # Use Unfold styled forms instead of default ones
12
+ from unfold.contrib.import_export.forms import ImportForm, ExportForm, SelectableFieldsExportForm
12
13
 
13
14
 
14
15
  class ImportExportMixin(BaseImportExportMixin):
15
- """Django-CFG enhanced ImportExportMixin with custom templates."""
16
+ """Django-CFG enhanced ImportExportMixin with custom templates and Unfold forms."""
16
17
  change_list_template = 'admin/import_export/change_list_import_export.html'
18
+ import_form_class = ImportForm
19
+ export_form_class = ExportForm
17
20
 
18
21
 
19
22
  class ImportExportModelAdmin(BaseImportExportModelAdmin):
20
- """Django-CFG enhanced ImportExportModelAdmin with custom templates."""
23
+ """Django-CFG enhanced ImportExportModelAdmin with custom templates and Unfold forms."""
21
24
  change_list_template = 'admin/import_export/change_list_import_export.html'
25
+ import_form_class = ImportForm
26
+ export_form_class = ExportForm
22
27
 
23
28
 
24
29
  class ExportMixin(BaseExportMixin):
25
- """Django-CFG enhanced ExportMixin with custom templates."""
30
+ """Django-CFG enhanced ExportMixin with custom templates and Unfold forms."""
26
31
  change_list_template = 'admin/import_export/change_list_export.html'
32
+ export_form_class = ExportForm
27
33
 
28
34
 
29
35
  class ImportMixin(BaseImportMixin):
30
- """Django-CFG enhanced ImportMixin with custom templates."""
36
+ """Django-CFG enhanced ImportMixin with custom templates and Unfold forms."""
31
37
  change_list_template = 'admin/import_export/change_list_import.html'
38
+ import_form_class = ImportForm
32
39
 
33
40
 
34
41
  __all__ = [
@@ -0,0 +1,9 @@
1
+ """
2
+ Django CFG Unfold Callbacks Module
3
+
4
+ Modular callback system for Django Unfold dashboard.
5
+ """
6
+
7
+ from .main import UnfoldCallbacks
8
+
9
+ __all__ = ['UnfoldCallbacks']
@@ -0,0 +1,50 @@
1
+ """
2
+ Quick actions callbacks.
3
+ """
4
+
5
+ import logging
6
+ from typing import List
7
+
8
+ from ..models.dashboard import QuickAction
9
+ from ..icons import Icons
10
+ from .base import get_user_admin_urls
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class ActionsCallbacks:
16
+ """Quick actions callbacks."""
17
+
18
+ def get_quick_actions(self) -> List[QuickAction]:
19
+ """Get quick action buttons as Pydantic models."""
20
+ # Get user admin URLs dynamically based on AUTH_USER_MODEL
21
+ user_admin_urls = get_user_admin_urls()
22
+
23
+ actions = [
24
+ QuickAction(
25
+ title="Add User",
26
+ description="Create new user account",
27
+ icon=Icons.PERSON_ADD,
28
+ link=user_admin_urls["add"],
29
+ color="primary",
30
+ category="admin",
31
+ ),
32
+ QuickAction(
33
+ title="Support Tickets",
34
+ description="Manage support tickets",
35
+ icon=Icons.SUPPORT_AGENT,
36
+ link="admin:django_cfg_support_ticket_changelist",
37
+ color="primary",
38
+ category="support",
39
+ ),
40
+ QuickAction(
41
+ title="Health Check",
42
+ description="System health status",
43
+ icon=Icons.HEALTH_AND_SAFETY,
44
+ link="/cfg/health/",
45
+ color="success",
46
+ category="system",
47
+ ),
48
+ ]
49
+
50
+ return actions
@@ -0,0 +1,98 @@
1
+ """
2
+ Base utilities and helper functions for callbacks.
3
+ """
4
+
5
+ import logging
6
+ from typing import Dict, Any, List
7
+ from django.contrib.auth import get_user_model
8
+ from django.core.management import get_commands
9
+ import importlib
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def get_available_commands():
15
+ """Get all available Django management commands."""
16
+ commands_dict = get_commands()
17
+ commands_list = []
18
+
19
+ for command_name, app_name in commands_dict.items():
20
+ try:
21
+ # Try to get command description
22
+ if app_name == 'django_cfg':
23
+ module_path = f'django_cfg.management.commands.{command_name}'
24
+ else:
25
+ module_path = f'{app_name}.management.commands.{command_name}'
26
+
27
+ try:
28
+ command_module = importlib.import_module(module_path)
29
+ if hasattr(command_module, 'Command'):
30
+ command_class = command_module.Command
31
+ description = getattr(command_class, 'help', f'{command_name} command')
32
+ else:
33
+ description = f'{command_name} command'
34
+ except ImportError:
35
+ description = f'{command_name} command'
36
+
37
+ commands_list.append({
38
+ 'name': command_name,
39
+ 'app': app_name,
40
+ 'description': description,
41
+ 'is_core': app_name.startswith('django.'),
42
+ 'is_custom': app_name == 'django_cfg',
43
+ })
44
+ except Exception:
45
+ # Skip problematic commands
46
+ continue
47
+
48
+ return commands_list
49
+
50
+
51
+ def get_commands_by_category():
52
+ """Get commands categorized by type."""
53
+ commands = get_available_commands()
54
+
55
+ categorized = {
56
+ 'django_cfg': [],
57
+ 'django_core': [],
58
+ 'third_party': [],
59
+ 'project': [],
60
+ }
61
+
62
+ for cmd in commands:
63
+ if cmd['app'] == 'django_cfg':
64
+ categorized['django_cfg'].append(cmd)
65
+ elif cmd['app'].startswith('django.'):
66
+ categorized['django_core'].append(cmd)
67
+ elif cmd['app'].startswith(('src.', 'api.', 'accounts.')):
68
+ categorized['project'].append(cmd)
69
+ else:
70
+ categorized['third_party'].append(cmd)
71
+
72
+ return categorized
73
+
74
+
75
+ def get_user_admin_urls():
76
+ """Get admin URLs for user model."""
77
+ try:
78
+ User = get_user_model()
79
+
80
+ app_label = User._meta.app_label
81
+ model_name = User._meta.model_name
82
+
83
+ return {
84
+ 'changelist': f'admin:{app_label}_{model_name}_changelist',
85
+ 'add': f'admin:{app_label}_{model_name}_add',
86
+ 'change': f'admin:{app_label}_{model_name}_change/{{id}}/',
87
+ 'delete': f'admin:{app_label}_{model_name}_delete/{{id}}/',
88
+ 'view': f'admin:{app_label}_{model_name}_view/{{id}}/',
89
+ }
90
+ except Exception:
91
+ # Universal fallback - return admin index for all actions
92
+ return {
93
+ 'changelist': 'admin:index',
94
+ 'add': 'admin:index',
95
+ 'change': 'admin:index',
96
+ 'delete': 'admin:index',
97
+ 'view': 'admin:index',
98
+ }
@@ -0,0 +1,224 @@
1
+ """
2
+ Charts data callbacks.
3
+ """
4
+
5
+ import logging
6
+ import random
7
+ from typing import Dict, Any, List
8
+ from datetime import timedelta
9
+
10
+ from django.db.models import Count
11
+ from django.utils import timezone
12
+ from django.contrib.auth import get_user_model
13
+
14
+ from ..models.dashboard import ChartData, ChartDataset
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ChartsCallbacks:
20
+ """Charts data callbacks."""
21
+
22
+ def _get_user_model(self):
23
+ """Get the user model safely."""
24
+ return get_user_model()
25
+
26
+ def _get_empty_chart_data(self, label: str) -> Dict[str, Any]:
27
+ """Get empty chart data structure."""
28
+ return ChartData(
29
+ labels=["No Data"],
30
+ datasets=[
31
+ ChartDataset(
32
+ label=label,
33
+ data=[0],
34
+ backgroundColor="rgba(156, 163, 175, 0.1)",
35
+ borderColor="rgb(156, 163, 175)",
36
+ tension=0.4
37
+ )
38
+ ]
39
+ ).model_dump()
40
+
41
+ def get_user_registration_chart_data(self) -> Dict[str, Any]:
42
+ """Get user registration chart data."""
43
+ try:
44
+ # Avoid database access during app initialization
45
+ from django.apps import apps
46
+ if not apps.ready:
47
+ return self._get_empty_chart_data("New Users")
48
+
49
+ User = self._get_user_model()
50
+
51
+ # Get last 7 days of registration data
52
+ end_date = timezone.now().date()
53
+ start_date = end_date - timedelta(days=6)
54
+
55
+ # Generate date range
56
+ date_range = []
57
+ current_date = start_date
58
+ while current_date <= end_date:
59
+ date_range.append(current_date)
60
+ current_date += timedelta(days=1)
61
+
62
+ # Get registration counts by date
63
+ registration_data = (
64
+ User.objects.filter(date_joined__date__gte=start_date)
65
+ .extra({'date': "date(date_joined)"})
66
+ .values('date')
67
+ .annotate(count=Count('id'))
68
+ .order_by('date')
69
+ )
70
+
71
+ # Create data dictionary for easy lookup
72
+ data_dict = {item['date']: item['count'] for item in registration_data}
73
+
74
+ # Build chart data
75
+ labels = [date.strftime("%m/%d") for date in date_range]
76
+ data_points = [data_dict.get(date, 0) for date in date_range]
77
+
78
+ chart_data = ChartData(
79
+ labels=labels,
80
+ datasets=[
81
+ ChartDataset(
82
+ label="New Users",
83
+ data=data_points,
84
+ backgroundColor="rgba(59, 130, 246, 0.1)",
85
+ borderColor="rgb(59, 130, 246)",
86
+ tension=0.4
87
+ )
88
+ ]
89
+ )
90
+
91
+ return chart_data.model_dump()
92
+
93
+ except Exception as e:
94
+ logger.error(f"Error getting user registration chart data: {e}")
95
+ return self._get_empty_chart_data("New Users")
96
+
97
+ def get_user_activity_chart_data(self) -> Dict[str, Any]:
98
+ """Get user activity chart data."""
99
+ try:
100
+ # Avoid database access during app initialization
101
+ from django.apps import apps
102
+ if not apps.ready:
103
+ return self._get_empty_chart_data("Active Users")
104
+
105
+ User = self._get_user_model()
106
+
107
+ # Get activity data for last 7 days
108
+ end_date = timezone.now().date()
109
+ start_date = end_date - timedelta(days=6)
110
+
111
+ # Generate date range
112
+ date_range = []
113
+ current_date = start_date
114
+ while current_date <= end_date:
115
+ date_range.append(current_date)
116
+ current_date += timedelta(days=1)
117
+
118
+ # Get login activity (users who logged in each day)
119
+ activity_data = (
120
+ User.objects.filter(last_login__date__gte=start_date, last_login__isnull=False)
121
+ .extra({'date': "date(last_login)"})
122
+ .values('date')
123
+ .annotate(count=Count('id'))
124
+ .order_by('date')
125
+ )
126
+
127
+ # Create data dictionary for easy lookup
128
+ data_dict = {item['date']: item['count'] for item in activity_data}
129
+
130
+ # Build chart data
131
+ labels = [date.strftime("%m/%d") for date in date_range]
132
+ data_points = [data_dict.get(date, 0) for date in date_range]
133
+
134
+ chart_data = ChartData(
135
+ labels=labels,
136
+ datasets=[
137
+ ChartDataset(
138
+ label="Active Users",
139
+ data=data_points,
140
+ backgroundColor="rgba(34, 197, 94, 0.1)",
141
+ borderColor="rgb(34, 197, 94)",
142
+ tension=0.4
143
+ )
144
+ ]
145
+ )
146
+
147
+ return chart_data.model_dump()
148
+
149
+ except Exception as e:
150
+ logger.error(f"Error getting user activity chart data: {e}")
151
+ return self._get_empty_chart_data("Active Users")
152
+
153
+ def get_activity_tracker_data(self) -> List[Dict[str, str]]:
154
+ """Get activity tracker data for the last 52 weeks (GitHub-style)."""
155
+ try:
156
+ # Avoid database access during app initialization
157
+ from django.apps import apps
158
+ if not apps.ready:
159
+ return self._get_empty_tracker_data()
160
+
161
+ User = self._get_user_model()
162
+
163
+ # Get data for last 52 weeks (365 days)
164
+ end_date = timezone.now().date()
165
+ start_date = end_date - timedelta(days=364) # 52 weeks * 7 days - 1
166
+
167
+ # Get activity data by date
168
+ activity_data = (
169
+ User.objects.filter(last_login__date__gte=start_date, last_login__isnull=False)
170
+ .extra({'date': "date(last_login)"})
171
+ .values('date')
172
+ .annotate(count=Count('id'))
173
+ .order_by('date')
174
+ )
175
+
176
+ # Create data dictionary for easy lookup
177
+ data_dict = {item['date']: item['count'] for item in activity_data}
178
+
179
+ # Generate tracker data for each day
180
+ tracker_data = []
181
+ current_date = start_date
182
+
183
+ while current_date <= end_date:
184
+ activity_count = data_dict.get(current_date, 0)
185
+
186
+ # Determine color based on activity level
187
+ if activity_count == 0:
188
+ color = "bg-base-200 dark:bg-base-700"
189
+ level = "No activity"
190
+ elif activity_count <= 2:
191
+ color = "bg-green-200 dark:bg-green-800"
192
+ level = "Low activity"
193
+ elif activity_count <= 5:
194
+ color = "bg-green-400 dark:bg-green-600"
195
+ level = "Medium activity"
196
+ elif activity_count <= 10:
197
+ color = "bg-green-600 dark:bg-green-500"
198
+ level = "High activity"
199
+ else:
200
+ color = "bg-green-800 dark:bg-green-400"
201
+ level = "Very high activity"
202
+
203
+ tracker_data.append({
204
+ "color": color,
205
+ "tooltip": f"{current_date.strftime('%Y-%m-%d')}: {activity_count} active users ({level})"
206
+ })
207
+
208
+ current_date += timedelta(days=1)
209
+
210
+ return tracker_data
211
+
212
+ except Exception as e:
213
+ logger.error(f"Error getting activity tracker data: {e}")
214
+ return self._get_empty_tracker_data()
215
+
216
+ def _get_empty_tracker_data(self) -> List[Dict[str, str]]:
217
+ """Get empty tracker data (365 days of no activity)."""
218
+ tracker_data = []
219
+ for i in range(365):
220
+ tracker_data.append({
221
+ "color": "bg-base-200 dark:bg-base-700",
222
+ "tooltip": f"Day {i + 1}: No data available"
223
+ })
224
+ return tracker_data