django-cfg 1.3.5__py3-none-any.whl → 1.3.9__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.
Files changed (252) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +258 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +171 -461
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
  48. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  49. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
  50. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  51. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  52. django_cfg/apps/payments/middleware/api_access.py +32 -6
  53. django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
  54. django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
  55. django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
  56. django_cfg/apps/payments/models/balance.py +12 -0
  57. django_cfg/apps/payments/models/currencies.py +106 -32
  58. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  59. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  60. django_cfg/apps/payments/services/core/payment_service.py +1 -1
  61. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  62. django_cfg/apps/payments/services/providers/base.py +95 -39
  63. django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
  64. django_cfg/apps/payments/services/providers/models/base.py +122 -0
  65. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  66. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  67. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  68. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  69. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  70. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  71. django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
  72. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  73. django_cfg/apps/payments/services/providers/registry.py +4 -32
  74. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  75. django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
  76. django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
  77. django_cfg/apps/payments/tasks/__init__.py +39 -0
  78. django_cfg/apps/payments/tasks/types.py +73 -0
  79. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  80. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  81. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  82. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  83. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  84. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  85. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  86. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  87. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  88. django_cfg/apps/payments/urls_admin.py +1 -1
  89. django_cfg/apps/payments/views/api/currencies.py +5 -5
  90. django_cfg/apps/payments/views/overview/services.py +2 -2
  91. django_cfg/apps/payments/views/serializers/currencies.py +4 -3
  92. django_cfg/apps/support/admin/__init__.py +10 -1
  93. django_cfg/apps/support/admin/support_admin.py +338 -141
  94. django_cfg/apps/tasks/admin/__init__.py +11 -0
  95. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  96. django_cfg/apps/urls.py +1 -2
  97. django_cfg/config.py +1 -1
  98. django_cfg/core/config.py +10 -5
  99. django_cfg/core/generation.py +1 -1
  100. django_cfg/management/commands/__init__.py +13 -1
  101. django_cfg/management/commands/app_agent_diagnose.py +470 -0
  102. django_cfg/management/commands/app_agent_generate.py +342 -0
  103. django_cfg/management/commands/app_agent_info.py +308 -0
  104. django_cfg/management/commands/migrate_all.py +9 -3
  105. django_cfg/management/commands/migrator.py +11 -6
  106. django_cfg/management/commands/rundramatiq.py +3 -2
  107. django_cfg/middleware/__init__.py +0 -2
  108. django_cfg/models/api_keys.py +115 -0
  109. django_cfg/modules/django_admin/__init__.py +64 -0
  110. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  111. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  112. django_cfg/modules/django_admin/decorators/display.py +106 -0
  113. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  114. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  115. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  116. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  117. django_cfg/modules/django_admin/models/__init__.py +20 -0
  118. django_cfg/modules/django_admin/models/action_models.py +33 -0
  119. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  120. django_cfg/modules/django_admin/models/base.py +26 -0
  121. django_cfg/modules/django_admin/models/display_models.py +31 -0
  122. django_cfg/modules/django_admin/utils/badges.py +159 -0
  123. django_cfg/modules/django_admin/utils/displays.py +247 -0
  124. django_cfg/modules/django_app_agent/__init__.py +87 -0
  125. django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
  126. django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
  127. django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
  128. django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
  129. django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
  130. django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
  131. django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
  132. django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
  133. django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
  134. django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
  135. django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
  136. django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
  137. django_cfg/modules/django_app_agent/core/__init__.py +33 -0
  138. django_cfg/modules/django_app_agent/core/config.py +300 -0
  139. django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
  140. django_cfg/modules/django_app_agent/models/__init__.py +71 -0
  141. django_cfg/modules/django_app_agent/models/base.py +283 -0
  142. django_cfg/modules/django_app_agent/models/context.py +496 -0
  143. django_cfg/modules/django_app_agent/models/enums.py +481 -0
  144. django_cfg/modules/django_app_agent/models/requests.py +500 -0
  145. django_cfg/modules/django_app_agent/models/responses.py +585 -0
  146. django_cfg/modules/django_app_agent/pytest.ini +6 -0
  147. django_cfg/modules/django_app_agent/services/__init__.py +42 -0
  148. django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
  149. django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
  150. django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
  151. django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
  152. django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
  153. django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
  154. django_cfg/modules/django_app_agent/services/base.py +437 -0
  155. django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
  156. django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
  157. django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
  158. django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
  159. django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
  160. django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
  161. django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
  162. django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
  163. django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
  164. django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
  165. django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
  166. django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
  167. django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
  168. django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
  169. django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
  170. django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
  171. django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
  172. django_cfg/modules/django_app_agent/services/report_service.py +332 -0
  173. django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
  174. django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
  175. django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
  176. django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
  177. django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
  178. django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
  179. django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
  180. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
  181. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
  182. django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
  183. django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
  184. django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
  185. django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
  186. django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
  187. django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
  188. django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
  189. django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
  190. django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
  191. django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
  192. django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
  193. django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
  194. django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
  195. django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
  196. django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
  197. django_cfg/modules/django_app_agent/ui/cli.py +419 -0
  198. django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
  199. django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
  200. django_cfg/modules/django_app_agent/utils/logging.py +360 -0
  201. django_cfg/modules/django_app_agent/utils/validation.py +417 -0
  202. django_cfg/modules/django_currency/__init__.py +2 -2
  203. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  204. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  205. django_cfg/modules/django_currency/core/converter.py +12 -12
  206. django_cfg/modules/django_currency/database/__init__.py +2 -2
  207. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  208. django_cfg/modules/django_llm/llm/client.py +10 -2
  209. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  210. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  211. django_cfg/modules/django_unfold/dashboard.py +14 -13
  212. django_cfg/modules/django_unfold/models/config.py +1 -1
  213. django_cfg/registry/core.py +3 -0
  214. django_cfg/registry/third_party.py +2 -2
  215. django_cfg/template_archive/django_sample.zip +0 -0
  216. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
  217. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/RECORD +224 -118
  218. django_cfg/apps/accounts/admin/activity.py +0 -96
  219. django_cfg/apps/accounts/admin/group.py +0 -17
  220. django_cfg/apps/accounts/admin/otp.py +0 -59
  221. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  222. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  223. django_cfg/apps/accounts/admin/user.py +0 -300
  224. django_cfg/apps/agents/core/agent.py +0 -281
  225. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  226. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  227. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  228. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  229. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  230. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  231. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  232. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  233. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  234. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  235. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  236. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  237. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  238. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  239. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  240. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  241. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  242. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  243. django_cfg/apps/tasks/admin.py +0 -320
  244. django_cfg/middleware/static_nocache.py +0 -55
  245. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  246. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  247. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  248. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  249. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  250. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
  251. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
  252. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,10 @@
1
1
  """
2
- External Data admin interfaces with Unfold optimization.
2
+ External Data admin interfaces using Django Admin Utilities.
3
+
4
+ Enhanced external data management with Material Icons and optimized queries.
3
5
  """
4
6
 
5
7
  from django.contrib import admin, messages
6
- from django.utils.html import format_html
7
8
  from django.urls import reverse
8
9
  from django.utils.http import urlencode
9
10
  from django.shortcuts import redirect
@@ -14,12 +15,23 @@ from django.utils import timezone
14
15
  from django import forms
15
16
  from django_json_widget.widgets import JSONEditorWidget
16
17
  from unfold.admin import ModelAdmin, TabularInline
17
- from unfold.decorators import display, action
18
- from unfold.enums import ActionVariant
19
18
  from unfold.contrib.filters.admin import AutocompleteSelectFilter, AutocompleteSelectMultipleFilter
20
19
  from unfold.contrib.forms.widgets import WysiwygWidget
21
20
  from django_cfg import ExportMixin
22
21
 
22
+ from django_cfg.modules.django_admin import (
23
+ OptimizedModelAdmin,
24
+ DisplayMixin,
25
+ MoneyDisplayConfig,
26
+ StatusBadgeConfig,
27
+ DateTimeDisplayConfig,
28
+ Icons,
29
+ ActionVariant,
30
+ display,
31
+ action
32
+ )
33
+ from django_cfg.modules.django_admin.utils.badges import StatusBadge
34
+
23
35
  from ..models.external_data import ExternalData, ExternalDataChunk, ExternalDataType, ExternalDataStatus
24
36
 
25
37
 
@@ -30,20 +42,17 @@ class ExternalDataChunkInline(TabularInline):
30
42
  verbose_name = "External Data Chunk"
31
43
  verbose_name_plural = "🔗 External Data Chunks (Read-only)"
32
44
  extra = 0
33
- max_num = 0 # No new chunks allowed
34
- can_delete = False # Prevent deletion through inline
35
- show_change_link = True # Allow viewing individual chunks
45
+ max_num = 0
46
+ can_delete = False
47
+ show_change_link = True
36
48
 
37
49
  def has_add_permission(self, request, obj=None):
38
- """Disable adding new chunks through inline."""
39
50
  return False
40
51
 
41
52
  def has_change_permission(self, request, obj=None):
42
- """Disable editing chunks through inline."""
43
53
  return False
44
54
 
45
55
  def has_delete_permission(self, request, obj=None):
46
- """Disable deleting chunks through inline."""
47
56
  return False
48
57
 
49
58
  fields = [
@@ -55,20 +64,15 @@ class ExternalDataChunkInline(TabularInline):
55
64
  'has_embedding_inline', 'embedding_cost', 'created_at'
56
65
  ]
57
66
 
58
- # Unfold specific options
59
- hide_title = False # Show titles for better UX
60
- classes = ['collapse'] # Collapsed by default
67
+ hide_title = False
68
+ classes = ['collapse']
61
69
 
62
70
  @display(description="Content Preview")
63
71
  def content_preview_inline(self, obj):
64
72
  """Shortened content preview for inline display."""
65
73
  if not obj.content:
66
- return "-"
67
- preview = obj.content[:100] + "..." if len(obj.content) > 100 else obj.content
68
- return format_html(
69
- '<div style="max-width: 300px; font-size: 12px; color: #666;">{}</div>',
70
- preview
71
- )
74
+ return ""
75
+ return obj.content[:100] + "..." if len(obj.content) > 100 else obj.content
72
76
 
73
77
  @display(description="Has Embedding", boolean=True)
74
78
  def has_embedding_inline(self, obj):
@@ -81,15 +85,19 @@ class ExternalDataChunkInline(TabularInline):
81
85
 
82
86
 
83
87
  @admin.register(ExternalData)
84
- class ExternalDataAdmin(ModelAdmin, ExportMixin):
85
- """Admin interface for ExternalData model with Unfold styling."""
88
+ class ExternalDataAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ExportMixin):
89
+ """Admin interface for ExternalData model using Django Admin Utilities."""
90
+
91
+ # Performance optimization
92
+ select_related_fields = ['user', 'category']
86
93
 
87
94
  list_display = [
88
- 'title_display', 'source_type_badge', 'source_identifier_display', 'user',
89
- 'status_badge', 'chunks_count_display', 'tokens_display', 'cost_display',
90
- 'visibility_badge', 'processed_at', 'created_at'
95
+ 'title_display', 'source_type_display', 'source_identifier_display', 'user_display',
96
+ 'status_display', 'chunks_count_display', 'tokens_display', 'cost_display',
97
+ 'visibility_display', 'processed_at_display', 'created_at_display'
91
98
  ]
92
- ordering = ['-created_at'] # Newest first
99
+ list_display_links = ['title_display']
100
+ ordering = ['-created_at']
93
101
  inlines = [ExternalDataChunkInline]
94
102
  list_filter = [
95
103
  'source_type', 'status', 'is_active', 'is_public',
@@ -100,51 +108,36 @@ class ExternalDataAdmin(ModelAdmin, ExportMixin):
100
108
  search_fields = ['title', 'description', 'source_identifier', 'user__username', 'user__email']
101
109
  autocomplete_fields = ['user', 'category']
102
110
  readonly_fields = [
103
- 'id', 'user', 'status', 'total_chunks', 'total_tokens', 'processing_cost',
104
- 'processed_at', 'source_updated_at', 'processing_error', 'content_hash',
111
+ 'id', 'user', 'source_type', 'source_identifier', 'status',
112
+ 'processed_at', 'processing_error',
113
+ 'total_chunks', 'total_tokens', 'processing_cost',
105
114
  'created_at', 'updated_at'
106
115
  ]
107
116
 
108
117
  fieldsets = (
109
- ('📝 Basic Information', {
110
- 'fields': ('title', 'description', 'category'),
111
- 'description': 'Main information about the external data source'
112
- }),
113
- ('📄 Content', {
114
- 'fields': ('content',),
115
- 'description': 'Main content for vectorization'
118
+ ('🔗 External Data Info', {
119
+ 'fields': ('id', 'title', 'description', 'user', 'category'),
120
+ 'classes': ('tab',)
116
121
  }),
117
- ('🔗 Source Configuration', {
118
- 'fields': ('source_type', 'source_identifier', 'source_config'),
119
- 'description': 'Configure how data is extracted from the source'
122
+ ('📡 Source Details', {
123
+ 'fields': ('source_type', 'source_identifier', 'source_metadata'),
124
+ 'classes': ('tab',)
120
125
  }),
121
- ('⚙️ Processing Settings', {
122
- 'fields': ('chunk_size', 'overlap_size', 'embedding_model', 'similarity_threshold'),
123
- 'description': 'Settings for text chunking, vectorization, and search'
126
+ ('⚙️ Processing Status', {
127
+ 'fields': ('status', 'processed_at', 'processing_error'),
128
+ 'classes': ('tab',)
124
129
  }),
125
- ('🔒 Access Control', {
126
- 'fields': ('is_active', 'is_public'),
127
- 'description': 'Control visibility and access to this data'
128
- }),
129
- ('📊 Processing Status (Read-only)', {
130
- 'fields': ('status', 'processing_error', 'processed_at', 'source_updated_at', 'content_hash'),
131
- 'classes': ('collapse',),
132
- 'description': 'Current processing status and error information'
133
- }),
134
- ('📈 Statistics (Read-only)', {
130
+ ('📊 Statistics', {
135
131
  'fields': ('total_chunks', 'total_tokens', 'processing_cost'),
136
- 'classes': ('collapse',),
137
- 'description': 'Processing statistics and costs'
132
+ 'classes': ('tab',)
138
133
  }),
139
- ('📋 Additional Data (Read-only)', {
140
- 'fields': ('metadata', 'tags'),
141
- 'classes': ('collapse',),
142
- 'description': 'Auto-generated metadata and tags'
134
+ ('🔧 Settings', {
135
+ 'fields': ('is_active', 'is_public', 'embedding_model'),
136
+ 'classes': ('tab',)
143
137
  }),
144
- ('🔧 System Information (Read-only)', {
145
- 'fields': ('id', 'user', 'created_at', 'updated_at'),
146
- 'classes': ('collapse',),
147
- 'description': 'System-generated information'
138
+ (' Timestamps', {
139
+ 'fields': ('created_at', 'updated_at'),
140
+ 'classes': ('tab', 'collapse')
148
141
  })
149
142
  )
150
143
 
@@ -154,179 +147,182 @@ class ExternalDataAdmin(ModelAdmin, ExportMixin):
154
147
 
155
148
  # Form field overrides
156
149
  formfield_overrides = {
157
- JSONField: {
158
- "widget": JSONEditorWidget,
159
- }
150
+ models.TextField: {"widget": WysiwygWidget},
151
+ JSONField: {"widget": JSONEditorWidget}
160
152
  }
161
153
 
162
- def formfield_for_dbfield(self, db_field, request, **kwargs):
163
- """Custom field handling for better UX."""
164
- if db_field.name == 'content':
165
- kwargs['widget'] = WysiwygWidget()
166
- elif db_field.name == 'description':
167
- kwargs['widget'] = forms.Textarea(attrs={
168
- 'rows': 3,
169
- 'cols': 80,
170
- 'style': 'width: 100%;'
171
- })
172
- return super().formfield_for_dbfield(db_field, request, **kwargs)
173
-
174
- actions = [
175
- 'mark_for_reprocessing',
176
- 'regenerate_embeddings',
177
- 'activate_selected',
178
- 'deactivate_selected',
179
- 'make_public',
180
- 'make_private'
181
- ]
182
-
183
- # Actions for detail view (individual object edit page)
184
- actions_detail = ["reprocess_external_data"]
154
+ actions = ['reprocess_data', 'activate_data', 'deactivate_data', 'mark_as_public', 'mark_as_private']
185
155
 
186
- def get_queryset(self, request):
187
- """Optimize queryset with select_related and prefetch_related."""
188
- queryset = super().get_queryset(request).select_related('user', 'category')
189
- queryset = queryset.annotate(
190
- annotated_chunks_count=Count('chunks'),
191
- total_embedding_cost=Sum('chunks__embedding_cost')
192
- )
193
-
194
- # For non-superusers, filter by their own external data
195
- if not request.user.is_superuser:
196
- queryset = queryset.filter(user=request.user)
197
-
198
- return queryset
199
-
200
- def save_model(self, request, obj, form, change):
201
- """Automatically set user to current user when creating new external data."""
202
- if not change: # Only for new external data
203
- obj.user = request.user
204
- super().save_model(request, obj, form, change)
205
-
206
- @display(description="External Data Title", ordering="title")
156
+ @display(description="Title", ordering="title")
207
157
  def title_display(self, obj):
208
- """Display external data title with truncation."""
158
+ """Display external data title."""
209
159
  title = obj.title or "Untitled External Data"
210
160
  if len(title) > 50:
211
161
  title = title[:47] + "..."
212
- return format_html(
213
- '<div style="font-weight: 500;">{}</div>',
214
- title
162
+
163
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.CLOUD)
164
+ return StatusBadge.create(
165
+ text=title,
166
+ variant="primary",
167
+ config=config
215
168
  )
216
169
 
217
- @display(
218
- description="Source Type",
219
- ordering="source_type",
220
- label={
221
- 'model': 'info', # blue for Django models
222
- 'api': 'success', # green for API
223
- 'database': 'warning', # orange for database
224
- 'file': 'secondary', # gray for files
225
- 'custom': 'danger' # red for custom
170
+ @display(description="Source Type")
171
+ def source_type_display(self, obj):
172
+ """Display source type with badge."""
173
+ if not obj.source_type:
174
+ return "—"
175
+
176
+ type_variants = {
177
+ 'api': 'info',
178
+ 'webhook': 'success',
179
+ 'database': 'warning',
180
+ 'file': 'secondary'
226
181
  }
227
- )
228
- def source_type_badge(self, obj):
229
- """Display source type with color coding."""
230
- return obj.source_type, obj.get_source_type_display()
182
+ variant = type_variants.get(obj.source_type.lower(), 'secondary')
183
+
184
+ type_icons = {
185
+ 'api': Icons.API,
186
+ 'webhook': Icons.WEBHOOK,
187
+ 'database': Icons.STORAGE,
188
+ 'file': Icons.INSERT_DRIVE_FILE
189
+ }
190
+ icon = type_icons.get(obj.source_type.lower(), Icons.CLOUD)
191
+
192
+ config = StatusBadgeConfig(show_icons=True, icon=icon)
193
+ return StatusBadge.create(
194
+ text=obj.source_type.upper(),
195
+ variant=variant,
196
+ config=config
197
+ )
231
198
 
232
- @display(description="Source Identifier", ordering="source_identifier")
199
+ @display(description="Source ID", ordering="source_identifier")
233
200
  def source_identifier_display(self, obj):
234
201
  """Display source identifier with truncation."""
202
+ if not obj.source_identifier:
203
+ return "—"
204
+
235
205
  identifier = obj.source_identifier
236
206
  if len(identifier) > 30:
237
207
  identifier = identifier[:27] + "..."
238
- return format_html(
239
- '<code style="font-size: 12px;">{}</code>',
240
- identifier
208
+
209
+ return identifier
210
+
211
+ @display(description="User")
212
+ def user_display(self, obj):
213
+ """User display."""
214
+ if not obj.user:
215
+ return "—"
216
+ return self.display_user_simple(obj.user)
217
+
218
+ @display(description="Status")
219
+ def status_display(self, obj):
220
+ """Display processing status."""
221
+ status_config = StatusBadgeConfig(
222
+ custom_mappings={
223
+ 'pending': 'warning',
224
+ 'processing': 'info',
225
+ 'completed': 'success',
226
+ 'failed': 'danger',
227
+ 'cancelled': 'secondary'
228
+ },
229
+ show_icons=True,
230
+ icon=Icons.CHECK_CIRCLE if obj.status == 'completed' else Icons.ERROR if obj.status == 'failed' else Icons.SCHEDULE
241
231
  )
232
+ return self.display_status_auto(obj, 'status', status_config)
242
233
 
243
- @display(
244
- description="Status",
245
- ordering="status",
246
- label={
247
- 'pending': 'warning', # orange for pending
248
- 'processing': 'info', # blue for processing
249
- 'completed': 'success', # green for completed
250
- 'failed': 'danger', # red for failed
251
- 'outdated': 'secondary' # gray for outdated
252
- }
253
- )
254
- def status_badge(self, obj):
255
- """Display processing status with color coding."""
256
- return obj.status, obj.get_status_display()
257
-
258
- @display(
259
- description="Visibility",
260
- ordering="is_active",
261
- label={
262
- True: 'success', # green for active
263
- False: 'danger' # red for inactive
264
- }
265
- )
266
- def visibility_badge(self, obj):
267
- """Display visibility status with color coding."""
268
- if obj.is_active:
269
- status = "Active" + (" & Public" if obj.is_public else " & Private")
270
- return True, status
271
- else:
272
- return False, "Inactive"
273
-
274
- @display(description="Chunks", ordering="annotated_chunks_count")
234
+ @display(description="Chunks", ordering="chunks_count")
275
235
  def chunks_count_display(self, obj):
276
- """Display chunks count with link."""
277
- count = getattr(obj, 'annotated_chunks_count', obj.chunks.count())
278
- if count > 0:
279
- url = f"/admin/knowbase/externaldatachunk/?external_data__id__exact={obj.id}"
280
- return format_html(
281
- '<a href="{}" style="text-decoration: none;">{} chunks</a>',
282
- url, count
283
- )
284
- return "0 chunks"
236
+ """Display chunks count."""
237
+ count = obj.chunks_count or 0
238
+ return f"{count} chunks"
285
239
 
286
240
  @display(description="Tokens", ordering="total_tokens")
287
241
  def tokens_display(self, obj):
288
242
  """Display token count with formatting."""
289
- tokens = obj.total_tokens
243
+ tokens = obj.total_tokens or 0
290
244
  if tokens > 1000:
291
245
  return f"{tokens/1000:.1f}K"
292
246
  return str(tokens)
293
-
247
+
294
248
  @display(description="Cost (USD)", ordering="processing_cost")
295
249
  def cost_display(self, obj):
296
250
  """Display cost with currency formatting."""
297
- return f"${obj.processing_cost:.6f}"
298
-
299
- @display(description="Content Hash")
300
- def content_hash_display(self, obj):
301
- """Display content hash for change tracking."""
302
- if obj.content_hash:
303
- return format_html(
304
- '<code style="font-size: 10px; color: #666;">{}</code>',
305
- obj.content_hash[:12] + "..."
306
- )
307
- return "-"
251
+ config = MoneyDisplayConfig(
252
+ currency="USD",
253
+ decimal_places=6,
254
+ show_sign=False
255
+ )
256
+ return self.display_money_amount(obj, 'processing_cost', config)
257
+
258
+ @display(description="Visibility")
259
+ def visibility_display(self, obj):
260
+ """Display visibility status."""
261
+ if obj.is_public:
262
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.PUBLIC)
263
+ return StatusBadge.create(text="Public", variant="success", config=config)
264
+ else:
265
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.LOCK)
266
+ return StatusBadge.create(text="Private", variant="danger", config=config)
267
+
268
+ @display(description="Processed", ordering="processed_at")
269
+ def processed_at_display(self, obj):
270
+ """Processed time with relative display."""
271
+ if not obj.processed_at:
272
+ return "—"
273
+ config = DateTimeDisplayConfig(show_relative=True)
274
+ return self.display_datetime_relative(obj, 'processed_at', config)
275
+
276
+ @display(description="Created")
277
+ def created_at_display(self, obj):
278
+ """Created time with relative display."""
279
+ config = DateTimeDisplayConfig(show_relative=True)
280
+ return self.display_datetime_relative(obj, 'created_at', config)
281
+
282
+ @action(description="Reprocess data", variant=ActionVariant.INFO)
283
+ def reprocess_data(self, request, queryset):
284
+ """Reprocess selected external data."""
285
+ count = queryset.count()
286
+ messages.info(request, f"Reprocess functionality not implemented yet. {count} items selected.")
287
+
288
+ @action(description="Activate data", variant=ActionVariant.SUCCESS)
289
+ def activate_data(self, request, queryset):
290
+ """Activate selected external data."""
291
+ updated = queryset.update(is_active=True)
292
+ messages.success(request, f"Activated {updated} external data items.")
293
+
294
+ @action(description="Deactivate data", variant=ActionVariant.WARNING)
295
+ def deactivate_data(self, request, queryset):
296
+ """Deactivate selected external data."""
297
+ updated = queryset.update(is_active=False)
298
+ messages.warning(request, f"Deactivated {updated} external data items.")
299
+
300
+ @action(description="Mark as public", variant=ActionVariant.SUCCESS)
301
+ def mark_as_public(self, request, queryset):
302
+ """Mark selected data as public."""
303
+ updated = queryset.update(is_public=True)
304
+ messages.success(request, f"Marked {updated} items as public.")
305
+
306
+ @action(description="Mark as private", variant=ActionVariant.WARNING)
307
+ def mark_as_private(self, request, queryset):
308
+ """Mark selected data as private."""
309
+ updated = queryset.update(is_public=False)
310
+ messages.warning(request, f"Marked {updated} items as private.")
308
311
 
309
312
  def changelist_view(self, request, extra_context=None):
310
- """Add summary statistics to changelist."""
313
+ """Add external data statistics to changelist."""
311
314
  extra_context = extra_context or {}
312
315
 
313
- # Get summary statistics
314
316
  queryset = self.get_queryset(request)
315
317
  stats = queryset.aggregate(
316
- total_external_data=Count('id'),
317
- active_external_data=Count('id', filter=Q(is_active=True)),
318
- total_chunks=Sum('total_chunks'),
318
+ total_items=Count('id'),
319
+ active_items=Count('id', filter=Q(is_active=True)),
320
+ completed_items=Count('id', filter=Q(status='completed')),
321
+ total_chunks=Sum('chunks_count'),
319
322
  total_tokens=Sum('total_tokens'),
320
323
  total_cost=Sum('processing_cost')
321
324
  )
322
325
 
323
- # Status breakdown
324
- status_counts = dict(
325
- queryset.values_list('status').annotate(
326
- count=Count('id')
327
- )
328
- )
329
-
330
326
  # Source type breakdown
331
327
  source_type_counts = dict(
332
328
  queryset.values_list('source_type').annotate(
@@ -335,254 +331,89 @@ class ExternalDataAdmin(ModelAdmin, ExportMixin):
335
331
  )
336
332
 
337
333
  extra_context['external_data_stats'] = {
338
- 'total_external_data': stats['total_external_data'] or 0,
339
- 'active_external_data': stats['active_external_data'] or 0,
334
+ 'total_items': stats['total_items'] or 0,
335
+ 'active_items': stats['active_items'] or 0,
336
+ 'completed_items': stats['completed_items'] or 0,
340
337
  'total_chunks': stats['total_chunks'] or 0,
341
338
  'total_tokens': stats['total_tokens'] or 0,
342
339
  'total_cost': f"${(stats['total_cost'] or 0):.6f}",
343
- 'status_counts': status_counts,
344
340
  'source_type_counts': source_type_counts
345
341
  }
346
342
 
347
343
  return super().changelist_view(request, extra_context)
348
-
349
- @admin.action(description='Mark selected for reprocessing')
350
- def mark_for_reprocessing(self, request, queryset):
351
- """Mark external data for reprocessing."""
352
- updated = queryset.update(
353
- status=ExternalDataStatus.PENDING,
354
- processing_error='',
355
- updated_at=timezone.now()
356
- )
357
- self.message_user(
358
- request,
359
- f"Marked {updated} external data sources for reprocessing."
360
- )
361
-
362
- @admin.action(description='Activate selected external data')
363
- def activate_selected(self, request, queryset):
364
- """Activate selected external data."""
365
- updated = queryset.update(is_active=True)
366
- self.message_user(
367
- request,
368
- f"Activated {updated} external data sources."
369
- )
370
-
371
- @admin.action(description='Deactivate selected external data')
372
- def deactivate_selected(self, request, queryset):
373
- """Deactivate selected external data."""
374
- updated = queryset.update(is_active=False)
375
- self.message_user(
376
- request,
377
- f"Deactivated {updated} external data sources."
378
- )
379
-
380
- @admin.action(description='Make selected external data public')
381
- def make_public(self, request, queryset):
382
- """Make selected external data public."""
383
- updated = queryset.update(is_public=True)
384
- self.message_user(
385
- request,
386
- f"Made {updated} external data sources public."
387
- )
388
-
389
- @admin.action(description='Make selected external data private')
390
- def make_private(self, request, queryset):
391
- """Make selected external data private."""
392
- updated = queryset.update(is_public=False)
393
- self.message_user(
394
- request,
395
- f"Made {updated} external data sources private."
396
- )
397
-
398
- @admin.action(description='🔄 Regenerate embeddings for selected external data')
399
- def regenerate_embeddings(self, request, queryset):
400
- """Regenerate embeddings for selected external data sources."""
401
- external_data_ids = [str(obj.id) for obj in queryset]
402
-
403
- if not external_data_ids:
404
- self.message_user(
405
- request,
406
- "No external data selected for regeneration.",
407
- level=messages.WARNING
408
- )
409
- return
410
-
411
- try:
412
- # Use manager method for regeneration
413
- result = ExternalData.objects.regenerate_external_data(external_data_ids)
414
-
415
- if result['success']:
416
- success_msg = (
417
- f"✅ Successfully queued {result['regenerated_count']} external data sources "
418
- f"for regeneration."
419
- )
420
-
421
- if result['failed_count'] > 0:
422
- success_msg += f" {result['failed_count']} failed to queue."
423
-
424
- self.message_user(request, success_msg, level=messages.SUCCESS)
425
-
426
- # Show errors if any
427
- if result.get('errors'):
428
- for error in result['errors'][:3]: # Show first 3 errors
429
- self.message_user(request, f"❌ {error}", level=messages.ERROR)
430
-
431
- if len(result['errors']) > 3:
432
- self.message_user(
433
- request,
434
- f"... and {len(result['errors']) - 3} more errors.",
435
- level=messages.WARNING
436
- )
437
- else:
438
- error_msg = result.get('error', 'Unknown error occurred')
439
- self.message_user(
440
- request,
441
- f"❌ Failed to regenerate external data: {error_msg}",
442
- level=messages.ERROR
443
- )
444
-
445
- except Exception as e:
446
- self.message_user(
447
- request,
448
- f"❌ Error during regeneration: {str(e)}",
449
- level=messages.ERROR
450
- )
451
-
452
- @action(
453
- description="🔄 Reprocess External Data",
454
- icon="refresh",
455
- variant=ActionVariant.WARNING
456
- )
457
- def reprocess_external_data(self, request, object_id):
458
- """Force reprocessing of the external data source."""
459
- try:
460
- # Get the external data object
461
- external_data = self.get_object(request, object_id)
462
- if not external_data:
463
- self.message_user(
464
- request,
465
- "External data not found.",
466
- level=messages.ERROR
467
- )
468
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
469
-
470
- # Use manager method for regeneration
471
- result = ExternalData.objects.regenerate_external_data([str(external_data.id)])
472
-
473
- if result['success']:
474
- self.message_user(
475
- request,
476
- f"✅ Successfully queued '{external_data.title}' for reprocessing. "
477
- f"Processing will begin shortly in the background.",
478
- level=messages.SUCCESS
479
- )
480
- else:
481
- error_msg = result.get('error', 'Unknown error occurred')
482
- self.message_user(
483
- request,
484
- f"❌ Failed to reprocess '{external_data.title}': {error_msg}",
485
- level=messages.ERROR
486
- )
487
-
488
- except Exception as e:
489
- self.message_user(
490
- request,
491
- f"❌ Error during reprocessing: {str(e)}",
492
- level=messages.ERROR
493
- )
494
-
495
- # Redirect back to the same page
496
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
497
-
498
- def has_add_permission(self, request):
499
- """Allow adding external data."""
500
- return True
501
-
502
- def has_change_permission(self, request, obj=None):
503
- """Check change permissions."""
504
- if obj is not None and not request.user.is_superuser:
505
- return obj.user == request.user
506
- return super().has_change_permission(request, obj)
507
-
508
- def has_delete_permission(self, request, obj=None):
509
- """Check delete permissions."""
510
- if obj is not None and not request.user.is_superuser:
511
- return obj.user == request.user
512
- return super().has_delete_permission(request, obj)
513
344
 
514
345
 
515
346
  @admin.register(ExternalDataChunk)
516
- class ExternalDataChunkAdmin(ModelAdmin):
517
- """Admin interface for ExternalDataChunk model with Unfold styling."""
347
+ class ExternalDataChunkAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ExportMixin):
348
+ """Admin interface for ExternalDataChunk model using Django Admin Utilities."""
349
+
350
+ # Performance optimization
351
+ select_related_fields = ['external_data', 'user']
518
352
 
519
353
  list_display = [
520
- 'chunk_display', 'external_data_link', 'user', 'token_count_display',
521
- 'embedding_status', 'embedding_cost_display', 'created_at'
354
+ 'chunk_display', 'external_data_display', 'user_display', 'token_count_display',
355
+ 'embedding_status', 'embedding_cost_display', 'created_at_display'
522
356
  ]
523
- ordering = ['-created_at'] # Newest first
357
+ list_display_links = ['chunk_display']
358
+ ordering = ['-created_at']
524
359
  list_filter = [
525
- 'embedding_model', 'external_data__source_type',
526
- 'external_data__status', 'created_at',
360
+ 'embedding_model', 'created_at',
527
361
  ('user', AutocompleteSelectFilter),
528
362
  ('external_data', AutocompleteSelectFilter)
529
363
  ]
530
- search_fields = ['external_data__title', 'external_data__source_identifier', 'content', 'user__username']
364
+ search_fields = ['external_data__title', 'user__username', 'content']
365
+ autocomplete_fields = ['external_data', 'user']
531
366
  readonly_fields = [
532
- 'id', 'user', 'external_data', 'chunk_index', 'embedding_info',
533
- 'token_count', 'character_count', 'embedding_cost', 'embedding_model',
367
+ 'id', 'token_count', 'character_count', 'embedding_cost',
534
368
  'created_at', 'updated_at', 'content_preview'
535
369
  ]
536
370
 
537
371
  fieldsets = (
538
- ('📄 Content', {
539
- 'fields': ('content_preview', 'content'),
540
- 'description': 'Text content of this chunk'
372
+ ('🔗 Chunk Info', {
373
+ 'fields': ('id', 'external_data', 'user', 'chunk_index'),
374
+ 'classes': ('tab',)
541
375
  }),
542
- ('🔗 Relationships (Read-only)', {
543
- 'fields': ('external_data', 'user', 'chunk_index'),
544
- 'description': 'Links to parent external data and owner'
545
- }),
546
- ('📊 Metrics (Read-only)', {
547
- 'fields': ('embedding_info', 'token_count', 'character_count', 'embedding_cost'),
548
- 'description': 'Processing metrics and costs'
376
+ ('📝 Content', {
377
+ 'fields': ('content_preview', 'content'),
378
+ 'classes': ('tab',)
549
379
  }),
550
- ('🧠 Vector Embedding (Read-only)', {
551
- 'fields': ('embedding_model', 'embedding'),
552
- 'classes': ('collapse',),
553
- 'description': 'Vector representation for semantic search'
380
+ ('🧠 Embedding', {
381
+ 'fields': ('embedding_model', 'token_count', 'character_count', 'embedding_cost'),
382
+ 'classes': ('tab',)
554
383
  }),
555
- ('📋 Metadata (Read-only)', {
556
- 'fields': ('chunk_metadata',),
557
- 'classes': ('collapse',),
558
- 'description': 'Auto-generated chunk metadata'
384
+ ('🔧 Vector', {
385
+ 'fields': ('embedding',),
386
+ 'classes': ('tab', 'collapse')
559
387
  }),
560
- ('🔧 System Information (Read-only)', {
561
- 'fields': ('id', 'created_at', 'updated_at'),
562
- 'classes': ('collapse',),
563
- 'description': 'System-generated information'
388
+ (' Timestamps', {
389
+ 'fields': ('created_at', 'updated_at'),
390
+ 'classes': ('tab', 'collapse')
564
391
  })
565
392
  )
566
393
 
567
- # Unfold configuration
568
- compressed_fields = True
569
- warn_unsaved_form = True
570
-
571
- # Form field overrides
572
- formfield_overrides = {
573
- JSONField: {
574
- "widget": JSONEditorWidget,
575
- }
576
- }
577
-
578
- def get_queryset(self, request):
579
- """Optimize queryset with select_related."""
580
- return super().get_queryset(request).select_related('external_data', 'user')
394
+ actions = ['regenerate_embeddings', 'clear_embeddings']
581
395
 
582
396
  @display(description="Chunk", ordering="chunk_index")
583
397
  def chunk_display(self, obj):
584
398
  """Display chunk identifier."""
585
- return f"Chunk {obj.chunk_index + 1}"
399
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.ARTICLE)
400
+ return StatusBadge.create(
401
+ text=f"Chunk {obj.chunk_index + 1}",
402
+ variant="info",
403
+ config=config
404
+ )
405
+
406
+ @display(description="External Data", ordering="external_data__title")
407
+ def external_data_display(self, obj):
408
+ """Display external data title."""
409
+ return obj.external_data.title or "Untitled External Data"
410
+
411
+ @display(description="User")
412
+ def user_display(self, obj):
413
+ """User display."""
414
+ if not obj.user:
415
+ return "—"
416
+ return self.display_user_simple(obj.user)
586
417
 
587
418
  @display(description="Tokens", ordering="token_count")
588
419
  def token_count_display(self, obj):
@@ -592,95 +423,46 @@ class ExternalDataChunkAdmin(ModelAdmin):
592
423
  return f"{tokens/1000:.1f}K"
593
424
  return str(tokens)
594
425
 
595
- @display(
596
- description="Embedding",
597
- label={
598
- True: 'success', # green for has embedding
599
- False: 'danger' # red for no embedding
600
- }
601
- )
426
+ @display(description="Embedding")
602
427
  def embedding_status(self, obj):
603
428
  """Display embedding status."""
604
429
  has_embedding = obj.embedding is not None and len(obj.embedding) > 0
605
- return has_embedding, "✓ Vectorized" if has_embedding else "✗ Not vectorized"
606
-
607
- @display(description="External Data", ordering="external_data__title")
608
- def external_data_link(self, obj):
609
- """Display external data title with admin link."""
610
- url = reverse('admin:django_cfg_knowbase_externaldata_change', args=[obj.external_data.id])
611
- return format_html(
612
- '<a href="{}" style="text-decoration: none;">{}</a>',
613
- url,
614
- obj.external_data.title
615
- )
430
+ if has_embedding:
431
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.CHECK_CIRCLE)
432
+ return StatusBadge.create(text=" Vectorized", variant="success", config=config)
433
+ else:
434
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.ERROR)
435
+ return StatusBadge.create(text="✗ Not vectorized", variant="danger", config=config)
616
436
 
617
437
  @display(description="Cost (USD)", ordering="embedding_cost")
618
438
  def embedding_cost_display(self, obj):
619
439
  """Display embedding cost with currency formatting."""
620
- return f"${obj.embedding_cost:.6f}"
440
+ config = MoneyDisplayConfig(
441
+ currency="USD",
442
+ decimal_places=6,
443
+ show_sign=False
444
+ )
445
+ return self.display_money_amount(obj, 'embedding_cost', config)
446
+
447
+ @display(description="Created")
448
+ def created_at_display(self, obj):
449
+ """Created time with relative display."""
450
+ config = DateTimeDisplayConfig(show_relative=True)
451
+ return self.display_datetime_relative(obj, 'created_at', config)
621
452
 
622
453
  @display(description="Content Preview")
623
454
  def content_preview(self, obj):
624
455
  """Display content preview with truncation."""
625
- preview = obj.content[:200] + "..." if len(obj.content) > 200 else obj.content
626
- return format_html(
627
- '<div style="max-width: 400px; word-wrap: break-word;">{}</div>',
628
- preview
629
- )
630
-
631
- @display(description="Embedding Info")
632
- def embedding_info(self, obj):
633
- """Display embedding information safely."""
634
- if obj.embedding is not None and len(obj.embedding) > 0:
635
- return format_html(
636
- '<span style="color: green;">✓ Vector ({} dimensions)</span>',
637
- len(obj.embedding)
638
- )
639
- return format_html(
640
- '<span style="color: red;">✗ No embedding</span>'
641
- )
642
-
643
- def changelist_view(self, request, extra_context=None):
644
- """Add chunk statistics to changelist."""
645
- extra_context = extra_context or {}
646
-
647
- queryset = self.get_queryset(request)
648
- stats = queryset.aggregate(
649
- total_chunks=Count('id'),
650
- total_tokens=Sum('token_count'),
651
- total_characters=Sum('character_count'),
652
- total_embedding_cost=Sum('embedding_cost'),
653
- avg_tokens_per_chunk=Avg('token_count')
654
- )
655
-
656
- # Model breakdown
657
- model_counts = dict(
658
- queryset.values_list('embedding_model').annotate(
659
- count=Count('id')
660
- )
661
- )
662
-
663
- extra_context['chunk_stats'] = {
664
- 'total_chunks': stats['total_chunks'] or 0,
665
- 'total_tokens': stats['total_tokens'] or 0,
666
- 'total_characters': stats['total_characters'] or 0,
667
- 'total_embedding_cost': f"${(stats['total_embedding_cost'] or 0):.6f}",
668
- 'avg_tokens_per_chunk': f"{(stats['avg_tokens_per_chunk'] or 0):.0f}",
669
- 'model_counts': model_counts
670
- }
671
-
672
- return super().changelist_view(request, extra_context)
673
-
674
- def has_add_permission(self, request):
675
- """Chunks are created automatically."""
676
- return False
677
-
678
- def has_change_permission(self, request, obj=None):
679
- """Chunks are read-only."""
680
- return False
456
+ return obj.content[:200] + "..." if len(obj.content) > 200 else obj.content
681
457
 
682
- def has_delete_permission(self, request, obj=None):
683
- """Allow deletion for cleanup."""
684
- if obj is not None and not request.user.is_superuser:
685
- return obj.user == request.user
686
- return super().has_delete_permission(request, obj)
458
+ @action(description="Regenerate embeddings", variant=ActionVariant.INFO)
459
+ def regenerate_embeddings(self, request, queryset):
460
+ """Regenerate embeddings for selected chunks."""
461
+ count = queryset.count()
462
+ messages.info(request, f"Regenerate embeddings functionality not implemented yet. {count} chunks selected.")
463
+
464
+ @action(description="Clear embeddings", variant=ActionVariant.WARNING)
465
+ def clear_embeddings(self, request, queryset):
466
+ """Clear embeddings for selected chunks."""
467
+ updated = queryset.update(embedding=None)
468
+ messages.warning(request, f"Cleared embeddings for {updated} chunks.")