django-cfg 1.4.120__py3-none-any.whl → 1.5.1__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/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/grpc/admin/config.py +89 -0
- 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/__init__.py +0 -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/migrations/__init__.py +0 -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/maintenance/admin/api_key_admin.py +7 -8
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- 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/tasks/admin/task_log.py +20 -47
- django_cfg/apps/urls.py +7 -1
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +6 -0
- django_cfg/core/builders/apps_builder.py +3 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +10 -0
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/modules/base.py +15 -0
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- 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 +198 -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 +28 -0
- django_cfg/pyproject.toml +3 -5
- django_cfg/registry/modules.py +6 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/METADATA +10 -1
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/RECORD +79 -30
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- /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.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic HTML elements for Django Admin.
|
|
3
|
+
|
|
4
|
+
Provides fundamental HTML building blocks: icons, spans, text, divs, links, and empty placeholders.
|
|
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 BaseElements:
|
|
14
|
+
"""Basic HTML building blocks."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def icon(icon_name: str, size: str = "xs", css_class: str = "") -> SafeString:
|
|
18
|
+
"""
|
|
19
|
+
Render Material Icon.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
icon_name: Icon name from Icons class
|
|
23
|
+
size: xs, sm, base, lg, xl
|
|
24
|
+
css_class: Additional CSS classes
|
|
25
|
+
"""
|
|
26
|
+
size_classes = {
|
|
27
|
+
'xs': 'text-xs',
|
|
28
|
+
'sm': 'text-sm',
|
|
29
|
+
'base': 'text-base',
|
|
30
|
+
'lg': 'text-lg',
|
|
31
|
+
'xl': 'text-xl'
|
|
32
|
+
}
|
|
33
|
+
size_class = size_classes.get(size, 'text-xs')
|
|
34
|
+
classes = f"material-symbols-outlined {size_class}"
|
|
35
|
+
if css_class:
|
|
36
|
+
classes += f" {css_class}"
|
|
37
|
+
|
|
38
|
+
return format_html('<span class="{}">{}</span>', classes, icon_name)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def span(text: Any, css_class: str = "") -> SafeString:
|
|
42
|
+
"""
|
|
43
|
+
Render text in span with optional CSS class.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
text: Text to display
|
|
47
|
+
css_class: CSS classes
|
|
48
|
+
"""
|
|
49
|
+
if css_class:
|
|
50
|
+
return format_html('<span class="{}">{}</span>', css_class, escape(str(text)))
|
|
51
|
+
return format_html('<span>{}</span>', escape(str(text)))
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def text(
|
|
55
|
+
content: Any,
|
|
56
|
+
variant: Optional[str] = None,
|
|
57
|
+
size: Optional[str] = None,
|
|
58
|
+
weight: Optional[str] = None,
|
|
59
|
+
muted: bool = False
|
|
60
|
+
) -> SafeString:
|
|
61
|
+
"""
|
|
62
|
+
Render styled text with semantic variants.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
content: Text content (can be SafeString from other html methods)
|
|
66
|
+
variant: Color variant - 'success', 'warning', 'danger', 'info', 'primary'
|
|
67
|
+
size: Size - 'xs', 'sm', 'base', 'lg', 'xl', '2xl'
|
|
68
|
+
weight: Font weight - 'normal', 'medium', 'semibold', 'bold'
|
|
69
|
+
muted: Use muted/subtle color
|
|
70
|
+
|
|
71
|
+
Usage:
|
|
72
|
+
# Success text
|
|
73
|
+
html.text("$1,234.56", variant="success", size="lg")
|
|
74
|
+
|
|
75
|
+
# Muted small text
|
|
76
|
+
html.text("(12.5%)", muted=True, size="sm")
|
|
77
|
+
|
|
78
|
+
# Combined with other methods
|
|
79
|
+
total = html.number(1234.56, prefix="$")
|
|
80
|
+
html.text(total, variant="success", size="lg")
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
SafeString with styled text
|
|
84
|
+
"""
|
|
85
|
+
classes = []
|
|
86
|
+
|
|
87
|
+
# Variant colors
|
|
88
|
+
if variant:
|
|
89
|
+
variant_classes = {
|
|
90
|
+
'success': 'text-success-600 dark:text-success-400',
|
|
91
|
+
'warning': 'text-warning-600 dark:text-warning-400',
|
|
92
|
+
'danger': 'text-danger-600 dark:text-danger-400',
|
|
93
|
+
'info': 'text-info-600 dark:text-info-400',
|
|
94
|
+
'primary': 'text-primary-600 dark:text-primary-400',
|
|
95
|
+
}
|
|
96
|
+
classes.append(variant_classes.get(variant, ''))
|
|
97
|
+
|
|
98
|
+
# Muted
|
|
99
|
+
if muted:
|
|
100
|
+
classes.append('text-font-subtle-light dark:text-font-subtle-dark')
|
|
101
|
+
|
|
102
|
+
# Size
|
|
103
|
+
if size:
|
|
104
|
+
size_classes = {
|
|
105
|
+
'xs': 'text-xs',
|
|
106
|
+
'sm': 'text-sm',
|
|
107
|
+
'base': 'text-base',
|
|
108
|
+
'lg': 'text-lg',
|
|
109
|
+
'xl': 'text-xl',
|
|
110
|
+
'2xl': 'text-2xl',
|
|
111
|
+
}
|
|
112
|
+
classes.append(size_classes.get(size, ''))
|
|
113
|
+
|
|
114
|
+
# Weight
|
|
115
|
+
if weight:
|
|
116
|
+
weight_classes = {
|
|
117
|
+
'normal': 'font-normal',
|
|
118
|
+
'medium': 'font-medium',
|
|
119
|
+
'semibold': 'font-semibold',
|
|
120
|
+
'bold': 'font-bold',
|
|
121
|
+
}
|
|
122
|
+
classes.append(weight_classes.get(weight, ''))
|
|
123
|
+
|
|
124
|
+
css_class = ' '.join(filter(None, classes))
|
|
125
|
+
|
|
126
|
+
if css_class:
|
|
127
|
+
return format_html('<span class="{}">{}</span>', css_class, content)
|
|
128
|
+
return format_html('<span>{}</span>', content)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def div(content: Any, css_class: str = "") -> SafeString:
|
|
132
|
+
"""
|
|
133
|
+
Render content in div with optional CSS class.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
content: Content to display (can be SafeString)
|
|
137
|
+
css_class: CSS classes
|
|
138
|
+
"""
|
|
139
|
+
if css_class:
|
|
140
|
+
return format_html('<div class="{}">{}</div>', css_class, content)
|
|
141
|
+
return format_html('<div>{}</div>', content)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def link(url: str, text: str, css_class: str = "", target: str = "") -> SafeString:
|
|
145
|
+
"""
|
|
146
|
+
Render link.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
url: URL
|
|
150
|
+
text: Link text
|
|
151
|
+
css_class: CSS classes
|
|
152
|
+
target: Target attribute (_blank, _self, etc)
|
|
153
|
+
"""
|
|
154
|
+
if target:
|
|
155
|
+
return format_html(
|
|
156
|
+
'<a href="{}" class="{}" target="{}">{}</a>',
|
|
157
|
+
url, css_class, target, escape(text)
|
|
158
|
+
)
|
|
159
|
+
return format_html('<a href="{}" class="{}">{}</a>', url, css_class, escape(text))
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def empty(text: str = "—") -> SafeString:
|
|
163
|
+
"""Render empty/placeholder value."""
|
|
164
|
+
return format_html(
|
|
165
|
+
'<span class="text-font-subtle-light dark:text-font-subtle-dark">{}</span>',
|
|
166
|
+
escape(text)
|
|
167
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code display elements for Django Admin.
|
|
3
|
+
|
|
4
|
+
Provides inline code and code block rendering with syntax highlighting support.
|
|
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 CodeElements:
|
|
14
|
+
"""Code display elements."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def code(text: Any, css_class: str = "") -> SafeString:
|
|
18
|
+
"""
|
|
19
|
+
Render inline code.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
text: Code text
|
|
23
|
+
css_class: Additional CSS classes
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
html.code("/path/to/file")
|
|
27
|
+
html.code("command --arg value")
|
|
28
|
+
"""
|
|
29
|
+
base_classes = "font-mono text-xs bg-base-100 dark:bg-base-800 px-1.5 py-0.5 rounded"
|
|
30
|
+
classes = f"{base_classes} {css_class}".strip()
|
|
31
|
+
|
|
32
|
+
return format_html(
|
|
33
|
+
'<code class="{}">{}</code>',
|
|
34
|
+
classes,
|
|
35
|
+
escape(str(text))
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def code_block(
|
|
40
|
+
text: Any,
|
|
41
|
+
language: Optional[str] = None,
|
|
42
|
+
max_height: Optional[str] = None,
|
|
43
|
+
variant: str = "default"
|
|
44
|
+
) -> SafeString:
|
|
45
|
+
"""
|
|
46
|
+
Render code block with optional syntax highlighting and scrolling.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
text: Code content
|
|
50
|
+
language: Programming language (json, python, bash, etc.) - for future syntax highlighting
|
|
51
|
+
max_height: Max height with scrolling (e.g., "400px", "20rem")
|
|
52
|
+
variant: Color variant - default, warning, danger, success, info
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
html.code_block(json.dumps(data, indent=2), language="json")
|
|
56
|
+
html.code_block(stdout, max_height="400px")
|
|
57
|
+
html.code_block(stderr, max_height="400px", variant="warning")
|
|
58
|
+
"""
|
|
59
|
+
# Variant-specific styles
|
|
60
|
+
variant_classes = {
|
|
61
|
+
'default': 'bg-base-50 dark:bg-base-900 border-base-200 dark:border-base-700',
|
|
62
|
+
'warning': 'bg-warning-50 dark:bg-warning-900/20 border-warning-200 dark:border-warning-700',
|
|
63
|
+
'danger': 'bg-danger-50 dark:bg-danger-900/20 border-danger-200 dark:border-danger-700',
|
|
64
|
+
'success': 'bg-success-50 dark:bg-success-900/20 border-success-200 dark:border-success-700',
|
|
65
|
+
'info': 'bg-info-50 dark:bg-info-900/20 border-info-200 dark:border-info-700',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
variant_class = variant_classes.get(variant, variant_classes['default'])
|
|
69
|
+
|
|
70
|
+
# Base styles
|
|
71
|
+
base_classes = f"font-mono text-xs whitespace-pre-wrap break-words border rounded-md p-3 {variant_class}"
|
|
72
|
+
|
|
73
|
+
# Add max-height and overflow if specified
|
|
74
|
+
style = ""
|
|
75
|
+
if max_height:
|
|
76
|
+
style = f'style="max-height: {max_height}; overflow-y: auto;"'
|
|
77
|
+
|
|
78
|
+
# Add language class for potential syntax highlighting
|
|
79
|
+
lang_class = f"language-{language}" if language else ""
|
|
80
|
+
|
|
81
|
+
return format_html(
|
|
82
|
+
'<pre class="{} {}" {}><code>{}</code></pre>',
|
|
83
|
+
base_classes,
|
|
84
|
+
lang_class,
|
|
85
|
+
style,
|
|
86
|
+
escape(str(text))
|
|
87
|
+
)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Composition elements for Django Admin.
|
|
3
|
+
|
|
4
|
+
Provides methods for composing multiple elements together: inline, icon_text, header.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional, Union
|
|
8
|
+
|
|
9
|
+
from django.utils.html import escape, format_html
|
|
10
|
+
from django.utils.safestring import SafeString
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CompositionElements:
|
|
14
|
+
"""Element composition utilities."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def icon(icon_name: str, size: str = "xs", css_class: str = "") -> SafeString:
|
|
18
|
+
"""
|
|
19
|
+
Render Material Icon (helper for internal use).
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
icon_name: Icon name
|
|
23
|
+
size: Icon size
|
|
24
|
+
css_class: Additional CSS classes
|
|
25
|
+
"""
|
|
26
|
+
from .base import BaseElements
|
|
27
|
+
return BaseElements.icon(icon_name, size, css_class)
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def icon_text(icon_or_text: Union[str, Any], text: Any = None,
|
|
31
|
+
icon_size: str = "xs", separator: str = " ") -> SafeString:
|
|
32
|
+
"""
|
|
33
|
+
Render icon with text or emoji with text.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
icon_or_text: Icon from Icons class, emoji, or text if text param is None
|
|
37
|
+
text: Optional text to display after icon
|
|
38
|
+
icon_size: Icon size (xs, sm, base, lg, xl)
|
|
39
|
+
separator: Separator between icon and text
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
html.icon_text(Icons.EDIT, 5) # Icon with number
|
|
43
|
+
html.icon_text("📝", 5) # Emoji with number
|
|
44
|
+
html.icon_text("Active") # Just text
|
|
45
|
+
"""
|
|
46
|
+
if text is None:
|
|
47
|
+
# Just text
|
|
48
|
+
return format_html('<span>{}</span>', escape(str(icon_or_text)))
|
|
49
|
+
|
|
50
|
+
# Check if it's a Material Icon (from Icons class) or emoji
|
|
51
|
+
icon_str = str(icon_or_text)
|
|
52
|
+
|
|
53
|
+
# Detect if it's emoji by checking for non-ASCII characters
|
|
54
|
+
is_emoji = any(ord(c) > 127 for c in icon_str)
|
|
55
|
+
|
|
56
|
+
if is_emoji or icon_str in ['📝', '💬', '🛒', '👤', '📧', '🔔', '⚙️', '🔧', '📊', '🎯']:
|
|
57
|
+
# Emoji
|
|
58
|
+
icon_html = escape(icon_str)
|
|
59
|
+
else:
|
|
60
|
+
# Material Icon
|
|
61
|
+
icon_html = CompositionElements.icon(icon_str, size=icon_size)
|
|
62
|
+
|
|
63
|
+
return format_html('{}{}<span>{}</span>', icon_html, separator, escape(str(text)))
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def inline(*items, separator: str = " | ",
|
|
67
|
+
size: str = "small", css_class: str = "") -> SafeString:
|
|
68
|
+
"""
|
|
69
|
+
Render items inline with separator.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
*items: Variable number of SafeString/str items to join (filters out None values)
|
|
73
|
+
separator: Separator between items
|
|
74
|
+
size: small, medium, large
|
|
75
|
+
css_class: Additional CSS classes
|
|
76
|
+
|
|
77
|
+
Usage:
|
|
78
|
+
html.inline(
|
|
79
|
+
html.icon_text(Icons.EDIT, 5),
|
|
80
|
+
html.icon_text(Icons.CHAT, 10),
|
|
81
|
+
separator=" | "
|
|
82
|
+
)
|
|
83
|
+
"""
|
|
84
|
+
# Filter out None values
|
|
85
|
+
filtered_items = [item for item in items if item is not None]
|
|
86
|
+
|
|
87
|
+
if not filtered_items:
|
|
88
|
+
return format_html('<span class="text-font-subtle-light dark:text-font-subtle-dark">—</span>')
|
|
89
|
+
|
|
90
|
+
size_classes = {
|
|
91
|
+
'small': 'text-xs',
|
|
92
|
+
'medium': 'text-sm',
|
|
93
|
+
'large': 'text-base'
|
|
94
|
+
}
|
|
95
|
+
size_class = size_classes.get(size, 'text-xs')
|
|
96
|
+
|
|
97
|
+
classes = size_class
|
|
98
|
+
if css_class:
|
|
99
|
+
classes += f" {css_class}"
|
|
100
|
+
|
|
101
|
+
# Convert items to strings, keeping SafeString as-is
|
|
102
|
+
from django.utils.safestring import SafeString, mark_safe
|
|
103
|
+
processed_items = []
|
|
104
|
+
for item in filtered_items:
|
|
105
|
+
if isinstance(item, (SafeString, str)):
|
|
106
|
+
processed_items.append(item)
|
|
107
|
+
else:
|
|
108
|
+
processed_items.append(escape(str(item)))
|
|
109
|
+
|
|
110
|
+
# Join with separator
|
|
111
|
+
joined = mark_safe(separator.join(str(item) for item in processed_items))
|
|
112
|
+
|
|
113
|
+
return format_html('<span class="{}">{}</span>', classes, joined)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def header(
|
|
117
|
+
title: str,
|
|
118
|
+
subtitle: Optional[str] = None,
|
|
119
|
+
initials: Optional[str] = None,
|
|
120
|
+
avatar_variant: str = "primary"
|
|
121
|
+
) -> SafeString:
|
|
122
|
+
"""
|
|
123
|
+
Render header with avatar/initials, title and subtitle.
|
|
124
|
+
|
|
125
|
+
Creates a horizontal layout with circular avatar badge and text content.
|
|
126
|
+
Common pattern for displaying users, accounts, entities with identity.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
title: Main title text
|
|
130
|
+
subtitle: Optional subtitle text (smaller, muted)
|
|
131
|
+
initials: Optional initials for avatar (e.g., "AB", "JD")
|
|
132
|
+
avatar_variant: Color variant for avatar badge (primary, success, info, etc.)
|
|
133
|
+
|
|
134
|
+
Usage:
|
|
135
|
+
# User with avatar
|
|
136
|
+
html.header(
|
|
137
|
+
title="John Doe",
|
|
138
|
+
subtitle="john@example.com • Admin",
|
|
139
|
+
initials="JD"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Account with info
|
|
143
|
+
html.header(
|
|
144
|
+
title="Trading Account",
|
|
145
|
+
subtitle="Binance • SPOT • user@email.com",
|
|
146
|
+
initials="TA",
|
|
147
|
+
avatar_variant="success"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Simple title only
|
|
151
|
+
html.header(title="Item Name")
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
SafeString with header component HTML
|
|
155
|
+
"""
|
|
156
|
+
# Avatar/initials badge
|
|
157
|
+
avatar_html = ""
|
|
158
|
+
if initials:
|
|
159
|
+
avatar_html = format_html(
|
|
160
|
+
'<span class="inline-flex items-center justify-center rounded-full w-8 h-8 text-xs font-semibold {} mr-3">{}</span>',
|
|
161
|
+
CompositionElements._get_avatar_classes(avatar_variant),
|
|
162
|
+
escape(initials)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Title
|
|
166
|
+
title_html = format_html(
|
|
167
|
+
'<div class="font-medium text-sm text-font-default-light dark:text-font-default-dark">{}</div>',
|
|
168
|
+
escape(title)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Subtitle
|
|
172
|
+
subtitle_html = ""
|
|
173
|
+
if subtitle:
|
|
174
|
+
subtitle_html = format_html(
|
|
175
|
+
'<div class="text-xs text-font-subtle-light dark:text-font-subtle-dark mt-0.5">{}</div>',
|
|
176
|
+
escape(subtitle)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Combine
|
|
180
|
+
return format_html(
|
|
181
|
+
'<div class="flex items-center">{}<div>{}{}</div></div>',
|
|
182
|
+
avatar_html,
|
|
183
|
+
title_html,
|
|
184
|
+
subtitle_html
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _get_avatar_classes(variant: str) -> str:
|
|
189
|
+
"""Get CSS classes for avatar badge variant."""
|
|
190
|
+
variant_classes = {
|
|
191
|
+
'success': 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-200',
|
|
192
|
+
'warning': 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-200',
|
|
193
|
+
'danger': 'bg-danger-100 text-danger-800 dark:bg-danger-900 dark:text-danger-200',
|
|
194
|
+
'info': 'bg-info-100 text-info-800 dark:bg-info-900 dark:text-info-200',
|
|
195
|
+
'primary': 'bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200',
|
|
196
|
+
'secondary': 'bg-base-100 text-font-default-light dark:bg-base-800 dark:text-font-default-dark',
|
|
197
|
+
}
|
|
198
|
+
return variant_classes.get(variant, variant_classes['primary'])
|
|
@@ -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
|
+
)
|