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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/agents/admin/execution_admin.py +9 -2
- django_cfg/apps/agents/admin/registry_admin.py +10 -2
- django_cfg/apps/agents/admin/toolsets_admin.py +14 -3
- django_cfg/apps/knowbase/admin/archive_admin.py +3 -2
- django_cfg/apps/knowbase/admin/chat_admin.py +3 -2
- django_cfg/apps/knowbase/admin/document_admin.py +11 -2
- django_cfg/apps/knowbase/admin/external_data_admin.py +2 -1
- django_cfg/apps/urls.py +2 -2
- django_cfg/modules/django_import_export/__init__.py +12 -5
- django_cfg/modules/django_unfold/callbacks/__init__.py +9 -0
- django_cfg/modules/django_unfold/callbacks/actions.py +50 -0
- django_cfg/modules/django_unfold/callbacks/base.py +98 -0
- django_cfg/modules/django_unfold/callbacks/charts.py +224 -0
- django_cfg/modules/django_unfold/callbacks/commands.py +40 -0
- django_cfg/modules/django_unfold/callbacks/main.py +191 -0
- django_cfg/modules/django_unfold/callbacks/revolution.py +76 -0
- django_cfg/modules/django_unfold/callbacks/statistics.py +240 -0
- django_cfg/modules/django_unfold/callbacks/system.py +180 -0
- django_cfg/modules/django_unfold/callbacks/users.py +65 -0
- django_cfg/modules/django_unfold/models/config.py +10 -3
- django_cfg/modules/django_unfold/tailwind.py +68 -0
- django_cfg/templates/admin/components/action_grid.html +49 -0
- django_cfg/templates/admin/components/card.html +50 -0
- django_cfg/templates/admin/components/data_table.html +67 -0
- django_cfg/templates/admin/components/metric_card.html +39 -0
- django_cfg/templates/admin/components/modal.html +58 -0
- django_cfg/templates/admin/components/progress_bar.html +25 -0
- django_cfg/templates/admin/components/section_header.html +26 -0
- django_cfg/templates/admin/components/stat_item.html +32 -0
- django_cfg/templates/admin/components/stats_grid.html +72 -0
- django_cfg/templates/admin/components/status_badge.html +28 -0
- django_cfg/templates/admin/components/user_avatar.html +27 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +7 -7
- django_cfg/templates/admin/snippets/components/activity_tracker.html +48 -11
- django_cfg/templates/admin/snippets/components/charts_section.html +63 -13
- django_cfg/templates/admin/snippets/components/django_commands.html +18 -18
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -47
- django_cfg/templates/admin/snippets/components/recent_activity.html +28 -38
- django_cfg/templates/admin/snippets/components/recent_users_table.html +22 -53
- django_cfg/templates/admin/snippets/components/stats_cards.html +2 -66
- django_cfg/templates/admin/snippets/components/system_health.html +13 -63
- django_cfg/templates/admin/snippets/components/system_metrics.html +8 -25
- django_cfg/templates/admin/snippets/tabs/commands_tab.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +4 -4
- django_cfg/templates/admin/snippets/zones/zones_table.html +12 -33
- django_cfg/templatetags/django_cfg.py +2 -1
- {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/METADATA +2 -1
- {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/RECORD +52 -32
- django_cfg/modules/django_unfold/callbacks.py +0 -795
- {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.6.dist-info → django_cfg-1.2.8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
"""
|
2
|
+
Django commands callbacks.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import Dict, Any
|
7
|
+
|
8
|
+
from .base import get_available_commands, get_commands_by_category
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class CommandsCallbacks:
|
14
|
+
"""Django commands callbacks."""
|
15
|
+
|
16
|
+
def get_django_commands(self) -> Dict[str, Any]:
|
17
|
+
"""Get Django management commands information."""
|
18
|
+
try:
|
19
|
+
commands = get_available_commands()
|
20
|
+
categorized = get_commands_by_category()
|
21
|
+
|
22
|
+
return {
|
23
|
+
"commands": commands,
|
24
|
+
"categorized": categorized,
|
25
|
+
"total_commands": len(commands),
|
26
|
+
"categories": list(categorized.keys()),
|
27
|
+
"core_commands": len([cmd for cmd in commands if cmd['is_core']]),
|
28
|
+
"custom_commands": len([cmd for cmd in commands if cmd['is_custom']]),
|
29
|
+
}
|
30
|
+
except Exception as e:
|
31
|
+
logger.error(f"Error getting Django commands: {e}")
|
32
|
+
# Return safe fallback to prevent dashboard from breaking
|
33
|
+
return {
|
34
|
+
"commands": [],
|
35
|
+
"categorized": {},
|
36
|
+
"total_commands": 0,
|
37
|
+
"categories": [],
|
38
|
+
"core_commands": 0,
|
39
|
+
"custom_commands": 0,
|
40
|
+
}
|
@@ -0,0 +1,191 @@
|
|
1
|
+
"""
|
2
|
+
Main Unfold Dashboard Callbacks
|
3
|
+
|
4
|
+
Combines all callback modules into a single interface.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import json
|
9
|
+
from typing import Dict, Any
|
10
|
+
|
11
|
+
from django.utils import timezone
|
12
|
+
from django.conf import settings
|
13
|
+
|
14
|
+
from ...base import BaseCfgModule
|
15
|
+
from ..models.dashboard import DashboardData
|
16
|
+
|
17
|
+
from .statistics import StatisticsCallbacks
|
18
|
+
from .system import SystemCallbacks
|
19
|
+
from .actions import ActionsCallbacks
|
20
|
+
from .charts import ChartsCallbacks
|
21
|
+
from .commands import CommandsCallbacks
|
22
|
+
from .revolution import RevolutionCallbacks
|
23
|
+
from .users import UsersCallbacks
|
24
|
+
from .base import get_user_admin_urls
|
25
|
+
|
26
|
+
logger = logging.getLogger(__name__)
|
27
|
+
|
28
|
+
|
29
|
+
class UnfoldCallbacks(
|
30
|
+
BaseCfgModule,
|
31
|
+
StatisticsCallbacks,
|
32
|
+
SystemCallbacks,
|
33
|
+
ActionsCallbacks,
|
34
|
+
ChartsCallbacks,
|
35
|
+
CommandsCallbacks,
|
36
|
+
RevolutionCallbacks,
|
37
|
+
UsersCallbacks
|
38
|
+
):
|
39
|
+
"""
|
40
|
+
Main Unfold dashboard callbacks with full system monitoring.
|
41
|
+
|
42
|
+
Combines all callback modules using multiple inheritance for
|
43
|
+
clean separation of concerns while maintaining a single interface.
|
44
|
+
"""
|
45
|
+
|
46
|
+
def main_dashboard_callback(self, request, context: Dict[str, Any]) -> Dict[str, Any]:
|
47
|
+
"""
|
48
|
+
Main dashboard callback function with comprehensive system data.
|
49
|
+
|
50
|
+
Returns all dashboard data as Pydantic models for type safety.
|
51
|
+
"""
|
52
|
+
try:
|
53
|
+
# Get dashboard data using Pydantic models
|
54
|
+
user_stats = self.get_user_statistics()
|
55
|
+
support_stats = self.get_support_statistics()
|
56
|
+
system_health = self.get_system_health()
|
57
|
+
quick_actions = self.get_quick_actions()
|
58
|
+
|
59
|
+
# Combine all stat cards
|
60
|
+
all_stats = user_stats + support_stats
|
61
|
+
|
62
|
+
dashboard_data = DashboardData(
|
63
|
+
stat_cards=all_stats,
|
64
|
+
system_health=system_health,
|
65
|
+
quick_actions=quick_actions,
|
66
|
+
last_updated=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
67
|
+
environment=getattr(settings, "ENVIRONMENT", "development"),
|
68
|
+
)
|
69
|
+
|
70
|
+
# Convert to template context (using to_dict for Unfold compatibility)
|
71
|
+
cards_data = [card.to_dict() for card in dashboard_data.stat_cards]
|
72
|
+
|
73
|
+
context.update({
|
74
|
+
# Statistics cards
|
75
|
+
"cards": cards_data,
|
76
|
+
"user_stats": [card.to_dict() for card in user_stats],
|
77
|
+
"support_stats": [card.to_dict() for card in support_stats],
|
78
|
+
|
79
|
+
# System health (convert to dict for template)
|
80
|
+
"system_health": {
|
81
|
+
item.component + "_status": item.status
|
82
|
+
for item in dashboard_data.system_health
|
83
|
+
},
|
84
|
+
|
85
|
+
# System metrics
|
86
|
+
"system_metrics": self.get_system_metrics(),
|
87
|
+
|
88
|
+
# Quick actions
|
89
|
+
"quick_actions": [
|
90
|
+
action.model_dump() for action in dashboard_data.quick_actions
|
91
|
+
],
|
92
|
+
|
93
|
+
# Additional categorized actions
|
94
|
+
"admin_actions": [
|
95
|
+
action.model_dump()
|
96
|
+
for action in dashboard_data.quick_actions
|
97
|
+
if action.category == "admin"
|
98
|
+
],
|
99
|
+
"support_actions": [
|
100
|
+
action.model_dump()
|
101
|
+
for action in dashboard_data.quick_actions
|
102
|
+
if action.category == "support"
|
103
|
+
],
|
104
|
+
"system_actions": [
|
105
|
+
action.model_dump()
|
106
|
+
for action in dashboard_data.quick_actions
|
107
|
+
if action.category == "system"
|
108
|
+
],
|
109
|
+
|
110
|
+
# Revolution zones
|
111
|
+
"zones_table": {
|
112
|
+
"headers": [
|
113
|
+
{"label": "Zone"},
|
114
|
+
{"label": "Title"},
|
115
|
+
{"label": "Apps"},
|
116
|
+
{"label": "Endpoints"},
|
117
|
+
{"label": "Status"},
|
118
|
+
{"label": "Actions"},
|
119
|
+
],
|
120
|
+
"rows": self.get_revolution_zones_data()[0],
|
121
|
+
},
|
122
|
+
|
123
|
+
# Recent users
|
124
|
+
"recent_users": self.get_recent_users(),
|
125
|
+
"user_admin_urls": get_user_admin_urls(),
|
126
|
+
|
127
|
+
# App statistics
|
128
|
+
"app_statistics": self.get_app_statistics(),
|
129
|
+
|
130
|
+
# Django commands
|
131
|
+
"django_commands": self.get_django_commands(),
|
132
|
+
|
133
|
+
# Charts data - serialize to JSON for JavaScript
|
134
|
+
"charts": {
|
135
|
+
"user_registrations_json": json.dumps(self.get_user_registration_chart_data()),
|
136
|
+
"user_activity_json": json.dumps(self.get_user_activity_chart_data()),
|
137
|
+
"user_registrations": self.get_user_registration_chart_data(),
|
138
|
+
"user_activity": self.get_user_activity_chart_data(),
|
139
|
+
},
|
140
|
+
|
141
|
+
# Activity tracker data
|
142
|
+
"activity_tracker": self.get_activity_tracker_data(),
|
143
|
+
|
144
|
+
|
145
|
+
# Meta information
|
146
|
+
"last_updated": dashboard_data.last_updated,
|
147
|
+
"environment": dashboard_data.environment,
|
148
|
+
"dashboard_title": "Django CFG Dashboard",
|
149
|
+
})
|
150
|
+
|
151
|
+
# Log charts data for debugging
|
152
|
+
charts_data = context.get('charts', {})
|
153
|
+
logger.info(f"Charts data added to context: {list(charts_data.keys())}")
|
154
|
+
if 'user_registrations' in charts_data:
|
155
|
+
reg_data = charts_data['user_registrations']
|
156
|
+
logger.info(f"Registration chart labels: {reg_data.get('labels', [])}")
|
157
|
+
if 'user_activity' in charts_data:
|
158
|
+
act_data = charts_data['user_activity']
|
159
|
+
logger.info(f"Activity chart labels: {act_data.get('labels', [])}")
|
160
|
+
|
161
|
+
# Log recent users data for debugging
|
162
|
+
recent_users_data = context.get('recent_users', [])
|
163
|
+
logger.info(f"Recent users data count: {len(recent_users_data)}")
|
164
|
+
if recent_users_data:
|
165
|
+
logger.info(f"First user: {recent_users_data[0].get('username', 'N/A')}")
|
166
|
+
|
167
|
+
# Log activity tracker data for debugging
|
168
|
+
activity_tracker_data = context.get('activity_tracker', [])
|
169
|
+
logger.info(f"Activity tracker data count: {len(activity_tracker_data)}")
|
170
|
+
|
171
|
+
return context
|
172
|
+
|
173
|
+
except Exception as e:
|
174
|
+
logger.error(f"Dashboard callback error: {e}")
|
175
|
+
# Return minimal safe defaults
|
176
|
+
context.update({
|
177
|
+
"cards": [
|
178
|
+
{
|
179
|
+
"title": "System Error",
|
180
|
+
"value": "N/A",
|
181
|
+
"icon": "error",
|
182
|
+
"color": "danger",
|
183
|
+
"description": "Dashboard data unavailable"
|
184
|
+
}
|
185
|
+
],
|
186
|
+
"system_health": {},
|
187
|
+
"quick_actions": [],
|
188
|
+
"last_updated": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
189
|
+
"error": f"Dashboard error: {str(e)}",
|
190
|
+
})
|
191
|
+
return context
|
@@ -0,0 +1,76 @@
|
|
1
|
+
"""
|
2
|
+
Django Revolution integration callbacks.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import Dict, Any, List, Tuple
|
7
|
+
|
8
|
+
from django.conf import settings
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class RevolutionCallbacks:
|
14
|
+
"""Django Revolution integration callbacks."""
|
15
|
+
|
16
|
+
def get_revolution_zones_data(self) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
17
|
+
"""Get Django Revolution zones data."""
|
18
|
+
try:
|
19
|
+
# Try to get revolution config from Django settings
|
20
|
+
revolution_config = getattr(settings, "DJANGO_REVOLUTION", {})
|
21
|
+
zones = revolution_config.get("zones", {})
|
22
|
+
api_prefix = revolution_config.get("api_prefix", "apix")
|
23
|
+
|
24
|
+
zones_data = []
|
25
|
+
total_apps = 0
|
26
|
+
total_endpoints = 0
|
27
|
+
|
28
|
+
for zone_name, zone_config in zones.items():
|
29
|
+
# Handle both dict and object access
|
30
|
+
if isinstance(zone_config, dict):
|
31
|
+
title = zone_config.get("title", zone_name.title())
|
32
|
+
description = zone_config.get("description", f"{zone_name} zone")
|
33
|
+
apps = zone_config.get("apps", [])
|
34
|
+
public = zone_config.get("public", False)
|
35
|
+
auth_required = zone_config.get("auth_required", True)
|
36
|
+
else:
|
37
|
+
# Handle object access (for ZoneConfig instances)
|
38
|
+
title = getattr(zone_config, "title", zone_name.title())
|
39
|
+
description = getattr(zone_config, "description", f"{zone_name} zone")
|
40
|
+
apps = getattr(zone_config, "apps", [])
|
41
|
+
public = getattr(zone_config, "public", False)
|
42
|
+
auth_required = getattr(zone_config, "auth_required", True)
|
43
|
+
|
44
|
+
# Count actual endpoints by checking URL patterns (simplified estimate)
|
45
|
+
endpoint_count = len(apps) * 3 # Conservative estimate
|
46
|
+
|
47
|
+
zones_data.append({
|
48
|
+
"name": zone_name,
|
49
|
+
"title": title,
|
50
|
+
"description": description,
|
51
|
+
"app_count": len(apps),
|
52
|
+
"endpoint_count": endpoint_count,
|
53
|
+
"status": "active",
|
54
|
+
"public": public,
|
55
|
+
"auth_required": auth_required,
|
56
|
+
"schema_url": f"/schema/{zone_name}/schema/",
|
57
|
+
"swagger_url": f"/schema/{zone_name}/schema/swagger/",
|
58
|
+
"redoc_url": f"/schema/{zone_name}/redoc/",
|
59
|
+
"api_url": f"/{api_prefix}/{zone_name}/",
|
60
|
+
})
|
61
|
+
|
62
|
+
total_apps += len(apps)
|
63
|
+
total_endpoints += endpoint_count
|
64
|
+
|
65
|
+
return zones_data, {
|
66
|
+
"total_apps": total_apps,
|
67
|
+
"total_endpoints": total_endpoints,
|
68
|
+
"total_zones": len(zones),
|
69
|
+
}
|
70
|
+
except Exception as e:
|
71
|
+
logger.error(f"Error getting revolution zones: {e}")
|
72
|
+
return [], {
|
73
|
+
"total_apps": 0,
|
74
|
+
"total_endpoints": 0,
|
75
|
+
"total_zones": 0,
|
76
|
+
}
|
@@ -0,0 +1,240 @@
|
|
1
|
+
"""
|
2
|
+
Statistics callbacks for dashboard.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import List, Dict, Any
|
7
|
+
from datetime import timedelta
|
8
|
+
|
9
|
+
from django.db.models import Count
|
10
|
+
from django.utils import timezone
|
11
|
+
from django.contrib.auth import get_user_model
|
12
|
+
from django.apps import apps
|
13
|
+
|
14
|
+
from ..models.dashboard import StatCard
|
15
|
+
from ..icons import Icons
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class StatisticsCallbacks:
|
21
|
+
"""Statistics-related callbacks."""
|
22
|
+
|
23
|
+
def _get_user_model(self):
|
24
|
+
"""Get the user model safely."""
|
25
|
+
return get_user_model()
|
26
|
+
|
27
|
+
def get_user_statistics(self) -> List[StatCard]:
|
28
|
+
"""Get user-related statistics as Pydantic models."""
|
29
|
+
try:
|
30
|
+
User = self._get_user_model()
|
31
|
+
|
32
|
+
total_users = User.objects.count()
|
33
|
+
active_users = User.objects.filter(is_active=True).count()
|
34
|
+
new_users_7d = User.objects.filter(
|
35
|
+
date_joined__gte=timezone.now() - timedelta(days=7)
|
36
|
+
).count()
|
37
|
+
staff_users = User.objects.filter(is_staff=True).count()
|
38
|
+
|
39
|
+
return [
|
40
|
+
StatCard(
|
41
|
+
title="Total Users",
|
42
|
+
value=f"{total_users:,}",
|
43
|
+
icon=Icons.PEOPLE,
|
44
|
+
change=f"+{new_users_7d}" if new_users_7d > 0 else None,
|
45
|
+
change_type="positive" if new_users_7d > 0 else "neutral",
|
46
|
+
description="Registered users",
|
47
|
+
),
|
48
|
+
StatCard(
|
49
|
+
title="Active Users",
|
50
|
+
value=f"{active_users:,}",
|
51
|
+
icon=Icons.PERSON,
|
52
|
+
change=(
|
53
|
+
f"{(active_users/total_users*100):.1f}%"
|
54
|
+
if total_users > 0
|
55
|
+
else "0%"
|
56
|
+
),
|
57
|
+
change_type=(
|
58
|
+
"positive" if active_users > total_users * 0.7 else "neutral"
|
59
|
+
),
|
60
|
+
description="Currently active",
|
61
|
+
),
|
62
|
+
StatCard(
|
63
|
+
title="New This Week",
|
64
|
+
value=f"{new_users_7d:,}",
|
65
|
+
icon=Icons.PERSON_ADD,
|
66
|
+
change_type="positive" if new_users_7d > 0 else "neutral",
|
67
|
+
description="Last 7 days",
|
68
|
+
),
|
69
|
+
StatCard(
|
70
|
+
title="Staff Members",
|
71
|
+
value=f"{staff_users:,}",
|
72
|
+
icon=Icons.ADMIN_PANEL_SETTINGS,
|
73
|
+
change=(
|
74
|
+
f"{(staff_users/total_users*100):.1f}%" if total_users > 0 else "0%"
|
75
|
+
),
|
76
|
+
change_type="neutral",
|
77
|
+
description="Administrative access",
|
78
|
+
),
|
79
|
+
]
|
80
|
+
except Exception as e:
|
81
|
+
logger.error(f"Error getting user statistics: {e}")
|
82
|
+
return [
|
83
|
+
StatCard(
|
84
|
+
title="Users",
|
85
|
+
value="N/A",
|
86
|
+
icon=Icons.PEOPLE,
|
87
|
+
description="Data unavailable",
|
88
|
+
)
|
89
|
+
]
|
90
|
+
|
91
|
+
def get_support_statistics(self) -> List[StatCard]:
|
92
|
+
"""Get support ticket statistics as Pydantic models."""
|
93
|
+
try:
|
94
|
+
# Check if support is enabled
|
95
|
+
if not self.is_support_enabled():
|
96
|
+
return []
|
97
|
+
|
98
|
+
from django_cfg.apps.support.models import Ticket
|
99
|
+
|
100
|
+
total_tickets = Ticket.objects.count()
|
101
|
+
open_tickets = Ticket.objects.filter(status='open').count()
|
102
|
+
resolved_tickets = Ticket.objects.filter(status='resolved').count()
|
103
|
+
new_tickets_7d = Ticket.objects.filter(
|
104
|
+
created_at__gte=timezone.now() - timedelta(days=7)
|
105
|
+
).count()
|
106
|
+
|
107
|
+
return [
|
108
|
+
StatCard(
|
109
|
+
title="Total Tickets",
|
110
|
+
value=f"{total_tickets:,}",
|
111
|
+
icon=Icons.SUPPORT_AGENT,
|
112
|
+
change=f"+{new_tickets_7d}" if new_tickets_7d > 0 else None,
|
113
|
+
change_type="positive" if new_tickets_7d > 0 else "neutral",
|
114
|
+
description="All support tickets",
|
115
|
+
),
|
116
|
+
StatCard(
|
117
|
+
title="Open Tickets",
|
118
|
+
value=f"{open_tickets:,}",
|
119
|
+
icon=Icons.PENDING,
|
120
|
+
change=(
|
121
|
+
f"{(open_tickets/total_tickets*100):.1f}%"
|
122
|
+
if total_tickets > 0
|
123
|
+
else "0%"
|
124
|
+
),
|
125
|
+
change_type=(
|
126
|
+
"negative" if open_tickets > total_tickets * 0.3
|
127
|
+
else "positive" if open_tickets == 0
|
128
|
+
else "neutral"
|
129
|
+
),
|
130
|
+
description="Awaiting response",
|
131
|
+
),
|
132
|
+
StatCard(
|
133
|
+
title="Resolved",
|
134
|
+
value=f"{resolved_tickets:,}",
|
135
|
+
icon=Icons.CHECK_CIRCLE,
|
136
|
+
change=(
|
137
|
+
f"{(resolved_tickets/total_tickets*100):.1f}%"
|
138
|
+
if total_tickets > 0
|
139
|
+
else "0%"
|
140
|
+
),
|
141
|
+
change_type="positive",
|
142
|
+
description="Successfully resolved",
|
143
|
+
),
|
144
|
+
StatCard(
|
145
|
+
title="New This Week",
|
146
|
+
value=f"{new_tickets_7d:,}",
|
147
|
+
icon=Icons.NEW_RELEASES,
|
148
|
+
change_type="positive" if new_tickets_7d > 0 else "neutral",
|
149
|
+
description="Last 7 days",
|
150
|
+
),
|
151
|
+
]
|
152
|
+
except Exception as e:
|
153
|
+
logger.error(f"Error getting support statistics: {e}")
|
154
|
+
return [
|
155
|
+
StatCard(
|
156
|
+
title="Support",
|
157
|
+
value="N/A",
|
158
|
+
icon=Icons.SUPPORT_AGENT,
|
159
|
+
description="Data unavailable",
|
160
|
+
)
|
161
|
+
]
|
162
|
+
|
163
|
+
def get_app_statistics(self) -> Dict[str, Any]:
|
164
|
+
"""Get statistics for all apps and their models."""
|
165
|
+
stats = {"apps": {}, "total_records": 0, "total_models": 0, "total_apps": 0}
|
166
|
+
|
167
|
+
# Get all installed apps
|
168
|
+
for app_config in apps.get_app_configs():
|
169
|
+
app_label = app_config.label
|
170
|
+
|
171
|
+
# Skip system apps
|
172
|
+
if app_label in ["admin", "contenttypes", "sessions", "auth"]:
|
173
|
+
continue
|
174
|
+
|
175
|
+
app_stats = self._get_app_stats(app_label)
|
176
|
+
if app_stats:
|
177
|
+
stats["apps"][app_label] = app_stats
|
178
|
+
stats["total_records"] += app_stats.get("total_records", 0)
|
179
|
+
stats["total_models"] += app_stats.get("model_count", 0)
|
180
|
+
stats["total_apps"] += 1
|
181
|
+
|
182
|
+
return stats
|
183
|
+
|
184
|
+
def _get_app_stats(self, app_label: str) -> Dict[str, Any]:
|
185
|
+
"""Get statistics for a specific app."""
|
186
|
+
try:
|
187
|
+
app_config = apps.get_app_config(app_label)
|
188
|
+
# Convert generator to list to avoid len() error
|
189
|
+
models_list = list(app_config.get_models())
|
190
|
+
|
191
|
+
if not models_list:
|
192
|
+
return None
|
193
|
+
|
194
|
+
app_stats = {
|
195
|
+
"name": app_config.verbose_name or app_label.title(),
|
196
|
+
"models": {},
|
197
|
+
"total_records": 0,
|
198
|
+
"model_count": len(models_list),
|
199
|
+
}
|
200
|
+
|
201
|
+
for model in models_list:
|
202
|
+
try:
|
203
|
+
# Get model statistics
|
204
|
+
model_stats = self._get_model_stats(model)
|
205
|
+
if model_stats:
|
206
|
+
app_stats["models"][model._meta.model_name] = model_stats
|
207
|
+
app_stats["total_records"] += model_stats.get("count", 0)
|
208
|
+
except Exception:
|
209
|
+
continue
|
210
|
+
|
211
|
+
return app_stats
|
212
|
+
|
213
|
+
except Exception:
|
214
|
+
return None
|
215
|
+
|
216
|
+
def _get_model_stats(self, model) -> Dict[str, Any]:
|
217
|
+
"""Get statistics for a specific model."""
|
218
|
+
try:
|
219
|
+
# Get basic model info
|
220
|
+
model_stats = {
|
221
|
+
"name": model._meta.verbose_name_plural
|
222
|
+
or model._meta.verbose_name
|
223
|
+
or model._meta.model_name,
|
224
|
+
"count": model.objects.count(),
|
225
|
+
"fields_count": len(model._meta.fields),
|
226
|
+
"admin_url": f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist",
|
227
|
+
}
|
228
|
+
|
229
|
+
return model_stats
|
230
|
+
|
231
|
+
except Exception:
|
232
|
+
return None
|
233
|
+
|
234
|
+
def is_support_enabled(self) -> bool:
|
235
|
+
"""Check if support module is enabled."""
|
236
|
+
try:
|
237
|
+
from django_cfg.apps.support.models import Ticket
|
238
|
+
return True
|
239
|
+
except ImportError:
|
240
|
+
return False
|