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.

Files changed (182) hide show
  1. django_cfg/__init__.py +8 -4
  2. django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
  3. django_cfg/apps/dashboard/TRANSACTION_FIX.md +73 -0
  4. django_cfg/apps/dashboard/serializers/__init__.py +0 -12
  5. django_cfg/apps/dashboard/serializers/activity.py +1 -1
  6. django_cfg/apps/dashboard/services/__init__.py +0 -2
  7. django_cfg/apps/dashboard/services/charts_service.py +4 -3
  8. django_cfg/apps/dashboard/services/statistics_service.py +11 -2
  9. django_cfg/apps/dashboard/services/system_health_service.py +64 -106
  10. django_cfg/apps/dashboard/urls.py +0 -2
  11. django_cfg/apps/dashboard/views/__init__.py +0 -2
  12. django_cfg/apps/dashboard/views/commands_views.py +3 -6
  13. django_cfg/apps/dashboard/views/overview_views.py +14 -13
  14. django_cfg/apps/grpc/__init__.py +9 -0
  15. django_cfg/apps/grpc/admin/__init__.py +11 -0
  16. django_cfg/apps/{tasks → grpc}/admin/config.py +32 -41
  17. django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
  18. django_cfg/apps/grpc/apps.py +28 -0
  19. django_cfg/apps/grpc/auth/__init__.py +9 -0
  20. django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
  21. django_cfg/apps/grpc/interceptors/__init__.py +19 -0
  22. django_cfg/apps/grpc/interceptors/errors.py +241 -0
  23. django_cfg/apps/grpc/interceptors/logging.py +270 -0
  24. django_cfg/apps/grpc/interceptors/metrics.py +306 -0
  25. django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
  26. django_cfg/apps/grpc/management/__init__.py +1 -0
  27. django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
  28. django_cfg/apps/grpc/managers/__init__.py +10 -0
  29. django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
  30. django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
  31. django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
  32. django_cfg/apps/grpc/models/__init__.py +9 -0
  33. django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
  34. django_cfg/apps/grpc/serializers/__init__.py +23 -0
  35. django_cfg/apps/grpc/serializers/health.py +18 -0
  36. django_cfg/apps/grpc/serializers/requests.py +18 -0
  37. django_cfg/apps/grpc/serializers/services.py +50 -0
  38. django_cfg/apps/grpc/serializers/stats.py +22 -0
  39. django_cfg/apps/grpc/services/__init__.py +16 -0
  40. django_cfg/apps/grpc/services/base.py +375 -0
  41. django_cfg/apps/grpc/services/discovery.py +415 -0
  42. django_cfg/apps/grpc/urls.py +23 -0
  43. django_cfg/apps/grpc/utils/__init__.py +13 -0
  44. django_cfg/apps/grpc/utils/proto_gen.py +423 -0
  45. django_cfg/apps/grpc/views/__init__.py +9 -0
  46. django_cfg/apps/grpc/views/monitoring.py +497 -0
  47. django_cfg/apps/knowbase/apps.py +2 -2
  48. django_cfg/apps/maintenance/admin/api_key_admin.py +7 -9
  49. django_cfg/apps/maintenance/admin/site_admin.py +5 -4
  50. django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
  51. django_cfg/apps/payments/admin/balance_admin.py +26 -36
  52. django_cfg/apps/payments/admin/payment_admin.py +65 -85
  53. django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
  54. django_cfg/apps/rq/__init__.py +9 -0
  55. django_cfg/apps/rq/apps.py +80 -0
  56. django_cfg/apps/rq/management/__init__.py +1 -0
  57. django_cfg/apps/rq/management/commands/__init__.py +1 -0
  58. django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
  59. django_cfg/apps/rq/management/commands/rqstats.py +33 -0
  60. django_cfg/apps/rq/management/commands/rqworker.py +31 -0
  61. django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
  62. django_cfg/apps/rq/serializers/__init__.py +40 -0
  63. django_cfg/apps/rq/serializers/health.py +60 -0
  64. django_cfg/apps/rq/serializers/job.py +100 -0
  65. django_cfg/apps/rq/serializers/queue.py +80 -0
  66. django_cfg/apps/rq/serializers/schedule.py +178 -0
  67. django_cfg/apps/rq/serializers/testing.py +139 -0
  68. django_cfg/apps/rq/serializers/worker.py +58 -0
  69. django_cfg/apps/rq/services/__init__.py +25 -0
  70. django_cfg/apps/rq/services/config_helper.py +233 -0
  71. django_cfg/apps/rq/services/models/README.md +417 -0
  72. django_cfg/apps/rq/services/models/__init__.py +30 -0
  73. django_cfg/apps/rq/services/models/event.py +123 -0
  74. django_cfg/apps/rq/services/models/job.py +99 -0
  75. django_cfg/apps/rq/services/models/queue.py +92 -0
  76. django_cfg/apps/rq/services/models/worker.py +104 -0
  77. django_cfg/apps/rq/services/rq_converters.py +183 -0
  78. django_cfg/apps/rq/tasks/__init__.py +23 -0
  79. django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
  80. django_cfg/apps/rq/urls.py +54 -0
  81. django_cfg/apps/rq/views/__init__.py +19 -0
  82. django_cfg/apps/rq/views/jobs.py +882 -0
  83. django_cfg/apps/rq/views/monitoring.py +248 -0
  84. django_cfg/apps/rq/views/queues.py +261 -0
  85. django_cfg/apps/rq/views/schedule.py +400 -0
  86. django_cfg/apps/rq/views/testing.py +761 -0
  87. django_cfg/apps/rq/views/workers.py +195 -0
  88. django_cfg/apps/urls.py +13 -8
  89. django_cfg/config.py +106 -0
  90. django_cfg/core/base/config_model.py +16 -26
  91. django_cfg/core/builders/apps_builder.py +7 -11
  92. django_cfg/core/generation/integration_generators/__init__.py +3 -6
  93. django_cfg/core/generation/integration_generators/django_rq.py +80 -0
  94. django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
  95. django_cfg/core/generation/orchestrator.py +15 -15
  96. django_cfg/core/integration/display/startup.py +6 -20
  97. django_cfg/mixins/__init__.py +2 -0
  98. django_cfg/mixins/superadmin_api.py +59 -0
  99. django_cfg/models/__init__.py +3 -3
  100. django_cfg/models/api/grpc/__init__.py +59 -0
  101. django_cfg/models/api/grpc/config.py +364 -0
  102. django_cfg/models/django/__init__.py +3 -3
  103. django_cfg/models/django/django_rq.py +621 -0
  104. django_cfg/models/django/revolution_legacy.py +1 -1
  105. django_cfg/modules/base.py +19 -6
  106. django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
  107. django_cfg/modules/django_admin/config/background_task_config.py +4 -4
  108. django_cfg/modules/django_admin/utils/__init__.py +41 -3
  109. django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
  110. django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
  111. django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
  112. django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
  113. django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
  114. django_cfg/modules/django_admin/utils/html/badges.py +47 -0
  115. django_cfg/modules/django_admin/utils/html/base.py +167 -0
  116. django_cfg/modules/django_admin/utils/html/code.py +87 -0
  117. django_cfg/modules/django_admin/utils/html/composition.py +205 -0
  118. django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
  119. django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
  120. django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
  121. django_cfg/modules/django_admin/utils/html/progress.py +127 -0
  122. django_cfg/modules/django_admin/utils/html_builder.py +55 -408
  123. django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
  124. django_cfg/modules/django_unfold/navigation.py +21 -18
  125. django_cfg/pyproject.toml +4 -6
  126. django_cfg/registry/core.py +4 -7
  127. django_cfg/registry/modules.py +6 -0
  128. django_cfg/static/frontend/admin.zip +0 -0
  129. django_cfg/templates/admin/constance/includes/results_list.html +73 -0
  130. django_cfg/templates/admin/index.html +187 -62
  131. django_cfg/templatetags/django_cfg.py +61 -1
  132. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/METADATA +12 -4
  133. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/RECORD +140 -96
  134. django_cfg/apps/dashboard/permissions.py +0 -48
  135. django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
  136. django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
  137. django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
  138. django_cfg/apps/tasks/__init__.py +0 -64
  139. django_cfg/apps/tasks/admin/__init__.py +0 -4
  140. django_cfg/apps/tasks/admin/task_log.py +0 -265
  141. django_cfg/apps/tasks/apps.py +0 -15
  142. django_cfg/apps/tasks/filters/__init__.py +0 -10
  143. django_cfg/apps/tasks/filters/task_log.py +0 -121
  144. django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
  145. django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
  146. django_cfg/apps/tasks/models/__init__.py +0 -4
  147. django_cfg/apps/tasks/models/task_log.py +0 -246
  148. django_cfg/apps/tasks/serializers/__init__.py +0 -28
  149. django_cfg/apps/tasks/serializers/task_log.py +0 -249
  150. django_cfg/apps/tasks/services/__init__.py +0 -10
  151. django_cfg/apps/tasks/services/client/__init__.py +0 -7
  152. django_cfg/apps/tasks/services/client/client.py +0 -234
  153. django_cfg/apps/tasks/services/config_helper.py +0 -63
  154. django_cfg/apps/tasks/services/sync.py +0 -204
  155. django_cfg/apps/tasks/urls.py +0 -16
  156. django_cfg/apps/tasks/views/__init__.py +0 -10
  157. django_cfg/apps/tasks/views/task_log.py +0 -41
  158. django_cfg/apps/tasks/views/task_log_base.py +0 -41
  159. django_cfg/apps/tasks/views/task_log_overview.py +0 -100
  160. django_cfg/apps/tasks/views/task_log_related.py +0 -41
  161. django_cfg/apps/tasks/views/task_log_stats.py +0 -91
  162. django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
  163. django_cfg/core/generation/integration_generators/django_q2.py +0 -133
  164. django_cfg/core/generation/integration_generators/tasks.py +0 -88
  165. django_cfg/models/django/django_q2.py +0 -514
  166. django_cfg/models/tasks/__init__.py +0 -49
  167. django_cfg/models/tasks/backends.py +0 -122
  168. django_cfg/models/tasks/config.py +0 -209
  169. django_cfg/models/tasks/utils.py +0 -162
  170. django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
  171. django_cfg/modules/django_q2/README.md +0 -140
  172. django_cfg/modules/django_q2/__init__.py +0 -8
  173. django_cfg/modules/django_q2/apps.py +0 -107
  174. django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
  175. django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
  176. /django_cfg/apps/{tasks/migrations → grpc/management/commands}/__init__.py +0 -0
  177. /django_cfg/{modules/django_q2/management → apps/grpc/migrations}/__init__.py +0 -0
  178. /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
  179. /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
  180. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/WHEEL +0 -0
  181. {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
  182. {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)