django-cfg 1.4.87__py3-none-any.whl → 1.4.89__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/centrifugo/views/__init__.py +0 -2
- django_cfg/apps/dashboard/permissions.py +48 -0
- django_cfg/apps/dashboard/serializers/__init__.py +8 -1
- django_cfg/apps/dashboard/serializers/commands.py +29 -0
- django_cfg/apps/dashboard/services/__init__.py +2 -0
- django_cfg/{modules/django_unfold/callbacks/base.py → apps/dashboard/services/commands_security.py} +28 -90
- django_cfg/apps/dashboard/services/commands_service.py +208 -9
- django_cfg/apps/dashboard/services/overview_service.py +205 -0
- django_cfg/apps/dashboard/views/commands_views.py +92 -4
- django_cfg/apps/frontend/test_routing.py +134 -0
- django_cfg/apps/frontend/views.py +73 -28
- django_cfg/apps/urls.py +0 -1
- django_cfg/core/builders/apps_builder.py +0 -58
- django_cfg/modules/django_unfold/__init__.py +5 -24
- django_cfg/modules/django_unfold/models/__init__.py +0 -23
- django_cfg/modules/django_unfold/models/config.py +11 -65
- django_cfg/modules/django_unfold/{dashboard.py → navigation.py} +21 -152
- django_cfg/modules/django_unfold/tailwind.py +2 -4
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/third_party.py +0 -9
- django_cfg/routing/callbacks.py +1 -43
- django_cfg/static/frontend/admin/404/index.html +1 -1
- django_cfg/static/frontend/admin/404.html +1 -1
- django_cfg/static/frontend/admin/500/index.html +1 -1
- django_cfg/static/frontend/admin/_next/static/{ZJZBgOL9mO1koHrgaaLEV → 0sN9ktsgXH48ygtGSrhfu}/_buildManifest.js +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{19430.fe7bff7372f8a256.js → 19430.c4c95603c23c17fe.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-9443faa6df24aebf.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/94141-bc6d47f419b26b21.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{_app-c336f254967dd101.js → _app-c7dcd3aa616fab68.js} +6 -6
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{cookies-b39c7f22c066e2c6.js → cookies-97d279800f12aab4.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{privacy-5aedad0cf3a4f80f.js → privacy-1d5e6cd94689247e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{security-dbd854d0d5d483e2.js → security-55e49700e7a01f5a.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{terms-f3e1d2b9e5edf12f.js → terms-14c02bb2d3198352.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{centrifugo-22532c65971225eb.js → centrifugo-f9ecbc3ae0052a03.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-d4ccbe1265cbd853.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{webpack-da114020a6b940f5.js → webpack-5a92f81363b62aa7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/css/3063068f0d5a8a00.css +3 -0
- django_cfg/static/frontend/admin/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/21350d82a1f187e9-s.p.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
- django_cfg/static/frontend/admin/auth/index.html +1 -1
- django_cfg/static/frontend/admin/index.html +1 -1
- django_cfg/static/frontend/admin/legal/cookies/index.html +1 -1
- django_cfg/static/frontend/admin/legal/privacy/index.html +1 -1
- django_cfg/static/frontend/admin/legal/security/index.html +1 -1
- django_cfg/static/frontend/admin/legal/terms/index.html +1 -1
- django_cfg/static/frontend/admin/private/centrifugo/index.html +1 -1
- django_cfg/static/frontend/admin/private/index.html +1 -1
- django_cfg/static/frontend/admin/private/profile/index.html +1 -1
- django_cfg/static/frontend/admin/private/ui/index.html +2 -2
- django_cfg/templates/admin/index.html +1 -1
- {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/METADATA +1 -1
- {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/RECORD +62 -163
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +0 -260
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +0 -313
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +0 -803
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +0 -341
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +0 -432
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +0 -33
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +0 -210
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +0 -46
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +0 -123
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +0 -45
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +0 -84
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/stat_cards.html +0 -53
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +0 -91
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/tab_navigation.html +0 -29
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +0 -415
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +0 -61
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +0 -58
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +0 -48
- django_cfg/apps/centrifugo/templatetags/__init__.py +0 -1
- django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +0 -81
- django_cfg/apps/centrifugo/urls_admin.py +0 -20
- django_cfg/apps/centrifugo/views/dashboard.py +0 -28
- django_cfg/modules/django_dashboard/__init__.py +0 -23
- django_cfg/modules/django_dashboard/components.py +0 -312
- django_cfg/modules/django_dashboard/debug.py +0 -174
- django_cfg/modules/django_dashboard/management/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/debug_dashboard.py +0 -109
- django_cfg/modules/django_dashboard/sections/__init__.py +0 -1
- django_cfg/modules/django_dashboard/sections/base.py +0 -129
- django_cfg/modules/django_dashboard/sections/commands.py +0 -33
- django_cfg/modules/django_dashboard/sections/documentation.py +0 -393
- django_cfg/modules/django_dashboard/sections/overview.py +0 -398
- django_cfg/modules/django_dashboard/sections/stats.py +0 -48
- django_cfg/modules/django_dashboard/sections/system.py +0 -74
- django_cfg/modules/django_dashboard/sections/widgets.py +0 -222
- django_cfg/modules/django_unfold/callbacks/__init__.py +0 -9
- django_cfg/modules/django_unfold/callbacks/actions.py +0 -51
- django_cfg/modules/django_unfold/callbacks/apizones.py +0 -122
- django_cfg/modules/django_unfold/callbacks/charts.py +0 -223
- django_cfg/modules/django_unfold/callbacks/commands.py +0 -40
- django_cfg/modules/django_unfold/callbacks/main.py +0 -322
- django_cfg/modules/django_unfold/callbacks/statistics.py +0 -240
- django_cfg/modules/django_unfold/callbacks/system.py +0 -180
- django_cfg/modules/django_unfold/callbacks/users.py +0 -65
- django_cfg/modules/django_unfold/models/dashboard.py +0 -207
- django_cfg/modules/django_unfold/models/tabs.py +0 -26
- django_cfg/modules/django_unfold/models.py +0 -98
- django_cfg/modules/django_unfold/templates/unfold/helpers/app_list.html +0 -102
- django_cfg/static/frontend/admin/_next/static/chunks/23004-faae121bbfecc163.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-48bd5701f62faf27.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-fe9faa86ecdb0ce6.js +0 -1
- django_cfg/static/frontend/admin/_next/static/css/5f9a37b6e6a72303.css +0 -3
- django_cfg/static/frontend/admin/_next/static/media/438aa629764e75f3-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/4c9affa5bc8f420e-s.p.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/51251f8b9793cdb3-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/875ae681bfde4580-s.p.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/cc978ac5ee68c2b6-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/e857b654a2caa584-s.woff2 +0 -0
- django_cfg/templates/admin/sections/commands_section.html +0 -5
- django_cfg/templates/admin/sections/documentation_section.html +0 -5
- django_cfg/templates/admin/sections/overview_section.html +0 -5
- django_cfg/templates/admin/sections/stats_section.html +0 -5
- django_cfg/templates/admin/sections/system_section.html +0 -5
- django_cfg/templates/admin/sections/widgets_section.html +0 -11
- django_cfg/templates/admin_old/components/action_grid.html +0 -49
- django_cfg/templates/admin_old/components/card.html +0 -50
- django_cfg/templates/admin_old/components/data_table.html +0 -67
- django_cfg/templates/admin_old/components/metric_card.html +0 -39
- django_cfg/templates/admin_old/components/modal.html +0 -58
- django_cfg/templates/admin_old/components/progress_bar.html +0 -20
- django_cfg/templates/admin_old/components/section_header.html +0 -26
- django_cfg/templates/admin_old/components/stat_item.html +0 -32
- django_cfg/templates/admin_old/components/stats_grid.html +0 -72
- django_cfg/templates/admin_old/components/status_badge.html +0 -28
- django_cfg/templates/admin_old/components/user_avatar.html +0 -27
- django_cfg/templates/admin_old/constance/change_list.html +0 -74
- django_cfg/templates/admin_old/constance/includes/default_value.html +0 -24
- django_cfg/templates/admin_old/constance/includes/fieldset_header.html +0 -15
- django_cfg/templates/admin_old/constance/includes/results_list.html +0 -16
- django_cfg/templates/admin_old/constance/includes/setting_row.html +0 -50
- django_cfg/templates/admin_old/constance/includes/table_headers.html +0 -10
- django_cfg/templates/admin_old/examples/component_class_example.html +0 -156
- django_cfg/templates/admin_old/import_export/change_list_export.html +0 -24
- django_cfg/templates/admin_old/import_export/change_list_import.html +0 -24
- django_cfg/templates/admin_old/import_export/change_list_import_export.html +0 -34
- django_cfg/templates/admin_old/index.html +0 -80
- django_cfg/templates/admin_old/index_new.html +0 -119
- django_cfg/templates/admin_old/layouts/base_dashboard.html +0 -62
- django_cfg/templates/admin_old/layouts/dashboard_with_tabs.html +0 -176
- django_cfg/templates/admin_old/sections/commands_section.html +0 -549
- django_cfg/templates/admin_old/sections/documentation_section.html +0 -152
- django_cfg/templates/admin_old/sections/overview_section.html +0 -112
- django_cfg/templates/admin_old/sections/stats_section.html +0 -35
- django_cfg/templates/admin_old/sections/system_section.html +0 -99
- django_cfg/templates/admin_old/sections/widgets_section.html +0 -129
- django_cfg/templates/admin_old/snippets/components/activity_tracker.html +0 -70
- django_cfg/templates/admin_old/snippets/components/charts_section.html +0 -113
- django_cfg/templates/admin_old/snippets/components/django_commands.html +0 -270
- django_cfg/templates/admin_old/snippets/components/quick_actions.html +0 -66
- django_cfg/templates/admin_old/snippets/components/recent_activity_improved.html +0 -25
- django_cfg/templates/admin_old/snippets/components/recent_users_table.html +0 -102
- django_cfg/templates/admin_old/snippets/components/stats_cards.html +0 -4
- django_cfg/templates/admin_old/snippets/components/stats_tiles.html +0 -92
- django_cfg/templates/admin_old/snippets/components/system_health.html +0 -22
- django_cfg/templates/admin_old/snippets/components/system_metrics.html +0 -199
- django_cfg/templates/admin_old/snippets/components/user_permissions.html +0 -57
- django_cfg/templates/admin_old/snippets/tabs/app_stats_tab.html +0 -201
- django_cfg/templates/admin_old/snippets/tabs/commands_tab.html +0 -114
- django_cfg/templates/admin_old/snippets/tabs/documentation_tab.html +0 -42
- django_cfg/templates/admin_old/snippets/tabs/overview_tab.html +0 -116
- django_cfg/templates/admin_old/snippets/tabs/stats_tab.html +0 -89
- django_cfg/templates/admin_old/snippets/tabs/users_tab.html +0 -51
- django_cfg/templates/admin_old/snippets/tabs/widgets_tab.html +0 -38
- django_cfg/templates/admin_old/snippets/zones/zones_table.html +0 -176
- /django_cfg/static/frontend/admin/_next/static/{ZJZBgOL9mO1koHrgaaLEV → 0sN9ktsgXH48ygtGSrhfu}/_ssgManifest.js +0 -0
- {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Management command to debug dashboard rendering.
|
|
3
|
-
|
|
4
|
-
Renders all dashboard sections and saves them for inspection.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from django.contrib.auth import get_user_model
|
|
8
|
-
from django.core.management.base import BaseCommand
|
|
9
|
-
from django.test import RequestFactory
|
|
10
|
-
|
|
11
|
-
from django_cfg.modules.django_dashboard.debug import get_debugger
|
|
12
|
-
from django_cfg.modules.django_dashboard.sections.commands import CommandsSection
|
|
13
|
-
from django_cfg.modules.django_dashboard.sections.overview import OverviewSection
|
|
14
|
-
from django_cfg.modules.django_dashboard.sections.stats import StatsSection
|
|
15
|
-
from django_cfg.modules.django_dashboard.sections.system import SystemSection
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class Command(BaseCommand):
|
|
19
|
-
help = "Debug dashboard rendering - saves all sections to disk"
|
|
20
|
-
|
|
21
|
-
def add_arguments(self, parser):
|
|
22
|
-
parser.add_argument(
|
|
23
|
-
'--section',
|
|
24
|
-
type=str,
|
|
25
|
-
choices=['overview', 'stats', 'system', 'commands', 'all'],
|
|
26
|
-
default='all',
|
|
27
|
-
help='Which section to render (default: all)'
|
|
28
|
-
)
|
|
29
|
-
parser.add_argument(
|
|
30
|
-
'--user',
|
|
31
|
-
type=str,
|
|
32
|
-
help='Username to use for request (default: first superuser)'
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
def handle(self, *args, **options):
|
|
36
|
-
# Create mock request
|
|
37
|
-
factory = RequestFactory()
|
|
38
|
-
request = factory.get('/admin/')
|
|
39
|
-
|
|
40
|
-
# Get user for request
|
|
41
|
-
User = get_user_model()
|
|
42
|
-
username = options.get('user')
|
|
43
|
-
|
|
44
|
-
if username:
|
|
45
|
-
try:
|
|
46
|
-
user = User.objects.get(username=username)
|
|
47
|
-
except User.DoesNotExist:
|
|
48
|
-
self.stdout.write(
|
|
49
|
-
self.style.ERROR(f"User '{username}' not found")
|
|
50
|
-
)
|
|
51
|
-
return
|
|
52
|
-
else:
|
|
53
|
-
# Use first superuser
|
|
54
|
-
user = User.objects.filter(is_superuser=True).first()
|
|
55
|
-
if not user:
|
|
56
|
-
self.stdout.write(
|
|
57
|
-
self.style.WARNING("No superuser found, using anonymous request")
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
if user:
|
|
61
|
-
request.user = user
|
|
62
|
-
self.stdout.write(f"Using user: {user.username}")
|
|
63
|
-
|
|
64
|
-
# Get debugger
|
|
65
|
-
debugger = get_debugger()
|
|
66
|
-
self.stdout.write(f"Saving renders to: {debugger.output_dir}")
|
|
67
|
-
|
|
68
|
-
section_choice = options['section']
|
|
69
|
-
sections = {
|
|
70
|
-
'overview': OverviewSection,
|
|
71
|
-
'stats': StatsSection,
|
|
72
|
-
'system': SystemSection,
|
|
73
|
-
'commands': CommandsSection,
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if section_choice == 'all':
|
|
77
|
-
sections_to_render = sections.items()
|
|
78
|
-
else:
|
|
79
|
-
sections_to_render = [(section_choice, sections[section_choice])]
|
|
80
|
-
|
|
81
|
-
# Render sections
|
|
82
|
-
for name, SectionClass in sections_to_render:
|
|
83
|
-
self.stdout.write(f"\nRendering {name} section...")
|
|
84
|
-
|
|
85
|
-
try:
|
|
86
|
-
section = SectionClass(request)
|
|
87
|
-
html = section.render()
|
|
88
|
-
|
|
89
|
-
# Save render
|
|
90
|
-
path = debugger.save_section_render(
|
|
91
|
-
section_name=name,
|
|
92
|
-
html=html,
|
|
93
|
-
section_data=section.get_context_data() if hasattr(section, 'get_context_data') else None
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
self.stdout.write(
|
|
97
|
-
self.style.SUCCESS(f"✅ {name}: {len(html)} bytes, saved to {path.name}")
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
except Exception as e:
|
|
101
|
-
self.stdout.write(
|
|
102
|
-
self.style.ERROR(f"❌ {name}: {e}")
|
|
103
|
-
)
|
|
104
|
-
import traceback
|
|
105
|
-
traceback.print_exc()
|
|
106
|
-
|
|
107
|
-
self.stdout.write(
|
|
108
|
-
self.style.SUCCESS(f"\n✅ Done! Check renders in: {debugger.output_dir}")
|
|
109
|
-
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Dashboard sections package."""
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Base dashboard section classes.
|
|
3
|
-
|
|
4
|
-
Provides foundation for dashboard sections following Unfold's pattern.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from typing import Any, Dict, Optional
|
|
8
|
-
|
|
9
|
-
from django.http import HttpRequest
|
|
10
|
-
from django.template.loader import render_to_string
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class DashboardSection:
|
|
14
|
-
"""
|
|
15
|
-
Base class for dashboard sections.
|
|
16
|
-
|
|
17
|
-
Each section is responsible for:
|
|
18
|
-
- Data collection
|
|
19
|
-
- Context preparation
|
|
20
|
-
- Template rendering
|
|
21
|
-
|
|
22
|
-
Usage:
|
|
23
|
-
class MySection(DashboardSection):
|
|
24
|
-
template_name = "admin/sections/my_section.html"
|
|
25
|
-
|
|
26
|
-
def get_context_data(self, **kwargs):
|
|
27
|
-
context = super().get_context_data(**kwargs)
|
|
28
|
-
context['custom_data'] = self.get_custom_data()
|
|
29
|
-
return context
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
template_name: Optional[str] = None
|
|
33
|
-
title: Optional[str] = None
|
|
34
|
-
icon: Optional[str] = None
|
|
35
|
-
|
|
36
|
-
def __init__(self, request: HttpRequest) -> None:
|
|
37
|
-
"""Initialize section with request context."""
|
|
38
|
-
self.request = request
|
|
39
|
-
|
|
40
|
-
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
|
41
|
-
"""
|
|
42
|
-
Get context data for template rendering.
|
|
43
|
-
|
|
44
|
-
Override this method to add custom context.
|
|
45
|
-
"""
|
|
46
|
-
context = {
|
|
47
|
-
'request': self.request,
|
|
48
|
-
'section': self,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if self.title:
|
|
52
|
-
context['title'] = self.title
|
|
53
|
-
|
|
54
|
-
if self.icon:
|
|
55
|
-
context['icon'] = self.icon
|
|
56
|
-
|
|
57
|
-
context.update(kwargs)
|
|
58
|
-
return context
|
|
59
|
-
|
|
60
|
-
def render(self, **kwargs) -> str:
|
|
61
|
-
"""
|
|
62
|
-
Render the section template.
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
**kwargs: Additional context to pass to template
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
Rendered HTML string
|
|
69
|
-
"""
|
|
70
|
-
if not self.template_name:
|
|
71
|
-
raise NotImplementedError(
|
|
72
|
-
f"{self.__class__.__name__} must define 'template_name' or override render()"
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
context = self.get_context_data(**kwargs)
|
|
76
|
-
return render_to_string(self.template_name, context=context, request=self.request)
|
|
77
|
-
|
|
78
|
-
def __str__(self) -> str:
|
|
79
|
-
"""String representation."""
|
|
80
|
-
return self.title or self.__class__.__name__
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class DataSection(DashboardSection):
|
|
84
|
-
"""
|
|
85
|
-
Section that provides data-driven content.
|
|
86
|
-
|
|
87
|
-
Extend this for sections that need to fetch and process data.
|
|
88
|
-
"""
|
|
89
|
-
|
|
90
|
-
def get_data(self) -> Any:
|
|
91
|
-
"""
|
|
92
|
-
Get section data.
|
|
93
|
-
|
|
94
|
-
Override this to provide section-specific data.
|
|
95
|
-
"""
|
|
96
|
-
raise NotImplementedError(
|
|
97
|
-
f"{self.__class__.__name__} must implement get_data()"
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
|
101
|
-
"""Add data to context."""
|
|
102
|
-
context = super().get_context_data(**kwargs)
|
|
103
|
-
context['data'] = self.get_data()
|
|
104
|
-
return context
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class CardSection(DashboardSection):
|
|
108
|
-
"""
|
|
109
|
-
Section rendered as a card component.
|
|
110
|
-
|
|
111
|
-
Automatically wraps content in card template.
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
template_name = "admin/components/card.html"
|
|
115
|
-
content_template: Optional[str] = None
|
|
116
|
-
|
|
117
|
-
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
|
118
|
-
"""Add card-specific context."""
|
|
119
|
-
context = super().get_context_data(**kwargs)
|
|
120
|
-
|
|
121
|
-
# Render content if content_template is provided
|
|
122
|
-
if self.content_template:
|
|
123
|
-
context['content'] = render_to_string(
|
|
124
|
-
self.content_template,
|
|
125
|
-
context=context,
|
|
126
|
-
request=self.request
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
return context
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"""Commands section for dashboard."""
|
|
2
|
-
|
|
3
|
-
from typing import Any, Dict
|
|
4
|
-
|
|
5
|
-
from .base import DataSection
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class CommandsSection(DataSection):
|
|
9
|
-
"""
|
|
10
|
-
Management commands section.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
template_name = "admin/sections/commands_section.html"
|
|
14
|
-
title = "Management Commands"
|
|
15
|
-
icon = "terminal"
|
|
16
|
-
|
|
17
|
-
def get_data(self) -> Dict[str, Any]:
|
|
18
|
-
"""Get commands data."""
|
|
19
|
-
from django_cfg.modules.django_unfold.callbacks.base import (
|
|
20
|
-
get_available_commands,
|
|
21
|
-
get_commands_by_category,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
commands = get_available_commands()
|
|
25
|
-
categorized = get_commands_by_category()
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
'commands': commands,
|
|
29
|
-
'categories': categorized,
|
|
30
|
-
'total_commands': len(commands),
|
|
31
|
-
'core_commands': len([cmd for cmd in commands if cmd.get('is_core')]),
|
|
32
|
-
'custom_commands': len([cmd for cmd in commands if cmd.get('is_custom')]),
|
|
33
|
-
}
|
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
"""Documentation section for dashboard."""
|
|
2
|
-
|
|
3
|
-
import inspect
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any, Dict, List
|
|
7
|
-
|
|
8
|
-
from django.core.management import get_commands, load_command_class
|
|
9
|
-
|
|
10
|
-
from .base import DataSection
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class DocumentationSection(DataSection):
|
|
14
|
-
"""
|
|
15
|
-
Management commands documentation section.
|
|
16
|
-
|
|
17
|
-
Displays the README.md file from django_cfg/management/ directory.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
template_name = "admin/sections/documentation_section.html"
|
|
21
|
-
title = "Commands Documentation"
|
|
22
|
-
icon = "description"
|
|
23
|
-
|
|
24
|
-
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
|
25
|
-
"""Add documentation data directly to context (not nested under 'data')."""
|
|
26
|
-
# Call parent but skip DataSection's get_data() nesting
|
|
27
|
-
context = super(DataSection, self).get_context_data(**kwargs)
|
|
28
|
-
# Add our data directly to context
|
|
29
|
-
context.update(self.get_data())
|
|
30
|
-
return context
|
|
31
|
-
|
|
32
|
-
def find_module_readme(self, module_name: str) -> str:
|
|
33
|
-
"""Find and read README.md from module's management directory."""
|
|
34
|
-
import logging
|
|
35
|
-
logger = logging.getLogger(__name__)
|
|
36
|
-
|
|
37
|
-
try:
|
|
38
|
-
import django_cfg
|
|
39
|
-
django_cfg_path = Path(django_cfg.__file__).parent
|
|
40
|
-
|
|
41
|
-
# Try different paths
|
|
42
|
-
possible_paths = [
|
|
43
|
-
django_cfg_path / 'modules' / module_name / 'management' / 'README.md',
|
|
44
|
-
django_cfg_path / 'management' / 'commands' / module_name / 'README.md',
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
for readme_path in possible_paths:
|
|
48
|
-
if readme_path.exists():
|
|
49
|
-
logger.info(f"Found module README at: {readme_path}")
|
|
50
|
-
with open(readme_path, encoding='utf-8') as f:
|
|
51
|
-
return f.read()
|
|
52
|
-
except Exception as e:
|
|
53
|
-
logger.debug(f"Could not find README for module {module_name}: {e}")
|
|
54
|
-
|
|
55
|
-
return ""
|
|
56
|
-
|
|
57
|
-
def get_commands_structure(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
58
|
-
"""Get structured command data grouped by app."""
|
|
59
|
-
import logging
|
|
60
|
-
logger = logging.getLogger(__name__)
|
|
61
|
-
|
|
62
|
-
# Get all management commands
|
|
63
|
-
commands = get_commands()
|
|
64
|
-
|
|
65
|
-
# Filter only django_cfg commands
|
|
66
|
-
django_cfg_commands = {
|
|
67
|
-
name: app for name, app in commands.items()
|
|
68
|
-
if 'django_cfg' in app
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
logger.info(f"Found {len(django_cfg_commands)} django_cfg commands")
|
|
72
|
-
|
|
73
|
-
# Group commands by app
|
|
74
|
-
commands_by_app = {}
|
|
75
|
-
|
|
76
|
-
for cmd_name, app_name in sorted(django_cfg_commands.items()):
|
|
77
|
-
try:
|
|
78
|
-
# Load the command class
|
|
79
|
-
try:
|
|
80
|
-
command = load_command_class(app_name, cmd_name)
|
|
81
|
-
except (ImportError, ModuleNotFoundError) as e:
|
|
82
|
-
logger.debug(f"Skipping command {cmd_name}: module not found - {e}")
|
|
83
|
-
continue
|
|
84
|
-
|
|
85
|
-
# Use app_name as group key
|
|
86
|
-
if app_name not in commands_by_app:
|
|
87
|
-
# Create display name from app_name
|
|
88
|
-
display_name = app_name.replace('django_cfg.modules.', '').replace('django_cfg', 'Django CFG').replace('_', ' ').title()
|
|
89
|
-
|
|
90
|
-
commands_by_app[app_name] = {
|
|
91
|
-
'name': app_name,
|
|
92
|
-
'display_name': display_name,
|
|
93
|
-
'commands': []
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
# Extract command information
|
|
97
|
-
command_info = {
|
|
98
|
-
'name': cmd_name,
|
|
99
|
-
'help': getattr(command, 'help', 'No description available'),
|
|
100
|
-
'docstring': inspect.cleandoc(command.__doc__) if command.__doc__ else '',
|
|
101
|
-
'web_executable': getattr(command, 'web_executable', None),
|
|
102
|
-
'requires_input': getattr(command, 'requires_input', None),
|
|
103
|
-
'is_destructive': getattr(command, 'is_destructive', None),
|
|
104
|
-
'arguments': []
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
# Get arguments
|
|
108
|
-
try:
|
|
109
|
-
parser = command.create_parser('', cmd_name)
|
|
110
|
-
actions = [action for action in parser._actions if action.dest != 'help']
|
|
111
|
-
|
|
112
|
-
for action in actions:
|
|
113
|
-
if action.option_strings:
|
|
114
|
-
command_info['arguments'].append({
|
|
115
|
-
'options': action.option_strings,
|
|
116
|
-
'help': action.help or 'No description',
|
|
117
|
-
'default': action.default if action.default != '==SUPPRESS==' else None,
|
|
118
|
-
'required': getattr(action, 'required', False),
|
|
119
|
-
})
|
|
120
|
-
except Exception as e:
|
|
121
|
-
logger.debug(f"Could not extract arguments for {cmd_name}: {e}")
|
|
122
|
-
|
|
123
|
-
commands_by_app[app_name]['commands'].append(command_info)
|
|
124
|
-
|
|
125
|
-
except Exception as e:
|
|
126
|
-
logger.error(f"Error loading command {cmd_name}: {e}")
|
|
127
|
-
continue
|
|
128
|
-
|
|
129
|
-
return commands_by_app
|
|
130
|
-
|
|
131
|
-
def generate_documentation_from_commands(self) -> str:
|
|
132
|
-
"""Generate markdown documentation from command docstrings and module READMEs."""
|
|
133
|
-
import logging
|
|
134
|
-
logger = logging.getLogger(__name__)
|
|
135
|
-
|
|
136
|
-
markdown_lines = []
|
|
137
|
-
markdown_lines.append("# Django-CFG Management Commands\n\n")
|
|
138
|
-
|
|
139
|
-
# Check for main README.md
|
|
140
|
-
try:
|
|
141
|
-
import django_cfg
|
|
142
|
-
django_cfg_path = Path(django_cfg.__file__).parent
|
|
143
|
-
main_readme_path = django_cfg_path / 'management' / 'README.md'
|
|
144
|
-
|
|
145
|
-
if main_readme_path.exists():
|
|
146
|
-
logger.info("Found main management README.md, using it as primary documentation")
|
|
147
|
-
with open(main_readme_path, encoding='utf-8') as f:
|
|
148
|
-
return f.read() # Use existing README if it exists
|
|
149
|
-
except Exception as e:
|
|
150
|
-
logger.debug(f"No main README found, continuing with auto-generation: {e}")
|
|
151
|
-
|
|
152
|
-
markdown_lines.append("_Auto-generated documentation from command docstrings and module READMEs._\n\n")
|
|
153
|
-
|
|
154
|
-
# Get all management commands
|
|
155
|
-
commands = get_commands()
|
|
156
|
-
|
|
157
|
-
# Filter only django_cfg commands
|
|
158
|
-
django_cfg_commands = {
|
|
159
|
-
name: app for name, app in commands.items()
|
|
160
|
-
if 'django_cfg' in app
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
logger.info(f"Found {len(django_cfg_commands)} django_cfg commands")
|
|
164
|
-
|
|
165
|
-
# Group commands by module
|
|
166
|
-
commands_by_module = {}
|
|
167
|
-
module_paths = {} # Store module paths for README lookup
|
|
168
|
-
|
|
169
|
-
for cmd_name, app_name in sorted(django_cfg_commands.items()):
|
|
170
|
-
try:
|
|
171
|
-
# Load the command class
|
|
172
|
-
command = load_command_class(app_name, cmd_name)
|
|
173
|
-
|
|
174
|
-
# Get module path
|
|
175
|
-
module_path = command.__module__
|
|
176
|
-
if 'modules' in module_path:
|
|
177
|
-
# Extract module name like 'django_ngrok', 'django_tasks', etc.
|
|
178
|
-
parts = module_path.split('.')
|
|
179
|
-
if 'modules' in parts:
|
|
180
|
-
module_idx = parts.index('modules')
|
|
181
|
-
if len(parts) > module_idx + 1:
|
|
182
|
-
module_name = parts[module_idx + 1]
|
|
183
|
-
else:
|
|
184
|
-
module_name = 'core'
|
|
185
|
-
else:
|
|
186
|
-
module_name = 'core'
|
|
187
|
-
else:
|
|
188
|
-
module_name = 'core'
|
|
189
|
-
|
|
190
|
-
if module_name not in commands_by_module:
|
|
191
|
-
commands_by_module[module_name] = []
|
|
192
|
-
module_paths[module_name] = module_path
|
|
193
|
-
|
|
194
|
-
commands_by_module[module_name].append((cmd_name, command))
|
|
195
|
-
|
|
196
|
-
except Exception as e:
|
|
197
|
-
logger.error(f"Error loading command {cmd_name}: {e}")
|
|
198
|
-
continue
|
|
199
|
-
|
|
200
|
-
# Generate documentation for each module
|
|
201
|
-
for module_name in sorted(commands_by_module.keys()):
|
|
202
|
-
markdown_lines.append(f"\n## {module_name.replace('_', ' ').title()} Commands\n\n")
|
|
203
|
-
|
|
204
|
-
# Check for module-specific README
|
|
205
|
-
module_readme = self.find_module_readme(module_name)
|
|
206
|
-
if module_readme:
|
|
207
|
-
markdown_lines.append(f"_{module_name} module documentation:_\n\n")
|
|
208
|
-
markdown_lines.append(module_readme)
|
|
209
|
-
markdown_lines.append("\n\n### Available Commands\n\n")
|
|
210
|
-
|
|
211
|
-
for cmd_name, command_class in sorted(commands_by_module[module_name]):
|
|
212
|
-
markdown_lines.append(f"\n#### `{cmd_name}`\n\n")
|
|
213
|
-
|
|
214
|
-
# Get help text
|
|
215
|
-
if hasattr(command_class, 'help'):
|
|
216
|
-
markdown_lines.append(f"{command_class.help}\n\n")
|
|
217
|
-
|
|
218
|
-
# Get class docstring
|
|
219
|
-
if command_class.__doc__:
|
|
220
|
-
doc = inspect.cleandoc(command_class.__doc__)
|
|
221
|
-
markdown_lines.append(f"{doc}\n\n")
|
|
222
|
-
|
|
223
|
-
# Get metadata
|
|
224
|
-
metadata = []
|
|
225
|
-
if hasattr(command_class, 'web_executable'):
|
|
226
|
-
metadata.append(f"**Web Executable**: {'Yes' if command_class.web_executable else 'No'}")
|
|
227
|
-
if hasattr(command_class, 'requires_input'):
|
|
228
|
-
metadata.append(f"**Requires Input**: {'Yes' if command_class.requires_input else 'No'}")
|
|
229
|
-
if hasattr(command_class, 'is_destructive'):
|
|
230
|
-
metadata.append(f"**Destructive**: {'Yes' if command_class.is_destructive else 'No'}")
|
|
231
|
-
|
|
232
|
-
if metadata:
|
|
233
|
-
markdown_lines.append("**Metadata:**\n\n")
|
|
234
|
-
for meta in metadata:
|
|
235
|
-
markdown_lines.append(f"- {meta}\n")
|
|
236
|
-
markdown_lines.append("\n")
|
|
237
|
-
|
|
238
|
-
# Get arguments
|
|
239
|
-
try:
|
|
240
|
-
parser = command_class.create_parser('', cmd_name)
|
|
241
|
-
actions = [action for action in parser._actions if action.dest != 'help']
|
|
242
|
-
|
|
243
|
-
if actions:
|
|
244
|
-
markdown_lines.append("**Arguments:**\n\n")
|
|
245
|
-
for action in actions:
|
|
246
|
-
if action.option_strings:
|
|
247
|
-
opts = ', '.join(f"`{opt}`" for opt in action.option_strings)
|
|
248
|
-
help_text = action.help or 'No description'
|
|
249
|
-
markdown_lines.append(f"- {opts}: {help_text}\n")
|
|
250
|
-
markdown_lines.append("\n")
|
|
251
|
-
except Exception as e:
|
|
252
|
-
logger.debug(f"Could not extract arguments for {cmd_name}: {e}")
|
|
253
|
-
|
|
254
|
-
# Add usage example if available in docstring
|
|
255
|
-
if command_class.__doc__ and '```' in command_class.__doc__:
|
|
256
|
-
markdown_lines.append("**Example:**\n\n")
|
|
257
|
-
|
|
258
|
-
return ''.join(markdown_lines)
|
|
259
|
-
|
|
260
|
-
def get_data(self) -> Dict[str, Any]:
|
|
261
|
-
"""Get structured command documentation data."""
|
|
262
|
-
import logging
|
|
263
|
-
logger = logging.getLogger(__name__)
|
|
264
|
-
|
|
265
|
-
try:
|
|
266
|
-
commands_structure = self.get_commands_structure()
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
'commands_by_module': commands_structure,
|
|
270
|
-
'total_commands': sum(len(app['commands']) for app in commands_structure.values()),
|
|
271
|
-
'total_modules': len(commands_structure),
|
|
272
|
-
}
|
|
273
|
-
except Exception as e:
|
|
274
|
-
logger.error(f"Error generating command structure: {e}", exc_info=True)
|
|
275
|
-
return {
|
|
276
|
-
'commands_by_module': {},
|
|
277
|
-
'total_commands': 0,
|
|
278
|
-
'total_modules': 0,
|
|
279
|
-
'error': str(e)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
def get_data_old(self) -> Dict[str, Any]:
|
|
283
|
-
"""DEPRECATED: Get documentation content from README.md or auto-generate from docstrings."""
|
|
284
|
-
import logging
|
|
285
|
-
logger = logging.getLogger(__name__)
|
|
286
|
-
|
|
287
|
-
# Check if we should use auto-generation
|
|
288
|
-
use_autogen = os.environ.get('DJANGO_CFG_AUTOGEN_DOCS', 'true').lower() == 'true'
|
|
289
|
-
|
|
290
|
-
readme_content = ""
|
|
291
|
-
documentation_html = ""
|
|
292
|
-
readme_path = None
|
|
293
|
-
readme_exists = False
|
|
294
|
-
|
|
295
|
-
if use_autogen:
|
|
296
|
-
# Auto-generate documentation from command docstrings
|
|
297
|
-
logger.info("Auto-generating documentation from command docstrings")
|
|
298
|
-
try:
|
|
299
|
-
readme_content = self.generate_documentation_from_commands()
|
|
300
|
-
|
|
301
|
-
# Convert to HTML
|
|
302
|
-
try:
|
|
303
|
-
import markdown
|
|
304
|
-
documentation_html = markdown.markdown(
|
|
305
|
-
readme_content,
|
|
306
|
-
extensions=[
|
|
307
|
-
'markdown.extensions.fenced_code',
|
|
308
|
-
'markdown.extensions.tables',
|
|
309
|
-
'markdown.extensions.toc',
|
|
310
|
-
'markdown.extensions.nl2br',
|
|
311
|
-
'markdown.extensions.sane_lists',
|
|
312
|
-
]
|
|
313
|
-
)
|
|
314
|
-
logger.info("Auto-generated documentation rendered successfully")
|
|
315
|
-
except ImportError:
|
|
316
|
-
logger.warning("Markdown library not available, will display as plain text")
|
|
317
|
-
|
|
318
|
-
readme_exists = True
|
|
319
|
-
readme_path = "Auto-generated from docstrings"
|
|
320
|
-
|
|
321
|
-
except Exception as e:
|
|
322
|
-
logger.error(f"Error auto-generating documentation: {e}", exc_info=True)
|
|
323
|
-
readme_content = f"Error auto-generating documentation: {str(e)}"
|
|
324
|
-
else:
|
|
325
|
-
# Use static README.md file
|
|
326
|
-
# Try multiple path resolution strategies
|
|
327
|
-
possible_paths = [
|
|
328
|
-
# Strategy 1: Relative to this file
|
|
329
|
-
Path(__file__).parent.parent.parent / 'management' / 'README.md',
|
|
330
|
-
# Strategy 2: Using django_cfg module location
|
|
331
|
-
Path(__file__).parent.parent.parent / 'management' / 'README.md',
|
|
332
|
-
# Strategy 3: Absolute path for django_cfg package
|
|
333
|
-
Path('/Users/markinmatrix/Documents/htdocs/@CARAPIS/encar_parser_new/@projects/django-cfg/projects/django-cfg-dev/src/django_cfg/management/README.md'),
|
|
334
|
-
]
|
|
335
|
-
|
|
336
|
-
# Find the first existing path
|
|
337
|
-
for path in possible_paths:
|
|
338
|
-
if path.exists():
|
|
339
|
-
readme_path = path
|
|
340
|
-
logger.info(f"Found README.md at: {readme_path}")
|
|
341
|
-
break
|
|
342
|
-
|
|
343
|
-
# If still not found, try using django_cfg module import
|
|
344
|
-
if not readme_path or not readme_path.exists():
|
|
345
|
-
try:
|
|
346
|
-
import django_cfg
|
|
347
|
-
django_cfg_path = Path(django_cfg.__file__).parent
|
|
348
|
-
readme_path = django_cfg_path / 'management' / 'README.md'
|
|
349
|
-
logger.info(f"Using django_cfg module path: {readme_path}")
|
|
350
|
-
except Exception as e:
|
|
351
|
-
logger.error(f"Error finding django_cfg module: {e}")
|
|
352
|
-
readme_path = possible_paths[0] # Fallback to first path
|
|
353
|
-
|
|
354
|
-
try:
|
|
355
|
-
if readme_path and readme_path.exists():
|
|
356
|
-
logger.info(f"Reading README from: {readme_path}")
|
|
357
|
-
with open(readme_path, encoding='utf-8') as f:
|
|
358
|
-
readme_content = f.read()
|
|
359
|
-
logger.info(f"README content loaded: {len(readme_content)} characters")
|
|
360
|
-
|
|
361
|
-
# Try to convert markdown to HTML
|
|
362
|
-
try:
|
|
363
|
-
import markdown
|
|
364
|
-
documentation_html = markdown.markdown(
|
|
365
|
-
readme_content,
|
|
366
|
-
extensions=[
|
|
367
|
-
'markdown.extensions.fenced_code',
|
|
368
|
-
'markdown.extensions.tables',
|
|
369
|
-
'markdown.extensions.toc',
|
|
370
|
-
'markdown.extensions.nl2br',
|
|
371
|
-
'markdown.extensions.sane_lists',
|
|
372
|
-
]
|
|
373
|
-
)
|
|
374
|
-
logger.info("Markdown rendered successfully")
|
|
375
|
-
except ImportError:
|
|
376
|
-
logger.warning("Markdown library not available, will display as plain text")
|
|
377
|
-
pass
|
|
378
|
-
|
|
379
|
-
readme_exists = True
|
|
380
|
-
else:
|
|
381
|
-
logger.error(f"README.md not found at: {readme_path}")
|
|
382
|
-
readme_content = "README.md not found. Searched paths:\n" + "\n".join(str(p) for p in possible_paths)
|
|
383
|
-
|
|
384
|
-
except Exception as e:
|
|
385
|
-
logger.error(f"Error loading documentation: {e}", exc_info=True)
|
|
386
|
-
readme_content = f"Error loading documentation: {str(e)}"
|
|
387
|
-
|
|
388
|
-
return {
|
|
389
|
-
'readme_content': readme_content,
|
|
390
|
-
'documentation_content': documentation_html if documentation_html else None,
|
|
391
|
-
'readme_path': str(readme_path) if readme_path else 'Not found',
|
|
392
|
-
'readme_exists': readme_exists,
|
|
393
|
-
}
|