django-cfg 1.4.120__py3-none-any.whl → 1.5.2__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 +8 -4
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
- django_cfg/apps/dashboard/TRANSACTION_FIX.md +73 -0
- django_cfg/apps/dashboard/serializers/__init__.py +0 -12
- django_cfg/apps/dashboard/serializers/activity.py +1 -1
- django_cfg/apps/dashboard/services/__init__.py +0 -2
- django_cfg/apps/dashboard/services/charts_service.py +4 -3
- django_cfg/apps/dashboard/services/statistics_service.py +11 -2
- django_cfg/apps/dashboard/services/system_health_service.py +64 -106
- django_cfg/apps/dashboard/urls.py +0 -2
- django_cfg/apps/dashboard/views/__init__.py +0 -2
- django_cfg/apps/dashboard/views/commands_views.py +3 -6
- django_cfg/apps/dashboard/views/overview_views.py +14 -13
- django_cfg/apps/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/{tasks → grpc}/admin/config.py +32 -41
- django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
- django_cfg/apps/grpc/apps.py +28 -0
- django_cfg/apps/grpc/auth/__init__.py +9 -0
- django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
- django_cfg/apps/grpc/interceptors/__init__.py +19 -0
- django_cfg/apps/grpc/interceptors/errors.py +241 -0
- django_cfg/apps/grpc/interceptors/logging.py +270 -0
- django_cfg/apps/grpc/interceptors/metrics.py +306 -0
- django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
- django_cfg/apps/grpc/management/__init__.py +1 -0
- django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
- django_cfg/apps/grpc/managers/__init__.py +10 -0
- django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
- django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
- django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
- django_cfg/apps/grpc/models/__init__.py +9 -0
- django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
- django_cfg/apps/grpc/serializers/__init__.py +23 -0
- django_cfg/apps/grpc/serializers/health.py +18 -0
- django_cfg/apps/grpc/serializers/requests.py +18 -0
- django_cfg/apps/grpc/serializers/services.py +50 -0
- django_cfg/apps/grpc/serializers/stats.py +22 -0
- django_cfg/apps/grpc/services/__init__.py +16 -0
- django_cfg/apps/grpc/services/base.py +375 -0
- django_cfg/apps/grpc/services/discovery.py +415 -0
- django_cfg/apps/grpc/urls.py +23 -0
- django_cfg/apps/grpc/utils/__init__.py +13 -0
- django_cfg/apps/grpc/utils/proto_gen.py +423 -0
- django_cfg/apps/grpc/views/__init__.py +9 -0
- django_cfg/apps/grpc/views/monitoring.py +497 -0
- django_cfg/apps/knowbase/apps.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +7 -9
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
- django_cfg/apps/payments/admin/balance_admin.py +26 -36
- django_cfg/apps/payments/admin/payment_admin.py +65 -85
- django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
- django_cfg/apps/rq/__init__.py +9 -0
- django_cfg/apps/rq/apps.py +80 -0
- django_cfg/apps/rq/management/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
- django_cfg/apps/rq/management/commands/rqstats.py +33 -0
- django_cfg/apps/rq/management/commands/rqworker.py +31 -0
- django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
- django_cfg/apps/rq/serializers/__init__.py +40 -0
- django_cfg/apps/rq/serializers/health.py +60 -0
- django_cfg/apps/rq/serializers/job.py +100 -0
- django_cfg/apps/rq/serializers/queue.py +80 -0
- django_cfg/apps/rq/serializers/schedule.py +178 -0
- django_cfg/apps/rq/serializers/testing.py +139 -0
- django_cfg/apps/rq/serializers/worker.py +58 -0
- django_cfg/apps/rq/services/__init__.py +25 -0
- django_cfg/apps/rq/services/config_helper.py +233 -0
- django_cfg/apps/rq/services/models/README.md +417 -0
- django_cfg/apps/rq/services/models/__init__.py +30 -0
- django_cfg/apps/rq/services/models/event.py +123 -0
- django_cfg/apps/rq/services/models/job.py +99 -0
- django_cfg/apps/rq/services/models/queue.py +92 -0
- django_cfg/apps/rq/services/models/worker.py +104 -0
- django_cfg/apps/rq/services/rq_converters.py +183 -0
- django_cfg/apps/rq/tasks/__init__.py +23 -0
- django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
- django_cfg/apps/rq/urls.py +54 -0
- django_cfg/apps/rq/views/__init__.py +19 -0
- django_cfg/apps/rq/views/jobs.py +882 -0
- django_cfg/apps/rq/views/monitoring.py +248 -0
- django_cfg/apps/rq/views/queues.py +261 -0
- django_cfg/apps/rq/views/schedule.py +400 -0
- django_cfg/apps/rq/views/testing.py +761 -0
- django_cfg/apps/rq/views/workers.py +195 -0
- django_cfg/apps/urls.py +13 -8
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +16 -26
- django_cfg/core/builders/apps_builder.py +7 -11
- django_cfg/core/generation/integration_generators/__init__.py +3 -6
- django_cfg/core/generation/integration_generators/django_rq.py +80 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +15 -15
- django_cfg/core/integration/display/startup.py +6 -20
- django_cfg/mixins/__init__.py +2 -0
- django_cfg/mixins/superadmin_api.py +59 -0
- django_cfg/models/__init__.py +3 -3
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/models/django/__init__.py +3 -3
- django_cfg/models/django/django_rq.py +621 -0
- django_cfg/models/django/revolution_legacy.py +1 -1
- django_cfg/modules/base.py +19 -6
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- django_cfg/modules/django_admin/config/background_task_config.py +4 -4
- django_cfg/modules/django_admin/utils/__init__.py +41 -3
- django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
- django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
- django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
- django_cfg/modules/django_admin/utils/html/badges.py +47 -0
- django_cfg/modules/django_admin/utils/html/base.py +167 -0
- django_cfg/modules/django_admin/utils/html/code.py +87 -0
- django_cfg/modules/django_admin/utils/html/composition.py +205 -0
- django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
- django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
- django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
- django_cfg/modules/django_admin/utils/html/progress.py +127 -0
- django_cfg/modules/django_admin/utils/html_builder.py +55 -408
- django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
- django_cfg/modules/django_unfold/navigation.py +21 -18
- django_cfg/pyproject.toml +4 -6
- django_cfg/registry/core.py +4 -7
- django_cfg/registry/modules.py +6 -0
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/constance/includes/results_list.html +73 -0
- django_cfg/templates/admin/index.html +187 -62
- django_cfg/templatetags/django_cfg.py +61 -1
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/METADATA +12 -4
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/RECORD +140 -96
- django_cfg/apps/dashboard/permissions.py +0 -48
- django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
- django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
- django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
- django_cfg/apps/tasks/__init__.py +0 -64
- django_cfg/apps/tasks/admin/__init__.py +0 -4
- django_cfg/apps/tasks/admin/task_log.py +0 -265
- django_cfg/apps/tasks/apps.py +0 -15
- django_cfg/apps/tasks/filters/__init__.py +0 -10
- django_cfg/apps/tasks/filters/task_log.py +0 -121
- django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
- django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
- django_cfg/apps/tasks/models/__init__.py +0 -4
- django_cfg/apps/tasks/models/task_log.py +0 -246
- django_cfg/apps/tasks/serializers/__init__.py +0 -28
- django_cfg/apps/tasks/serializers/task_log.py +0 -249
- django_cfg/apps/tasks/services/__init__.py +0 -10
- django_cfg/apps/tasks/services/client/__init__.py +0 -7
- django_cfg/apps/tasks/services/client/client.py +0 -234
- django_cfg/apps/tasks/services/config_helper.py +0 -63
- django_cfg/apps/tasks/services/sync.py +0 -204
- django_cfg/apps/tasks/urls.py +0 -16
- django_cfg/apps/tasks/views/__init__.py +0 -10
- django_cfg/apps/tasks/views/task_log.py +0 -41
- django_cfg/apps/tasks/views/task_log_base.py +0 -41
- django_cfg/apps/tasks/views/task_log_overview.py +0 -100
- django_cfg/apps/tasks/views/task_log_related.py +0 -41
- django_cfg/apps/tasks/views/task_log_stats.py +0 -91
- django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
- django_cfg/core/generation/integration_generators/django_q2.py +0 -133
- django_cfg/core/generation/integration_generators/tasks.py +0 -88
- django_cfg/models/django/django_q2.py +0 -514
- django_cfg/models/tasks/__init__.py +0 -49
- django_cfg/models/tasks/backends.py +0 -122
- django_cfg/models/tasks/config.py +0 -209
- django_cfg/models/tasks/utils.py +0 -162
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- django_cfg/modules/django_q2/README.md +0 -140
- django_cfg/modules/django_q2/__init__.py +0 -8
- django_cfg/modules/django_q2/apps.py +0 -107
- django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
- django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
- /django_cfg/apps/{tasks/migrations → grpc/management/commands}/__init__.py +0 -0
- /django_cfg/{modules/django_q2/management → apps/grpc/migrations}/__init__.py +0 -0
- /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
- /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Formatting elements for Django Admin.
|
|
3
|
+
|
|
4
|
+
Provides number and UUID formatting utilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from django.utils.html import escape, format_html
|
|
10
|
+
from django.utils.safestring import SafeString
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FormattingElements:
|
|
14
|
+
"""Formatting utilities for numbers and UUIDs."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def empty(text: str = "—") -> SafeString:
|
|
18
|
+
"""Helper for empty values."""
|
|
19
|
+
from .base import BaseElements
|
|
20
|
+
return BaseElements.empty(text)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def number(
|
|
24
|
+
value: Any,
|
|
25
|
+
precision: int = 8,
|
|
26
|
+
thousands_separator: bool = True,
|
|
27
|
+
strip_zeros: bool = True,
|
|
28
|
+
min_threshold: Optional[float] = None,
|
|
29
|
+
compact: bool = False,
|
|
30
|
+
prefix: str = "",
|
|
31
|
+
suffix: str = "",
|
|
32
|
+
css_class: str = ""
|
|
33
|
+
) -> SafeString:
|
|
34
|
+
"""
|
|
35
|
+
Format numeric values with smart precision handling.
|
|
36
|
+
|
|
37
|
+
Handles:
|
|
38
|
+
- Trailing zeros removal (20.000000 → 20)
|
|
39
|
+
- Scientific notation (0E-18 → 0)
|
|
40
|
+
- Thousands separators (1000000 → 1,000,000)
|
|
41
|
+
- Very small numbers (0.0000001 → < 0.00000001)
|
|
42
|
+
- Compact notation with K/M/B/T suffixes (1500000 → 1.5M)
|
|
43
|
+
- Negative values
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
value: Numeric value (int, float, Decimal, str)
|
|
47
|
+
precision: Number of decimal places (default: 8)
|
|
48
|
+
thousands_separator: Add thousands separator (default: True)
|
|
49
|
+
strip_zeros: Remove trailing zeros (default: True)
|
|
50
|
+
min_threshold: Show "< threshold" for very small numbers
|
|
51
|
+
compact: Use compact notation with K/M/B/T suffixes (default: False)
|
|
52
|
+
prefix: Text before number (e.g., "$", "BTC ")
|
|
53
|
+
suffix: Text after number (e.g., " USD", "%")
|
|
54
|
+
css_class: Additional CSS classes
|
|
55
|
+
|
|
56
|
+
Usage:
|
|
57
|
+
# Crypto balance
|
|
58
|
+
html.number(0.00012345, precision=8) # "0.00012345"
|
|
59
|
+
html.number(20.000000, precision=8) # "20"
|
|
60
|
+
html.number(1000000.5) # "1,000,000.5"
|
|
61
|
+
|
|
62
|
+
# Currency
|
|
63
|
+
html.number(1234.56, precision=2, prefix="$") # "$1,234.56"
|
|
64
|
+
|
|
65
|
+
# Compact notation
|
|
66
|
+
html.number(1500, compact=True, prefix="$") # "$1.5K"
|
|
67
|
+
html.number(2500000, compact=True, prefix="$") # "$2.5M"
|
|
68
|
+
html.number(3500000000, compact=True, prefix="$") # "$3.5B"
|
|
69
|
+
html.number(1200000000000, compact=True, prefix="$") # "$1.2T"
|
|
70
|
+
|
|
71
|
+
# Very small numbers
|
|
72
|
+
html.number(0.0000001, precision=8, min_threshold=1e-8) # "< 0.00000001"
|
|
73
|
+
|
|
74
|
+
# Scientific notation handling
|
|
75
|
+
html.number("0E-18", precision=8) # "0"
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
SafeString with formatted number
|
|
79
|
+
"""
|
|
80
|
+
from decimal import Decimal, InvalidOperation
|
|
81
|
+
|
|
82
|
+
# Handle None/empty
|
|
83
|
+
if value is None or value == "":
|
|
84
|
+
return FormattingElements.empty("—")
|
|
85
|
+
|
|
86
|
+
# Convert to Decimal for precise calculations
|
|
87
|
+
try:
|
|
88
|
+
if isinstance(value, str):
|
|
89
|
+
# Handle scientific notation in strings
|
|
90
|
+
decimal_value = Decimal(value)
|
|
91
|
+
elif isinstance(value, (int, float)):
|
|
92
|
+
decimal_value = Decimal(str(value))
|
|
93
|
+
elif isinstance(value, Decimal):
|
|
94
|
+
decimal_value = value
|
|
95
|
+
else:
|
|
96
|
+
return FormattingElements.empty(str(value))
|
|
97
|
+
except (InvalidOperation, ValueError):
|
|
98
|
+
return FormattingElements.empty(str(value))
|
|
99
|
+
|
|
100
|
+
# Check if value is effectively zero (scientific notation like 0E-18)
|
|
101
|
+
if decimal_value == 0:
|
|
102
|
+
formatted = "0"
|
|
103
|
+
else:
|
|
104
|
+
# Check min_threshold for very small positive numbers
|
|
105
|
+
if min_threshold and 0 < abs(decimal_value) < min_threshold:
|
|
106
|
+
threshold_str = f"{min_threshold:.{precision}f}"
|
|
107
|
+
if strip_zeros:
|
|
108
|
+
threshold_str = threshold_str.rstrip('0').rstrip('.')
|
|
109
|
+
return format_html(
|
|
110
|
+
'<span class="{}">{}< {}{}</span>',
|
|
111
|
+
css_class,
|
|
112
|
+
escape(prefix),
|
|
113
|
+
threshold_str,
|
|
114
|
+
escape(suffix)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Compact notation with K/M/B/T suffixes
|
|
118
|
+
if compact:
|
|
119
|
+
abs_value = abs(float(decimal_value))
|
|
120
|
+
is_negative = decimal_value < 0
|
|
121
|
+
compact_suffix = ""
|
|
122
|
+
|
|
123
|
+
# Determine divisor and suffix
|
|
124
|
+
if abs_value >= 1_000_000_000_000: # Trillion
|
|
125
|
+
divided_value = abs_value / 1_000_000_000_000
|
|
126
|
+
compact_suffix = "T"
|
|
127
|
+
elif abs_value >= 1_000_000_000: # Billion
|
|
128
|
+
divided_value = abs_value / 1_000_000_000
|
|
129
|
+
compact_suffix = "B"
|
|
130
|
+
elif abs_value >= 1_000_000: # Million
|
|
131
|
+
divided_value = abs_value / 1_000_000
|
|
132
|
+
compact_suffix = "M"
|
|
133
|
+
elif abs_value >= 1_000: # Thousand
|
|
134
|
+
divided_value = abs_value / 1_000
|
|
135
|
+
compact_suffix = "K"
|
|
136
|
+
else:
|
|
137
|
+
# Below 1000, use normal formatting
|
|
138
|
+
divided_value = abs_value
|
|
139
|
+
compact_suffix = ""
|
|
140
|
+
|
|
141
|
+
# Format with precision (use 1 decimal for compact)
|
|
142
|
+
compact_precision = 1 if compact_suffix else precision
|
|
143
|
+
formatted = f"{divided_value:.{compact_precision}f}"
|
|
144
|
+
|
|
145
|
+
# Strip trailing zeros if requested
|
|
146
|
+
if strip_zeros:
|
|
147
|
+
formatted = formatted.rstrip('0').rstrip('.')
|
|
148
|
+
|
|
149
|
+
# Add negative sign back
|
|
150
|
+
if is_negative:
|
|
151
|
+
formatted = f"-{formatted}"
|
|
152
|
+
|
|
153
|
+
# Add compact suffix
|
|
154
|
+
formatted += compact_suffix
|
|
155
|
+
else:
|
|
156
|
+
# Format with precision
|
|
157
|
+
formatted = f"{decimal_value:.{precision}f}"
|
|
158
|
+
|
|
159
|
+
# Strip trailing zeros if requested (only for non-compact)
|
|
160
|
+
if strip_zeros:
|
|
161
|
+
formatted = formatted.rstrip('0').rstrip('.')
|
|
162
|
+
|
|
163
|
+
# Add thousands separator (only for non-compact)
|
|
164
|
+
if thousands_separator:
|
|
165
|
+
parts = formatted.split('.')
|
|
166
|
+
integer_part = parts[0]
|
|
167
|
+
decimal_part = parts[1] if len(parts) > 1 else None
|
|
168
|
+
|
|
169
|
+
# Handle negative sign
|
|
170
|
+
is_negative = integer_part.startswith('-')
|
|
171
|
+
if is_negative:
|
|
172
|
+
integer_part = integer_part[1:]
|
|
173
|
+
|
|
174
|
+
# Add commas
|
|
175
|
+
integer_part = f"{int(integer_part):,}"
|
|
176
|
+
|
|
177
|
+
# Restore negative sign
|
|
178
|
+
if is_negative:
|
|
179
|
+
integer_part = f"-{integer_part}"
|
|
180
|
+
|
|
181
|
+
# Rebuild number
|
|
182
|
+
formatted = integer_part
|
|
183
|
+
if decimal_part:
|
|
184
|
+
formatted += f".{decimal_part}"
|
|
185
|
+
|
|
186
|
+
# Add prefix/suffix
|
|
187
|
+
result = f"{prefix}{formatted}{suffix}"
|
|
188
|
+
|
|
189
|
+
# Wrap in span with CSS class if provided
|
|
190
|
+
if css_class:
|
|
191
|
+
return format_html('<span class="{}">{}</span>', css_class, result)
|
|
192
|
+
|
|
193
|
+
return format_html('<span>{}</span>', result)
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def uuid_short(uuid_value: Any, length: int = 6, show_tooltip: bool = True) -> SafeString:
|
|
197
|
+
"""
|
|
198
|
+
Shorten UUID to first N characters with optional tooltip.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
uuid_value: UUID string or UUID object
|
|
202
|
+
length: Number of characters to show (default: 6)
|
|
203
|
+
show_tooltip: Show full UUID on hover (default: True)
|
|
204
|
+
|
|
205
|
+
Usage:
|
|
206
|
+
html.uuid_short(obj.id) # "a1b2c3..."
|
|
207
|
+
html.uuid_short(obj.id, length=8) # "a1b2c3d4..."
|
|
208
|
+
html.uuid_short(obj.id, show_tooltip=False) # Just short version
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
SafeString with shortened UUID
|
|
212
|
+
"""
|
|
213
|
+
uuid_str = str(uuid_value)
|
|
214
|
+
|
|
215
|
+
# Remove dashes for cleaner display
|
|
216
|
+
uuid_clean = uuid_str.replace('-', '')
|
|
217
|
+
|
|
218
|
+
# Take first N characters
|
|
219
|
+
short_uuid = uuid_clean[:length]
|
|
220
|
+
|
|
221
|
+
if show_tooltip:
|
|
222
|
+
return format_html(
|
|
223
|
+
'<code class="font-mono text-xs bg-base-100 dark:bg-base-800 px-1.5 py-0.5 rounded cursor-help" title="{}">{}</code>',
|
|
224
|
+
uuid_str,
|
|
225
|
+
short_uuid
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return format_html(
|
|
229
|
+
'<code class="font-mono text-xs bg-base-100 dark:bg-base-800 px-1.5 py-0.5 rounded">{}</code>',
|
|
230
|
+
short_uuid
|
|
231
|
+
)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Key-value display elements for Django Admin.
|
|
3
|
+
|
|
4
|
+
Provides utilities for displaying key-value pairs, breakdowns, and lists.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, List, Optional
|
|
8
|
+
|
|
9
|
+
from django.utils.html import escape, format_html
|
|
10
|
+
from django.utils.safestring import SafeString, mark_safe
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class KeyValueElements:
|
|
14
|
+
"""Key-value display utilities."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def icon(icon_name: str, size: str = "xs") -> SafeString:
|
|
18
|
+
"""Helper to get icon (internal use)."""
|
|
19
|
+
from .base import BaseElements
|
|
20
|
+
return BaseElements.icon(icon_name, size)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def text(content: Any, variant: Optional[str] = None, size: Optional[str] = None) -> SafeString:
|
|
24
|
+
"""Helper to get styled text (internal use)."""
|
|
25
|
+
from .base import BaseElements
|
|
26
|
+
return BaseElements.text(content, variant=variant, size=size)
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def key_value(
|
|
30
|
+
key: str,
|
|
31
|
+
value: Any,
|
|
32
|
+
icon: Optional[str] = None,
|
|
33
|
+
indent: bool = False,
|
|
34
|
+
divider: bool = False,
|
|
35
|
+
value_variant: Optional[str] = None,
|
|
36
|
+
value_size: Optional[str] = None
|
|
37
|
+
) -> SafeString:
|
|
38
|
+
"""
|
|
39
|
+
Render single key-value pair.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
key: Key text
|
|
43
|
+
value: Value (can be SafeString from other html methods)
|
|
44
|
+
icon: Material icon name
|
|
45
|
+
indent: Indent the item
|
|
46
|
+
divider: Show divider above
|
|
47
|
+
value_variant: Color variant for value ('success', 'warning', etc)
|
|
48
|
+
value_size: Size for value ('sm', 'base', 'lg')
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
html.key_value('Total', '100 BTC')
|
|
52
|
+
html.key_value('Available', '60 BTC', icon=Icons.CHECK_CIRCLE, indent=True)
|
|
53
|
+
html.key_value('Price', '$1,234', divider=True, value_variant='success', value_size='lg')
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
SafeString with key-value HTML
|
|
57
|
+
"""
|
|
58
|
+
# Classes
|
|
59
|
+
classes = ['mb-2']
|
|
60
|
+
if indent:
|
|
61
|
+
classes.append('ml-5')
|
|
62
|
+
if divider:
|
|
63
|
+
classes.append('mt-4 pt-2 border-t border-base-200 dark:border-base-700')
|
|
64
|
+
|
|
65
|
+
# Wrap value if variant or size specified
|
|
66
|
+
if value_variant or value_size:
|
|
67
|
+
value = KeyValueElements.text(value, variant=value_variant, size=value_size)
|
|
68
|
+
|
|
69
|
+
# Build parts
|
|
70
|
+
parts = []
|
|
71
|
+
parts.append(f'<div class="{" ".join(classes)}">')
|
|
72
|
+
|
|
73
|
+
# Icon
|
|
74
|
+
if icon:
|
|
75
|
+
parts.append(str(KeyValueElements.icon(icon, size="xs")))
|
|
76
|
+
parts.append(' ')
|
|
77
|
+
|
|
78
|
+
# Key
|
|
79
|
+
parts.append(f'<span class="font-semibold text-font-default-light dark:text-font-default-dark">{escape(key)}:</span> ')
|
|
80
|
+
|
|
81
|
+
# Value
|
|
82
|
+
parts.append(str(value))
|
|
83
|
+
|
|
84
|
+
parts.append('</div>')
|
|
85
|
+
|
|
86
|
+
return mark_safe(''.join(parts))
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def divider(css_class: str = "my-2") -> SafeString:
|
|
90
|
+
"""
|
|
91
|
+
Render horizontal divider line.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
css_class: CSS classes for the hr element
|
|
95
|
+
|
|
96
|
+
Usage:
|
|
97
|
+
html.breakdown(
|
|
98
|
+
section1,
|
|
99
|
+
html.divider(),
|
|
100
|
+
section2,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
SafeString with hr element
|
|
105
|
+
"""
|
|
106
|
+
return format_html('<hr class="{}">', css_class)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def breakdown(*items: SafeString) -> SafeString:
|
|
110
|
+
"""
|
|
111
|
+
Combine multiple key-value pairs into a breakdown section.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
*items: Variable number of SafeStrings (from html.key_value())
|
|
115
|
+
|
|
116
|
+
Usage:
|
|
117
|
+
html.breakdown(
|
|
118
|
+
html.key_value('Total', total_val),
|
|
119
|
+
html.key_value('Available', avail_val, icon=Icons.CHECK_CIRCLE, indent=True),
|
|
120
|
+
html.key_value('Locked', locked_val, icon=Icons.LOCK, indent=True),
|
|
121
|
+
html.key_value('Price', price, divider=True) if has_price else None,
|
|
122
|
+
html.key_value('Total Value', usd_val, value_variant='success', value_size='lg') if has_price else None,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
SafeString with combined breakdown HTML
|
|
127
|
+
"""
|
|
128
|
+
# Filter out None values
|
|
129
|
+
filtered = [str(item) for item in items if item is not None]
|
|
130
|
+
|
|
131
|
+
return mark_safe(''.join(filtered))
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def key_value_list(
|
|
135
|
+
items: List[dict],
|
|
136
|
+
layout: str = "vertical",
|
|
137
|
+
key_width: Optional[str] = None,
|
|
138
|
+
spacing: str = "normal"
|
|
139
|
+
) -> SafeString:
|
|
140
|
+
"""
|
|
141
|
+
Render key-value pairs as a formatted list.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
items: List of dicts with 'key', 'value', and optional keys:
|
|
145
|
+
- icon: Material icon name
|
|
146
|
+
- indent: Boolean for indentation
|
|
147
|
+
- value_class: Tailwind classes for value
|
|
148
|
+
- divider: Boolean to show divider above
|
|
149
|
+
- size: 'sm', 'base', 'lg'
|
|
150
|
+
layout: "vertical" or "horizontal"
|
|
151
|
+
key_width: Fixed width for keys (e.g., "100px") for alignment
|
|
152
|
+
spacing: "tight", "normal", "relaxed"
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
# Simple key-value list
|
|
156
|
+
html.key_value_list([
|
|
157
|
+
{'key': 'Total', 'value': '100 BTC', 'size': 'lg'},
|
|
158
|
+
{'key': 'Available', 'value': '60 BTC', 'indent': True},
|
|
159
|
+
{'key': 'Locked', 'value': '40 BTC', 'indent': True},
|
|
160
|
+
])
|
|
161
|
+
|
|
162
|
+
# With icons and styling
|
|
163
|
+
html.key_value_list([
|
|
164
|
+
{'key': 'Available', 'value': '60 BTC', 'icon': Icons.CHECK_CIRCLE},
|
|
165
|
+
{'key': 'Total Value', 'value': '$1,234.56', 'value_class': 'text-success-600 text-lg', 'divider': True},
|
|
166
|
+
])
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
SafeString with formatted key-value list HTML
|
|
170
|
+
"""
|
|
171
|
+
spacing_map = {
|
|
172
|
+
'tight': 'mb-1',
|
|
173
|
+
'normal': 'mb-2',
|
|
174
|
+
'relaxed': 'mb-3'
|
|
175
|
+
}
|
|
176
|
+
spacing_class = spacing_map.get(spacing, 'mb-2')
|
|
177
|
+
|
|
178
|
+
parts = []
|
|
179
|
+
for item in items:
|
|
180
|
+
key = item.get('key', '')
|
|
181
|
+
value = item.get('value', '')
|
|
182
|
+
icon = item.get('icon', '')
|
|
183
|
+
indent = item.get('indent', False)
|
|
184
|
+
value_class = item.get('value_class', '')
|
|
185
|
+
divider = item.get('divider', False)
|
|
186
|
+
size = item.get('size', 'base')
|
|
187
|
+
|
|
188
|
+
# Icon HTML
|
|
189
|
+
icon_html = ""
|
|
190
|
+
if icon:
|
|
191
|
+
icon_html = f'{KeyValueElements.icon(icon, size="xs")} '
|
|
192
|
+
|
|
193
|
+
# Size classes
|
|
194
|
+
size_map = {
|
|
195
|
+
'sm': 'text-sm',
|
|
196
|
+
'base': 'text-base',
|
|
197
|
+
'lg': 'text-lg'
|
|
198
|
+
}
|
|
199
|
+
size_class = size_map.get(size, 'text-base')
|
|
200
|
+
|
|
201
|
+
# Classes
|
|
202
|
+
indent_class = 'ml-5' if indent else ''
|
|
203
|
+
divider_class = 'mt-4 pt-2 border-t border-base-200 dark:border-base-700' if divider else ''
|
|
204
|
+
|
|
205
|
+
# Build item HTML
|
|
206
|
+
item_html = format_html(
|
|
207
|
+
'<div class="{} {} {} {}">{}<span class="font-semibold">{}:</span> <span class="{}">{}</span></div>',
|
|
208
|
+
spacing_class,
|
|
209
|
+
indent_class,
|
|
210
|
+
divider_class,
|
|
211
|
+
size_class,
|
|
212
|
+
icon_html,
|
|
213
|
+
escape(key),
|
|
214
|
+
value_class,
|
|
215
|
+
value # Already SafeString from html.number()
|
|
216
|
+
)
|
|
217
|
+
parts.append(item_html)
|
|
218
|
+
|
|
219
|
+
return mark_safe(''.join(str(p) for p in parts))
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markdown integration for HTML builder.
|
|
3
|
+
|
|
4
|
+
Provides thin wrapper methods that delegate to MarkdownRenderer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, Union
|
|
9
|
+
|
|
10
|
+
from django.utils.safestring import SafeString
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MarkdownIntegration:
|
|
14
|
+
"""Markdown rendering integration for HtmlBuilder."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def markdown(
|
|
18
|
+
text: str,
|
|
19
|
+
css_class: str = "",
|
|
20
|
+
max_height: Optional[str] = None,
|
|
21
|
+
enable_plugins: bool = True
|
|
22
|
+
) -> SafeString:
|
|
23
|
+
"""
|
|
24
|
+
Render markdown text to beautifully styled HTML.
|
|
25
|
+
|
|
26
|
+
Delegates to MarkdownRenderer.render_markdown() for actual rendering.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
text: Markdown content
|
|
30
|
+
css_class: Additional CSS classes
|
|
31
|
+
max_height: Max height with scrolling (e.g., "400px", "20rem")
|
|
32
|
+
enable_plugins: Enable mistune plugins (tables, strikethrough, etc.)
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
# Simple markdown rendering
|
|
36
|
+
html.markdown("# Hello\\n\\nThis is **bold** text")
|
|
37
|
+
|
|
38
|
+
# With custom styling
|
|
39
|
+
html.markdown(obj.description, css_class="my-custom-class")
|
|
40
|
+
|
|
41
|
+
# With max height for long content
|
|
42
|
+
html.markdown(obj.documentation, max_height="500px")
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
SafeString with rendered HTML
|
|
46
|
+
"""
|
|
47
|
+
# Import here to avoid circular dependency
|
|
48
|
+
from ..markdown.renderer import MarkdownRenderer
|
|
49
|
+
|
|
50
|
+
return MarkdownRenderer.render_markdown(
|
|
51
|
+
text=text,
|
|
52
|
+
css_class=css_class,
|
|
53
|
+
max_height=max_height,
|
|
54
|
+
enable_plugins=enable_plugins
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def markdown_docs(
|
|
59
|
+
content: Union[str, Path],
|
|
60
|
+
collapsible: bool = True,
|
|
61
|
+
title: str = "Documentation",
|
|
62
|
+
icon: str = "description",
|
|
63
|
+
max_height: Optional[str] = "500px",
|
|
64
|
+
enable_plugins: bool = True,
|
|
65
|
+
default_open: bool = False
|
|
66
|
+
) -> SafeString:
|
|
67
|
+
"""
|
|
68
|
+
Render markdown documentation from string or file with collapsible UI.
|
|
69
|
+
|
|
70
|
+
Auto-detects whether content is a file path or markdown string.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
content: Markdown string or path to .md file
|
|
74
|
+
collapsible: Wrap in collapsible details/summary
|
|
75
|
+
title: Title for collapsible section
|
|
76
|
+
icon: Material icon name for title
|
|
77
|
+
max_height: Max height for scrolling
|
|
78
|
+
enable_plugins: Enable markdown plugins
|
|
79
|
+
default_open: Open by default if collapsible
|
|
80
|
+
|
|
81
|
+
Usage:
|
|
82
|
+
# From string with collapse
|
|
83
|
+
html.markdown_docs(obj.description, title="Description")
|
|
84
|
+
|
|
85
|
+
# From file
|
|
86
|
+
html.markdown_docs("docs/api.md", title="API Documentation")
|
|
87
|
+
|
|
88
|
+
# Simple, no collapse
|
|
89
|
+
html.markdown_docs(obj.notes, collapsible=False)
|
|
90
|
+
|
|
91
|
+
# Open by default
|
|
92
|
+
html.markdown_docs(obj.readme, default_open=True)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Rendered markdown with beautiful Tailwind styling
|
|
96
|
+
"""
|
|
97
|
+
# Import here to avoid circular dependency
|
|
98
|
+
from ..markdown.renderer import MarkdownRenderer
|
|
99
|
+
|
|
100
|
+
return MarkdownRenderer.render(
|
|
101
|
+
content=content,
|
|
102
|
+
collapsible=collapsible,
|
|
103
|
+
title=title,
|
|
104
|
+
icon=icon,
|
|
105
|
+
max_height=max_height,
|
|
106
|
+
enable_plugins=enable_plugins,
|
|
107
|
+
default_open=default_open
|
|
108
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Progress bar elements for Django Admin.
|
|
3
|
+
|
|
4
|
+
Provides progress bar rendering with multiple colored segments.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django.utils.html import escape
|
|
8
|
+
from django.utils.safestring import SafeString, mark_safe
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProgressElements:
|
|
12
|
+
"""Progress bar display elements."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def segment(percentage: float, variant: str = 'primary', label: str = ''):
|
|
16
|
+
"""
|
|
17
|
+
Create progress bar segment with named parameters.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
percentage: Percentage value (0-100)
|
|
21
|
+
variant: Color variant ('success', 'warning', 'danger', 'info', 'primary')
|
|
22
|
+
label: Label text
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
html.segment(percentage=60, variant='success', label='Available')
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
dict with segment data
|
|
29
|
+
"""
|
|
30
|
+
return {'percentage': percentage, 'variant': variant, 'label': label}
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def progress_bar(
|
|
34
|
+
*segments,
|
|
35
|
+
width: str = "w-full max-w-xs",
|
|
36
|
+
height: str = "h-6",
|
|
37
|
+
show_labels: bool = True,
|
|
38
|
+
rounded: bool = True
|
|
39
|
+
) -> SafeString:
|
|
40
|
+
"""
|
|
41
|
+
Render progress bar with multiple colored segments using Tailwind.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
*segments: Variable number of segment dicts (from html.segment())
|
|
45
|
+
width: Tailwind width classes (default: "w-full max-w-xs")
|
|
46
|
+
height: Tailwind height class (default: "h-6" = 24px for visibility)
|
|
47
|
+
show_labels: Show percentage labels below the bar
|
|
48
|
+
rounded: Use rounded corners
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
html.progress_bar(
|
|
52
|
+
html.segment(percentage=60, variant='success', label='Available'),
|
|
53
|
+
html.segment(percentage=40, variant='warning', label='Locked')
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
SafeString with progress bar HTML
|
|
58
|
+
"""
|
|
59
|
+
# Standard Tailwind colors with dark mode (работают всегда!)
|
|
60
|
+
# Progress bars need visible contrast
|
|
61
|
+
variant_bg_map = {
|
|
62
|
+
'success': 'bg-green-600 dark:bg-green-500',
|
|
63
|
+
'warning': 'bg-yellow-600 dark:bg-yellow-500',
|
|
64
|
+
'danger': 'bg-red-600 dark:bg-red-500',
|
|
65
|
+
'info': 'bg-blue-600 dark:bg-blue-500',
|
|
66
|
+
'primary': 'bg-primary-600 dark:bg-primary-500',
|
|
67
|
+
'secondary': 'bg-gray-400 dark:bg-gray-500',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Standard Tailwind text colors with dark mode
|
|
71
|
+
variant_text_map = {
|
|
72
|
+
'success': 'text-green-700 dark:text-green-300',
|
|
73
|
+
'warning': 'text-yellow-700 dark:text-yellow-300',
|
|
74
|
+
'danger': 'text-red-700 dark:text-red-300',
|
|
75
|
+
'info': 'text-blue-700 dark:text-blue-300',
|
|
76
|
+
'primary': 'text-primary-700 dark:text-primary-300',
|
|
77
|
+
'secondary': 'text-gray-600 dark:text-gray-400',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Build segments HTML
|
|
81
|
+
segments_html = []
|
|
82
|
+
for seg in segments:
|
|
83
|
+
pct = seg['percentage']
|
|
84
|
+
variant = seg['variant']
|
|
85
|
+
bg_class = variant_bg_map.get(variant, 'bg-base-200 dark:bg-base-700')
|
|
86
|
+
|
|
87
|
+
if pct > 0:
|
|
88
|
+
segments_html.append(
|
|
89
|
+
f'<div class="{bg_class}" style="width: {pct}%; height: 100%;"></div>'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Build labels HTML
|
|
93
|
+
labels_html = ""
|
|
94
|
+
if show_labels:
|
|
95
|
+
label_parts = []
|
|
96
|
+
for seg in segments:
|
|
97
|
+
pct = seg['percentage']
|
|
98
|
+
variant = seg['variant']
|
|
99
|
+
label = seg['label']
|
|
100
|
+
text_class = variant_text_map.get(variant, 'text-base-600')
|
|
101
|
+
|
|
102
|
+
if pct > 0 or label:
|
|
103
|
+
label_parts.append(
|
|
104
|
+
f'<span class="{text_class}">{escape(label)}: {pct:.1f}%</span>'
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if label_parts:
|
|
108
|
+
labels_html = (
|
|
109
|
+
f'<div class="flex justify-between mt-1 text-xs">'
|
|
110
|
+
f'{"".join(label_parts)}'
|
|
111
|
+
f'</div>'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Rounded class
|
|
115
|
+
rounded_class = 'rounded-lg' if rounded else ''
|
|
116
|
+
|
|
117
|
+
# Combine
|
|
118
|
+
html = (
|
|
119
|
+
f'<div class="{width}">'
|
|
120
|
+
f'<div class="bg-base-100 dark:bg-base-800 {height} {rounded_class} overflow-hidden flex">'
|
|
121
|
+
f'{"".join(segments_html)}'
|
|
122
|
+
f'</div>'
|
|
123
|
+
f'{labels_html}'
|
|
124
|
+
f'</div>'
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return mark_safe(html)
|