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,22 +1,34 @@
1
1
  """
2
- Document admin interfaces with Unfold optimization.
2
+ Document admin interfaces using Django Admin Utilities.
3
+
4
+ Enhanced document management with Material Icons and optimized queries.
3
5
  """
4
6
 
5
- from django.contrib import admin
6
- from django.utils.html import format_html
7
+ from django.contrib import admin, messages
7
8
  from django.urls import reverse
8
9
  from django.utils.safestring import mark_safe
9
10
  from django.db import models, IntegrityError
10
- from django.db.models import Count, Sum, Avg
11
+ from django.db.models import Count, Sum, Avg, Q
11
12
  from django.db.models.fields.json import JSONField
12
- from django.contrib import messages
13
13
  from django_json_widget.widgets import JSONEditorWidget
14
14
  from unfold.admin import ModelAdmin, TabularInline
15
- from unfold.decorators import display
16
15
  from unfold.contrib.filters.admin import AutocompleteSelectFilter, AutocompleteSelectMultipleFilter
17
16
  from unfold.contrib.forms.widgets import WysiwygWidget
18
17
  from django_cfg import ImportExportModelAdmin, ExportMixin, ImportForm, ExportForm
19
18
 
19
+ from django_cfg.modules.django_admin import (
20
+ OptimizedModelAdmin,
21
+ DisplayMixin,
22
+ MoneyDisplayConfig,
23
+ StatusBadgeConfig,
24
+ DateTimeDisplayConfig,
25
+ Icons,
26
+ ActionVariant,
27
+ display,
28
+ action
29
+ )
30
+ from django_cfg.modules.django_admin.utils.badges import StatusBadge
31
+
20
32
  from ..models import Document, DocumentChunk, DocumentCategory
21
33
 
22
34
 
@@ -27,20 +39,17 @@ class DocumentChunkInline(TabularInline):
27
39
  verbose_name = "Document Chunk"
28
40
  verbose_name_plural = "📄 Document Chunks (Read-only)"
29
41
  extra = 0
30
- max_num = 0 # No new chunks allowed
31
- can_delete = False # Prevent deletion through inline
32
- show_change_link = True # Allow viewing individual chunks
42
+ max_num = 0
43
+ can_delete = False
44
+ show_change_link = True
33
45
 
34
46
  def has_add_permission(self, request, obj=None):
35
- """Disable adding new chunks through inline."""
36
47
  return False
37
48
 
38
49
  def has_change_permission(self, request, obj=None):
39
- """Disable editing chunks through inline."""
40
50
  return False
41
51
 
42
52
  def has_delete_permission(self, request, obj=None):
43
- """Disable deleting chunks through inline."""
44
53
  return False
45
54
 
46
55
  fields = [
@@ -52,20 +61,15 @@ class DocumentChunkInline(TabularInline):
52
61
  'has_embedding_inline', 'embedding_cost', 'created_at'
53
62
  ]
54
63
 
55
- # Unfold specific options
56
- hide_title = False # Show titles for better UX
57
- classes = ['collapse'] # Collapsed by default
64
+ hide_title = False
65
+ classes = ['collapse']
58
66
 
59
67
  @display(description="Content Preview")
60
68
  def content_preview_inline(self, obj):
61
69
  """Shortened content preview for inline display."""
62
70
  if not obj.content:
63
- return "-"
64
- preview = obj.content[:100] + "..." if len(obj.content) > 100 else obj.content
65
- return format_html(
66
- '<div style="max-width: 300px; font-size: 12px; color: #666;">{}</div>',
67
- preview
68
- )
71
+ return ""
72
+ return obj.content[:100] + "..." if len(obj.content) > 100 else obj.content
69
73
 
70
74
  @display(description="Has Embedding", boolean=True)
71
75
  def has_embedding_inline(self, obj):
@@ -78,18 +82,23 @@ class DocumentChunkInline(TabularInline):
78
82
 
79
83
 
80
84
  @admin.register(Document)
81
- class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
82
- """Admin interface for Document model with Unfold styling."""
85
+ class DocumentAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ImportExportModelAdmin):
86
+ """Admin interface for Document model using Django Admin Utilities."""
87
+
88
+ # Performance optimization
89
+ select_related_fields = ['user']
83
90
 
84
91
  # Import/Export configuration
85
92
  import_form_class = ImportForm
86
93
  export_form_class = ExportForm
87
94
 
88
95
  list_display = [
89
- 'title_display', 'categories_display', 'user',
90
- 'visibility_badge', 'status_badge', 'chunks_count_display', 'vectorization_progress', 'tokens_display', 'cost_display', 'created_at'
96
+ 'title_display', 'categories_display', 'user_display',
97
+ 'visibility_display', 'status_display', 'chunks_count_display',
98
+ 'vectorization_progress', 'tokens_display', 'cost_display', 'created_at_display'
91
99
  ]
92
- ordering = ['-created_at'] # Newest first
100
+ list_display_links = ['title_display']
101
+ ordering = ['-created_at']
93
102
  inlines = [DocumentChunkInline]
94
103
  list_filter = [
95
104
  'processing_status', 'is_public', 'file_type', 'created_at',
@@ -106,29 +115,33 @@ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
106
115
  ]
107
116
 
108
117
  fieldsets = (
109
- ('Basic Information', {
110
- 'fields': ('id', 'title', 'user', 'categories', 'is_public', 'file_type', 'file_size')
118
+ ('📄 Basic Information', {
119
+ 'fields': ('id', 'title', 'user', 'categories', 'is_public', 'file_type', 'file_size'),
120
+ 'classes': ('tab',)
111
121
  }),
112
- ('Content', {
122
+ ('📝 Content', {
113
123
  'fields': ('content', 'content_hash', 'duplicate_check'),
124
+ 'classes': ('tab',)
114
125
  }),
115
- ('Processing Status', {
126
+ ('⚙️ Processing Status', {
116
127
  'fields': (
117
128
  'processing_status', 'processing_started_at',
118
129
  'processing_completed_at', 'processing_error'
119
- )
130
+ ),
131
+ 'classes': ('tab',)
120
132
  }),
121
- ('Statistics', {
122
- 'fields': ('chunks_count', 'total_tokens', 'total_cost_usd')
133
+ ('📊 Statistics', {
134
+ 'fields': ('chunks_count', 'total_tokens', 'total_cost_usd'),
135
+ 'classes': ('tab',)
123
136
  }),
124
- ('Metadata', {
137
+ ('🔧 Metadata', {
125
138
  'fields': ('metadata',),
126
- 'classes': ('collapse',),
139
+ 'classes': ('tab', 'collapse'),
127
140
  'description': 'Auto-generated metadata (read-only)'
128
141
  }),
129
- ('Timestamps', {
142
+ ('Timestamps', {
130
143
  'fields': ('created_at', 'updated_at'),
131
- 'classes': ('collapse',)
144
+ 'classes': ('tab', 'collapse')
132
145
  })
133
146
  )
134
147
  filter_horizontal = ['categories']
@@ -139,20 +152,16 @@ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
139
152
 
140
153
  # Form field overrides
141
154
  formfield_overrides = {
142
- models.TextField: {
143
- "widget": WysiwygWidget,
144
- },
145
- JSONField: {
146
- "widget": JSONEditorWidget,
147
- }
155
+ models.TextField: {"widget": WysiwygWidget},
156
+ JSONField: {"widget": JSONEditorWidget}
148
157
  }
149
158
 
159
+ actions = ['reprocess_documents', 'mark_as_public', 'mark_as_private']
160
+
150
161
  def get_queryset(self, request):
151
162
  """Optimize queryset with select_related and prefetch_related."""
152
- # Use all_users() to show all documents in admin, then filter by user if needed
153
163
  queryset = Document.objects.all_users().select_related('user').prefetch_related('categories')
154
164
 
155
- # For non-superusers, filter by their own documents
156
165
  if not request.user.is_superuser:
157
166
  queryset = queryset.filter(user=request.user)
158
167
 
@@ -160,10 +169,9 @@ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
160
169
 
161
170
  def save_model(self, request, obj, form, change):
162
171
  """Automatically set user to current user when creating new documents."""
163
- if not change: # Only for new documents
172
+ if not change:
164
173
  obj.user = request.user
165
174
 
166
- # Check for duplicates using manager method
167
175
  is_duplicate, existing_doc = Document.objects.check_duplicate_before_save(
168
176
  user=obj.user,
169
177
  content=obj.content
@@ -176,7 +184,6 @@ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
176
184
  f'(created {existing_doc.created_at.strftime("%Y-%m-%d %H:%M")}). '
177
185
  f'Please modify the content or update the existing document.'
178
186
  )
179
- # Don't save, just return - this will keep the form open with the error
180
187
  return
181
188
 
182
189
  try:
@@ -190,8 +197,6 @@ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
190
197
  )
191
198
  else:
192
199
  messages.error(request, f'Database error: {str(e)}')
193
-
194
- # Re-raise the exception to prevent saving
195
200
  raise
196
201
 
197
202
  @display(description="Document Title", ordering="title")
@@ -200,112 +205,71 @@ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
200
205
  title = obj.title or "Untitled Document"
201
206
  if len(title) > 50:
202
207
  title = title[:47] + "..."
203
- return format_html(
204
- '<div style="font-weight: 500;">{}</div>',
205
- title
208
+
209
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.DESCRIPTION)
210
+ return StatusBadge.create(
211
+ text=title,
212
+ variant="primary",
213
+ config=config
206
214
  )
207
215
 
208
- @display(
209
- description="Visibility",
210
- ordering="is_public",
211
- label={
212
- True: 'success', # green for public
213
- False: 'danger' # red for private
214
- }
215
- )
216
- def visibility_badge(self, obj):
217
- """Display visibility status with color coding."""
218
- return obj.is_public, "Public" if obj.is_public else "Private"
219
-
220
- @display(
221
- description="Status",
222
- ordering="processing_status",
223
- label={
224
- 'pending': 'warning', # orange for pending
225
- 'processing': 'info', # blue for processing
226
- 'completed': 'success', # green for completed
227
- 'failed': 'danger', # red for failed
228
- 'cancelled': 'secondary' # gray for cancelled
229
- }
230
- )
231
- def status_badge(self, obj):
232
- """Display processing status with color coding."""
233
- return obj.processing_status, obj.get_processing_status_display()
234
-
235
- @display(
236
- description="Categories",
237
- ordering="categories__name",
238
- label={
239
- 'public': 'success', # green for public categories
240
- 'private': 'danger', # red for private categories
241
- 'mixed': 'warning', # orange for mixed visibility
242
- 'none': 'info' # blue for no categories
243
- }
244
- )
216
+ @display(description="User")
217
+ def user_display(self, obj):
218
+ """User display."""
219
+ if not obj.user:
220
+ return "—"
221
+ return self.display_user_simple(obj.user)
222
+
223
+ @display(description="Visibility")
224
+ def visibility_display(self, obj):
225
+ """Display visibility status."""
226
+ if obj.is_public:
227
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.PUBLIC)
228
+ return StatusBadge.create(text="Public", variant="success", config=config)
229
+ else:
230
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.LOCK)
231
+ return StatusBadge.create(text="Private", variant="danger", config=config)
232
+
233
+ @display(description="Status")
234
+ def status_display(self, obj):
235
+ """Display processing status."""
236
+ status_config = StatusBadgeConfig(
237
+ custom_mappings={
238
+ 'pending': 'warning',
239
+ 'processing': 'info',
240
+ 'completed': 'success',
241
+ 'failed': 'danger',
242
+ 'cancelled': 'secondary'
243
+ },
244
+ show_icons=True,
245
+ icon=Icons.CHECK_CIRCLE if obj.processing_status == 'completed' else Icons.ERROR if obj.processing_status == 'failed' else Icons.SCHEDULE
246
+ )
247
+ return self.display_status_auto(obj, 'processing_status', status_config)
248
+
249
+ @display(description="Categories")
245
250
  def categories_display(self, obj):
246
- """Display multiple categories with Unfold label styling."""
251
+ """Display categories count."""
247
252
  categories = obj.categories.all()
248
253
 
249
254
  if not categories:
250
- return 'none', 'No categories'
255
+ return "No categories"
251
256
 
252
- # Determine overall visibility status
253
257
  public_count = sum(1 for cat in categories if cat.is_public)
254
258
  private_count = len(categories) - public_count
255
259
 
256
260
  if private_count == 0:
257
- status = 'public'
258
- description = f"{len(categories)} public"
261
+ return f"{len(categories)} public"
259
262
  elif public_count == 0:
260
- status = 'private'
261
- description = f"{len(categories)} private"
263
+ return f"{len(categories)} private"
262
264
  else:
263
- status = 'mixed'
264
- description = f"{public_count} public, {private_count} private"
265
-
266
- # Return tuple for label display: (status_key, display_text)
267
- return status, f"{', '.join(cat.name for cat in categories)} ({description})"
265
+ return f"{public_count} public, {private_count} private"
268
266
 
269
- @display(description="Category Details", dropdown=True)
270
- def category_dropdown(self, obj):
271
- """Display category details in dropdown."""
272
- categories = obj.categories.all()
273
-
274
- if not categories:
275
- return {
276
- "title": "No Categories",
277
- "content": "<p class='text-gray-500 p-4'>This document has no categories assigned.</p>"
278
- }
279
-
280
- # Build dropdown items for each category
281
- items = []
282
- for category in categories:
283
- status_icon = "🟢" if category.is_public else "🔴"
284
- visibility = "Public" if category.is_public else "Private"
285
-
286
- items.append({
287
- "title": f"{status_icon} {category.name}",
288
- "link": f"/admin/knowbase/documentcategory/{category.id}/change/"
289
- })
290
-
291
- return {
292
- "title": f"Categories ({len(categories)})",
293
- "striped": True,
294
- "height": 200,
295
- "width": 300,
296
- "items": items
297
- }
298
-
299
267
  @display(description="Chunks", ordering="chunks_count")
300
268
  def chunks_count_display(self, obj):
301
- """Display chunks count with link."""
269
+ """Display chunks count."""
302
270
  count = obj.chunks_count
303
271
  if count > 0:
304
- url = f"/admin/knowbase/documentchunk/?document__id__exact={obj.id}"
305
- return format_html(
306
- '<a href="{}" style="text-decoration: none;">{} chunks</a>',
307
- url, count
308
- )
272
+ return f"{count} chunks"
309
273
  return "0 chunks"
310
274
 
311
275
  @display(description="Tokens", ordering="total_tokens")
@@ -319,21 +283,24 @@ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
319
283
  @display(description="Cost (USD)", ordering="total_cost_usd")
320
284
  def cost_display(self, obj):
321
285
  """Display cost with currency formatting."""
322
- return f"${obj.total_cost_usd:.6f}"
323
-
324
- @display(
325
- description="Vectorization",
326
- label={
327
- 'completed': 'success', # green for 100%
328
- 'partial': 'warning', # orange for partial
329
- 'none': 'danger', # red for 0%
330
- 'no_chunks': 'info' # blue for no chunks
331
- }
332
- )
286
+ config = MoneyDisplayConfig(
287
+ currency="USD",
288
+ decimal_places=6,
289
+ show_sign=False
290
+ )
291
+ return self.display_money_amount(obj, 'total_cost_usd', config)
292
+
293
+ @display(description="Vectorization")
333
294
  def vectorization_progress(self, obj):
334
- """Display vectorization progress with color coding."""
295
+ """Display vectorization progress."""
335
296
  return Document.objects.get_vectorization_status_display(obj)
336
297
 
298
+ @display(description="Created")
299
+ def created_at_display(self, obj):
300
+ """Created time with relative display."""
301
+ config = DateTimeDisplayConfig(show_relative=True)
302
+ return self.display_datetime_relative(obj, 'created_at', config)
303
+
337
304
  @display(description="Processing Duration")
338
305
  def processing_duration_display(self, obj):
339
306
  """Display processing duration in readable format."""
@@ -357,37 +324,41 @@ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
357
324
 
358
325
  if isinstance(duplicate_info, str):
359
326
  if "No duplicates found" in duplicate_info:
360
- return format_html(
361
- '<span style="color: #059669;">✓ No duplicates found</span>'
362
- )
363
- return duplicate_info # "No content hash"
327
+ return "✓ No duplicates found"
328
+ return duplicate_info
364
329
 
365
- # Format the duplicate information for display
366
330
  duplicates_data = duplicate_info['duplicates']
367
331
  count = duplicate_info['count']
368
332
 
369
- duplicate_list = []
370
- for dup in duplicates_data:
371
- url = reverse('admin:django_cfg_knowbase_document_change', args=[dup.pk])
372
- duplicate_list.append(
373
- f'<a href="{url}" target="_blank">{dup.title}</a> '
374
- f'({dup.created_at.strftime("%Y-%m-%d")})'
375
- )
376
-
377
- warning_text = f"⚠️ Found {count} duplicate(s):<br>" + "<br>".join(duplicate_list)
333
+ duplicate_names = [dup.title for dup in duplicates_data[:3]]
334
+ result = f"⚠️ Found {count} duplicate(s): " + ", ".join(duplicate_names)
378
335
  if count > 3:
379
- warning_text += f"<br>... and {count - 3} more"
336
+ result += f" and {count - 3} more"
380
337
 
381
- return format_html(
382
- '<div style="color: #d97706; font-weight: 500;">{}</div>',
383
- warning_text
384
- )
338
+ return result
339
+
340
+ @action(description="Reprocess documents", variant=ActionVariant.INFO)
341
+ def reprocess_documents(self, request, queryset):
342
+ """Reprocess selected documents."""
343
+ count = queryset.count()
344
+ messages.info(request, f"Reprocessing functionality not implemented yet. {count} documents selected.")
345
+
346
+ @action(description="Mark as public", variant=ActionVariant.SUCCESS)
347
+ def mark_as_public(self, request, queryset):
348
+ """Mark selected documents as public."""
349
+ updated = queryset.update(is_public=True)
350
+ messages.success(request, f"Marked {updated} documents as public.")
351
+
352
+ @action(description="Mark as private", variant=ActionVariant.WARNING)
353
+ def mark_as_private(self, request, queryset):
354
+ """Mark selected documents as private."""
355
+ updated = queryset.update(is_public=False)
356
+ messages.warning(request, f"Marked {updated} documents as private.")
385
357
 
386
358
  def changelist_view(self, request, extra_context=None):
387
359
  """Add summary statistics to changelist."""
388
360
  extra_context = extra_context or {}
389
361
 
390
- # Get summary statistics
391
362
  queryset = self.get_queryset(request)
392
363
  stats = queryset.aggregate(
393
364
  total_documents=Count('id'),
@@ -396,7 +367,6 @@ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
396
367
  total_cost=Sum('total_cost_usd')
397
368
  )
398
369
 
399
- # Status breakdown
400
370
  status_counts = dict(
401
371
  queryset.values_list('processing_status').annotate(
402
372
  count=Count('id')
@@ -415,14 +385,18 @@ class DocumentAdmin(ModelAdmin, ImportExportModelAdmin):
415
385
 
416
386
 
417
387
  @admin.register(DocumentChunk)
418
- class DocumentChunkAdmin(ModelAdmin):
419
- """Admin interface for DocumentChunk model with Unfold styling."""
388
+ class DocumentChunkAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin):
389
+ """Admin interface for DocumentChunk model using Django Admin Utilities."""
390
+
391
+ # Performance optimization
392
+ select_related_fields = ['document', 'user']
420
393
 
421
394
  list_display = [
422
- 'chunk_display', 'document_link', 'user', 'token_count_display',
423
- 'embedding_status', 'embedding_cost_display', 'created_at'
395
+ 'chunk_display', 'document_display', 'user_display', 'token_count_display',
396
+ 'embedding_status', 'embedding_cost_display', 'created_at_display'
424
397
  ]
425
- ordering = ['-created_at'] # Newest first
398
+ list_display_links = ['chunk_display']
399
+ ordering = ['-created_at']
426
400
  list_filter = [
427
401
  'embedding_model', 'created_at',
428
402
  ('user', AutocompleteSelectFilter),
@@ -435,27 +409,30 @@ class DocumentChunkAdmin(ModelAdmin):
435
409
  ]
436
410
 
437
411
  fieldsets = (
438
- ('Basic Information', {
439
- 'fields': ('id', 'document', 'user', 'chunk_index')
412
+ ('📄 Basic Information', {
413
+ 'fields': ('id', 'document', 'user', 'chunk_index'),
414
+ 'classes': ('tab',)
440
415
  }),
441
- ('Content', {
442
- 'fields': ('content_preview', 'content')
416
+ ('📝 Content', {
417
+ 'fields': ('content_preview', 'content'),
418
+ 'classes': ('tab',)
443
419
  }),
444
- ('Embedding Information', {
420
+ ('🔗 Embedding Information', {
445
421
  'fields': ('embedding_model', 'token_count', 'character_count', 'embedding_cost'),
422
+ 'classes': ('tab',)
446
423
  }),
447
- ('Vector Embedding', {
424
+ ('🧠 Vector Embedding', {
448
425
  'fields': ('embedding',),
449
- 'classes': ('collapse',)
426
+ 'classes': ('tab', 'collapse')
450
427
  }),
451
- ('Metadata', {
428
+ ('🔧 Metadata', {
452
429
  'fields': ('metadata',),
453
- 'classes': ('collapse',),
430
+ 'classes': ('tab', 'collapse'),
454
431
  'description': 'Auto-generated chunk metadata (read-only)'
455
432
  }),
456
- ('Timestamps', {
433
+ ('Timestamps', {
457
434
  'fields': ('created_at', 'updated_at'),
458
- 'classes': ('collapse',)
435
+ 'classes': ('tab', 'collapse')
459
436
  })
460
437
  )
461
438
 
@@ -465,19 +442,32 @@ class DocumentChunkAdmin(ModelAdmin):
465
442
 
466
443
  # Form field overrides
467
444
  formfield_overrides = {
468
- JSONField: {
469
- "widget": JSONEditorWidget,
470
- }
445
+ JSONField: {"widget": JSONEditorWidget}
471
446
  }
472
447
 
473
- def get_queryset(self, request):
474
- """Optimize queryset with select_related."""
475
- return super().get_queryset(request).select_related('document', 'user')
448
+ actions = ['regenerate_embeddings', 'clear_embeddings']
476
449
 
477
450
  @display(description="Chunk", ordering="chunk_index")
478
451
  def chunk_display(self, obj):
479
452
  """Display chunk identifier."""
480
- return f"Chunk {obj.chunk_index + 1}"
453
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.ARTICLE)
454
+ return StatusBadge.create(
455
+ text=f"Chunk {obj.chunk_index + 1}",
456
+ variant="info",
457
+ config=config
458
+ )
459
+
460
+ @display(description="Document", ordering="document__title")
461
+ def document_display(self, obj):
462
+ """Display document title."""
463
+ return obj.document.title
464
+
465
+ @display(description="User")
466
+ def user_display(self, obj):
467
+ """User display."""
468
+ if not obj.user:
469
+ return "—"
470
+ return self.display_user_simple(obj.user)
481
471
 
482
472
  @display(description="Tokens", ordering="token_count")
483
473
  def token_count_display(self, obj):
@@ -487,58 +477,56 @@ class DocumentChunkAdmin(ModelAdmin):
487
477
  return f"{tokens/1000:.1f}K"
488
478
  return str(tokens)
489
479
 
490
- @display(
491
- description="Embedding",
492
- label={
493
- True: 'success', # green for has embedding
494
- False: 'danger' # red for no embedding
495
- }
496
- )
480
+ @display(description="Embedding")
497
481
  def embedding_status(self, obj):
498
482
  """Display embedding status."""
499
483
  has_embedding = obj.embedding is not None and len(obj.embedding) > 0
500
- return has_embedding, "✓ Vectorized" if has_embedding else "✗ Not vectorized"
501
-
502
- @display(description="Document", ordering="document__title")
503
- def document_link(self, obj):
504
- """Display document title with admin link."""
505
- url = reverse('admin:django_cfg_knowbase_document_change', args=[obj.document.id])
506
- return format_html(
507
- '<a href="{}" style="text-decoration: none;">{}</a>',
508
- url,
509
- obj.document.title
510
- )
484
+ if has_embedding:
485
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.CHECK_CIRCLE)
486
+ return StatusBadge.create(text="✓ Vectorized", variant="success", config=config)
487
+ else:
488
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.ERROR)
489
+ return StatusBadge.create(text="✗ Not vectorized", variant="danger", config=config)
511
490
 
512
491
  @display(description="Cost (USD)", ordering="embedding_cost")
513
492
  def embedding_cost_display(self, obj):
514
493
  """Display embedding cost with currency formatting."""
515
- return f"${obj.embedding_cost:.6f}"
494
+ config = MoneyDisplayConfig(
495
+ currency="USD",
496
+ decimal_places=6,
497
+ show_sign=False
498
+ )
499
+ return self.display_money_amount(obj, 'embedding_cost', config)
500
+
501
+ @display(description="Created")
502
+ def created_at_display(self, obj):
503
+ """Created time with relative display."""
504
+ config = DateTimeDisplayConfig(show_relative=True)
505
+ return self.display_datetime_relative(obj, 'created_at', config)
516
506
 
517
507
  @display(description="Content Preview")
518
508
  def content_preview(self, obj):
519
509
  """Display content preview with truncation."""
520
- preview = obj.content[:200] + "..." if len(obj.content) > 200 else obj.content
521
- return format_html(
522
- '<div style="max-width: 400px; word-wrap: break-word;">{}</div>',
523
- preview
524
- )
525
-
526
- @display(description="Has Embedding", boolean=True)
527
- def has_embedding(self, obj):
528
- """Check if chunk has embedding vector."""
529
- return obj.embedding is not None and len(obj.embedding) > 0
510
+ return obj.content[:200] + "..." if len(obj.content) > 200 else obj.content
530
511
 
531
512
  @display(description="Embedding Info")
532
513
  def embedding_info(self, obj):
533
514
  """Display embedding information safely."""
534
515
  if obj.embedding is not None and len(obj.embedding) > 0:
535
- return format_html(
536
- '<span style="color: green;">✓ Vector ({} dimensions)</span>',
537
- len(obj.embedding)
538
- )
539
- return format_html(
540
- '<span style="color: red;">✗ No embedding</span>'
541
- )
516
+ return f"✓ Vector ({len(obj.embedding)} dimensions)"
517
+ return " No embedding"
518
+
519
+ @action(description="Regenerate embeddings", variant=ActionVariant.INFO)
520
+ def regenerate_embeddings(self, request, queryset):
521
+ """Regenerate embeddings for selected chunks."""
522
+ count = queryset.count()
523
+ messages.info(request, f"Regenerate embeddings functionality not implemented yet. {count} chunks selected.")
524
+
525
+ @action(description="Clear embeddings", variant=ActionVariant.WARNING)
526
+ def clear_embeddings(self, request, queryset):
527
+ """Clear embeddings for selected chunks."""
528
+ updated = queryset.update(embedding=None)
529
+ messages.warning(request, f"Cleared embeddings for {updated} chunks.")
542
530
 
543
531
  def changelist_view(self, request, extra_context=None):
544
532
  """Add chunk statistics to changelist."""
@@ -553,7 +541,6 @@ class DocumentChunkAdmin(ModelAdmin):
553
541
  avg_tokens_per_chunk=Avg('token_count')
554
542
  )
555
543
 
556
- # Model breakdown
557
544
  model_counts = dict(
558
545
  queryset.values_list('embedding_model').annotate(
559
546
  count=Count('id')
@@ -573,28 +560,30 @@ class DocumentChunkAdmin(ModelAdmin):
573
560
 
574
561
 
575
562
  @admin.register(DocumentCategory)
576
- class DocumentCategoryAdmin(ModelAdmin, ImportExportModelAdmin):
577
- """Admin interface for DocumentCategory model with Unfold styling."""
563
+ class DocumentCategoryAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ImportExportModelAdmin):
564
+ """Admin interface for DocumentCategory model using Django Admin Utilities."""
578
565
 
579
566
  # Import/Export configuration
580
567
  import_form_class = ImportForm
581
568
  export_form_class = ExportForm
582
569
 
583
570
  list_display = [
584
- 'short_uuid', 'name', 'visibility_badge', 'document_count', 'created_at'
571
+ 'short_uuid', 'name_display', 'visibility_display', 'document_count', 'created_at_display'
585
572
  ]
586
- ordering = ['-created_at'] # Newest first
573
+ list_display_links = ['name_display']
574
+ ordering = ['-created_at']
587
575
  list_filter = ['is_public', 'created_at']
588
576
  search_fields = ['name', 'description']
589
577
  readonly_fields = ['id', 'created_at', 'updated_at']
590
578
 
591
579
  fieldsets = (
592
- ('Basic Information', {
593
- 'fields': ('id', 'name', 'description', 'is_public')
580
+ ('📁 Basic Information', {
581
+ 'fields': ('id', 'name', 'description', 'is_public'),
582
+ 'classes': ('tab',)
594
583
  }),
595
- ('Timestamps', {
584
+ ('Timestamps', {
596
585
  'fields': ('created_at', 'updated_at'),
597
- 'classes': ('collapse',)
586
+ 'classes': ('tab', 'collapse')
598
587
  })
599
588
  )
600
589
 
@@ -604,36 +593,54 @@ class DocumentCategoryAdmin(ModelAdmin, ImportExportModelAdmin):
604
593
 
605
594
  # Form field overrides
606
595
  formfield_overrides = {
607
- models.TextField: {
608
- "widget": WysiwygWidget,
609
- }
596
+ models.TextField: {"widget": WysiwygWidget}
610
597
  }
611
598
 
612
- @display(
613
- description="Visibility",
614
- ordering="is_public",
615
- label={
616
- 'public': 'success',
617
- 'private': 'danger'
618
- }
619
- )
620
- def visibility_badge(self, obj):
621
- """Display visibility status with color coding."""
622
- status = 'public' if obj.is_public else 'private'
623
- label = 'Public' if obj.is_public else 'Private'
624
- return status, label
599
+ actions = ['make_public', 'make_private']
600
+
601
+ @display(description="Category Name")
602
+ def name_display(self, obj):
603
+ """Display category name."""
604
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.FOLDER)
605
+ return StatusBadge.create(
606
+ text=obj.name,
607
+ variant="primary",
608
+ config=config
609
+ )
610
+
611
+ @display(description="Visibility")
612
+ def visibility_display(self, obj):
613
+ """Display visibility status."""
614
+ if obj.is_public:
615
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.PUBLIC)
616
+ return StatusBadge.create(text="Public", variant="success", config=config)
617
+ else:
618
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.LOCK)
619
+ return StatusBadge.create(text="Private", variant="danger", config=config)
625
620
 
626
621
  @display(description="Documents", ordering="document_count")
627
622
  def document_count(self, obj):
628
623
  """Display count of documents in this category."""
629
624
  count = obj.documents.count()
630
- if count > 0:
631
- url = f"/admin/knowbase/document/?categories__id__exact={obj.id}"
632
- return format_html(
633
- '<a href="{}" style="text-decoration: none;">{} documents</a>',
634
- url, count
635
- )
636
- return "0 documents"
625
+ return f"{count} documents"
626
+
627
+ @display(description="Created")
628
+ def created_at_display(self, obj):
629
+ """Created time with relative display."""
630
+ config = DateTimeDisplayConfig(show_relative=True)
631
+ return self.display_datetime_relative(obj, 'created_at', config)
632
+
633
+ @action(description="Make public", variant=ActionVariant.SUCCESS)
634
+ def make_public(self, request, queryset):
635
+ """Make selected categories public."""
636
+ updated = queryset.update(is_public=True)
637
+ messages.success(request, f"Made {updated} categories public.")
638
+
639
+ @action(description="Make private", variant=ActionVariant.WARNING)
640
+ def make_private(self, request, queryset):
641
+ """Make selected categories private."""
642
+ updated = queryset.update(is_public=False)
643
+ messages.warning(request, f"Made {updated} categories private.")
637
644
 
638
645
  def get_queryset(self, request):
639
646
  """Optimize queryset with prefetch_related."""