django-cfg 1.3.7__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 (251) 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/config.py +1 -1
  97. django_cfg/core/config.py +10 -5
  98. django_cfg/core/generation.py +1 -1
  99. django_cfg/management/commands/__init__.py +13 -1
  100. django_cfg/management/commands/app_agent_diagnose.py +470 -0
  101. django_cfg/management/commands/app_agent_generate.py +342 -0
  102. django_cfg/management/commands/app_agent_info.py +308 -0
  103. django_cfg/management/commands/migrate_all.py +9 -3
  104. django_cfg/management/commands/migrator.py +11 -6
  105. django_cfg/management/commands/rundramatiq.py +3 -2
  106. django_cfg/middleware/__init__.py +0 -2
  107. django_cfg/models/api_keys.py +115 -0
  108. django_cfg/modules/django_admin/__init__.py +64 -0
  109. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  110. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  111. django_cfg/modules/django_admin/decorators/display.py +106 -0
  112. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  113. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  114. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  115. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  116. django_cfg/modules/django_admin/models/__init__.py +20 -0
  117. django_cfg/modules/django_admin/models/action_models.py +33 -0
  118. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  119. django_cfg/modules/django_admin/models/base.py +26 -0
  120. django_cfg/modules/django_admin/models/display_models.py +31 -0
  121. django_cfg/modules/django_admin/utils/badges.py +159 -0
  122. django_cfg/modules/django_admin/utils/displays.py +247 -0
  123. django_cfg/modules/django_app_agent/__init__.py +87 -0
  124. django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
  125. django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
  126. django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
  127. django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
  128. django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
  129. django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
  130. django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
  131. django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
  132. django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
  133. django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
  134. django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
  135. django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
  136. django_cfg/modules/django_app_agent/core/__init__.py +33 -0
  137. django_cfg/modules/django_app_agent/core/config.py +300 -0
  138. django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
  139. django_cfg/modules/django_app_agent/models/__init__.py +71 -0
  140. django_cfg/modules/django_app_agent/models/base.py +283 -0
  141. django_cfg/modules/django_app_agent/models/context.py +496 -0
  142. django_cfg/modules/django_app_agent/models/enums.py +481 -0
  143. django_cfg/modules/django_app_agent/models/requests.py +500 -0
  144. django_cfg/modules/django_app_agent/models/responses.py +585 -0
  145. django_cfg/modules/django_app_agent/pytest.ini +6 -0
  146. django_cfg/modules/django_app_agent/services/__init__.py +42 -0
  147. django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
  148. django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
  149. django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
  150. django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
  151. django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
  152. django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
  153. django_cfg/modules/django_app_agent/services/base.py +437 -0
  154. django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
  155. django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
  156. django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
  157. django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
  158. django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
  159. django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
  160. django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
  161. django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
  162. django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
  163. django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
  164. django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
  165. django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
  166. django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
  167. django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
  168. django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
  169. django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
  170. django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
  171. django_cfg/modules/django_app_agent/services/report_service.py +332 -0
  172. django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
  173. django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
  174. django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
  175. django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
  176. django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
  177. django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
  178. django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
  179. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
  180. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
  181. django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
  182. django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
  183. django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
  184. django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
  185. django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
  186. django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
  187. django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
  188. django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
  189. django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
  190. django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
  191. django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
  192. django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
  193. django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
  194. django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
  195. django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
  196. django_cfg/modules/django_app_agent/ui/cli.py +419 -0
  197. django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
  198. django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
  199. django_cfg/modules/django_app_agent/utils/logging.py +360 -0
  200. django_cfg/modules/django_app_agent/utils/validation.py +417 -0
  201. django_cfg/modules/django_currency/__init__.py +2 -2
  202. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  203. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  204. django_cfg/modules/django_currency/core/converter.py +12 -12
  205. django_cfg/modules/django_currency/database/__init__.py +2 -2
  206. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  207. django_cfg/modules/django_llm/llm/client.py +10 -2
  208. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  209. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  210. django_cfg/modules/django_unfold/dashboard.py +14 -13
  211. django_cfg/modules/django_unfold/models/config.py +1 -1
  212. django_cfg/registry/core.py +3 -0
  213. django_cfg/registry/third_party.py +2 -2
  214. django_cfg/template_archive/django_sample.zip +0 -0
  215. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
  216. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/RECORD +223 -117
  217. django_cfg/apps/accounts/admin/activity.py +0 -96
  218. django_cfg/apps/accounts/admin/group.py +0 -17
  219. django_cfg/apps/accounts/admin/otp.py +0 -59
  220. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  221. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  222. django_cfg/apps/accounts/admin/user.py +0 -300
  223. django_cfg/apps/agents/core/agent.py +0 -281
  224. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  225. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  226. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  227. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  228. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  229. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  230. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  231. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  232. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  233. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  234. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  235. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  236. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  237. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  238. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  239. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  240. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  241. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  242. django_cfg/apps/tasks/admin.py +0 -320
  243. django_cfg/middleware/static_nocache.py +0 -55
  244. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  245. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  246. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  247. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  248. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  249. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
  250. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
  251. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,12 @@
1
1
  """
2
- Admin interface for archive models with Unfold optimization.
2
+ Archive admin interfaces using Django Admin Utilities.
3
+
4
+ Enhanced archive management with Material Icons and optimized queries.
3
5
  """
4
6
 
5
7
  import hashlib
6
8
  import logging
7
9
  from django.contrib import admin, messages
8
- from django.utils.html import format_html
9
10
  from django.urls import reverse
10
11
  from django.utils.safestring import mark_safe
11
12
  from django.db import models
@@ -14,12 +15,23 @@ from django.db.models.fields.json import JSONField
14
15
  from django.shortcuts import redirect
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
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.archive import DocumentArchive, ArchiveItem, ArchiveItemChunk
24
36
 
25
37
  logger = logging.getLogger(__name__)
@@ -32,20 +44,17 @@ class ArchiveItemInline(TabularInline):
32
44
  verbose_name = "Archive Item"
33
45
  verbose_name_plural = "📁 Archive Items (Read-only)"
34
46
  extra = 0
35
- max_num = 0 # No new items allowed
36
- can_delete = False # Prevent deletion through inline
37
- show_change_link = True # Allow viewing individual items
47
+ max_num = 0
48
+ can_delete = False
49
+ show_change_link = True
38
50
 
39
51
  def has_add_permission(self, request, obj=None):
40
- """Disable adding new items through inline."""
41
52
  return False
42
53
 
43
54
  def has_change_permission(self, request, obj=None):
44
- """Disable editing items through inline."""
45
55
  return False
46
56
 
47
57
  def has_delete_permission(self, request, obj=None):
48
- """Disable deleting items through inline."""
49
58
  return False
50
59
 
51
60
  fields = [
@@ -57,9 +66,8 @@ class ArchiveItemInline(TabularInline):
57
66
  'is_processable', 'chunks_count', 'created_at'
58
67
  ]
59
68
 
60
- # Unfold specific options
61
- hide_title = False # Show titles for better UX
62
- classes = ['collapse'] # Collapsed by default
69
+ hide_title = False
70
+ classes = ['collapse']
63
71
 
64
72
  @display(description="File Size")
65
73
  def file_size_display_inline(self, obj):
@@ -77,15 +85,19 @@ class ArchiveItemInline(TabularInline):
77
85
 
78
86
 
79
87
  @admin.register(DocumentArchive)
80
- class DocumentArchiveAdmin(ExportMixin, ModelAdmin):
81
- """Admin interface for DocumentArchive."""
88
+ class DocumentArchiveAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ExportMixin):
89
+ """Admin interface for DocumentArchive using Django Admin Utilities."""
90
+
91
+ # Performance optimization
92
+ select_related_fields = ['user']
82
93
 
83
94
  list_display = [
84
- 'title_display', 'user', 'archive_type_badge', 'status_badge',
95
+ 'title_display', 'user_display', 'archive_type_display', 'status_display',
85
96
  'items_count', 'chunks_count', 'vectorization_progress', 'file_size_display',
86
- 'progress_display', 'created_at'
97
+ 'progress_display', 'created_at_display'
87
98
  ]
88
- ordering = ['-created_at'] # Newest first
99
+ list_display_links = ['title_display']
100
+ ordering = ['-created_at']
89
101
  inlines = [ArchiveItemInline]
90
102
  list_filter = [
91
103
  'processing_status', 'archive_type', 'is_public',
@@ -98,40 +110,33 @@ class DocumentArchiveAdmin(ExportMixin, ModelAdmin):
98
110
  'id', 'user', 'content_hash', 'original_filename', 'file_size', 'archive_type',
99
111
  'processing_status', 'processed_at', 'processing_duration_ms',
100
112
  'processing_error', 'total_items', 'processed_items', 'total_chunks',
101
- 'vectorized_chunks', 'total_tokens', 'total_cost_usd', 'created_at',
102
- 'updated_at', 'progress_display', 'vectorization_progress_display',
103
- 'items_link', 'chunks_link'
113
+ 'vectorized_chunks', 'total_cost_usd', 'created_at', 'updated_at'
104
114
  ]
115
+
105
116
  fieldsets = (
106
- ('Basic Information', {
107
- 'fields': ('id', 'title', 'description', 'user', 'categories', 'is_public')
117
+ ('📁 Archive Info', {
118
+ 'fields': ('id', 'title', 'description', 'user', 'categories', 'is_public'),
119
+ 'classes': ('tab',)
108
120
  }),
109
- ('Archive Details', {
110
- 'fields': (
111
- 'archive_file', 'original_filename', 'file_size', 'archive_type',
112
- 'content_hash'
113
- )
121
+ ('📄 File Details', {
122
+ 'fields': ('original_filename', 'file_size', 'archive_type', 'content_hash'),
123
+ 'classes': ('tab',)
114
124
  }),
115
- ('Processing Status', {
125
+ ('⚙️ Processing Status', {
116
126
  'fields': (
117
127
  'processing_status', 'processed_at', 'processing_duration_ms',
118
- 'processing_error', 'progress_display',
119
- 'vectorization_progress_display'
120
- )
128
+ 'processing_error'
129
+ ),
130
+ 'classes': ('tab',)
121
131
  }),
122
- ('Statistics', {
123
- 'fields': (
124
- 'total_items', 'processed_items', 'total_chunks',
125
- 'vectorized_chunks', 'total_tokens', 'total_cost_usd'
126
- )
127
- }),
128
- ('Related Data', {
129
- 'fields': ('items_link', 'chunks_link')
132
+ ('📊 Statistics', {
133
+ 'fields': ('total_items', 'processed_items', 'total_chunks', 'vectorized_chunks', 'total_cost_usd'),
134
+ 'classes': ('tab',)
130
135
  }),
131
- ('Timestamps', {
136
+ ('Timestamps', {
132
137
  'fields': ('created_at', 'updated_at'),
133
- 'classes': ('collapse',)
134
- }),
138
+ 'classes': ('tab', 'collapse')
139
+ })
135
140
  )
136
141
  filter_horizontal = ['categories']
137
142
 
@@ -141,485 +146,259 @@ class DocumentArchiveAdmin(ExportMixin, ModelAdmin):
141
146
 
142
147
  # Form field overrides
143
148
  formfield_overrides = {
144
- models.TextField: {
145
- "widget": WysiwygWidget,
146
- },
147
- JSONField: {
148
- "widget": JSONEditorWidget,
149
- }
149
+ models.TextField: {"widget": WysiwygWidget},
150
+ JSONField: {"widget": JSONEditorWidget}
150
151
  }
151
152
 
153
+ actions = ['reprocess_archives', 'mark_as_public', 'mark_as_private']
154
+
152
155
  @display(description="Archive Title", ordering="title")
153
156
  def title_display(self, obj):
154
- """Display archive title with truncation."""
157
+ """Display archive title."""
155
158
  title = obj.title or "Untitled Archive"
156
159
  if len(title) > 50:
157
160
  title = title[:47] + "..."
158
- return format_html(
159
- '<div style="font-weight: 500;">{}</div>',
160
- title
161
+
162
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.ARCHIVE)
163
+ return StatusBadge.create(
164
+ text=title,
165
+ variant="primary",
166
+ config=config
161
167
  )
162
168
 
163
- @display(
164
- description="Archive Type",
165
- ordering="archive_type",
166
- label={
167
- 'DOCUMENTS': 'info', # blue for documents
168
- 'CODE': 'success', # green for code
169
- 'MIXED': 'warning', # orange for mixed
170
- 'OTHER': 'danger' # red for other
171
- }
172
- )
173
- def archive_type_badge(self, obj):
174
- """Display archive type with color coding."""
175
- return obj.archive_type, obj.get_archive_type_display()
176
-
177
- @display(
178
- description="Status",
179
- ordering="processing_status",
180
- label={
181
- 'pending': 'warning', # orange for pending
182
- 'processing': 'info', # blue for processing
183
- 'completed': 'success', # green for completed
184
- 'failed': 'danger', # red for failed
185
- 'cancelled': 'secondary' # gray for cancelled
169
+ @display(description="User")
170
+ def user_display(self, obj):
171
+ """User display."""
172
+ if not obj.user:
173
+ return "—"
174
+ return self.display_user_simple(obj.user)
175
+
176
+ @display(description="Archive Type")
177
+ def archive_type_display(self, obj):
178
+ """Display archive type with badge."""
179
+ if not obj.archive_type:
180
+ return ""
181
+
182
+ type_variants = {
183
+ 'zip': 'info',
184
+ 'tar': 'warning',
185
+ 'rar': 'secondary',
186
+ '7z': 'primary'
186
187
  }
187
- )
188
- def status_badge(self, obj):
189
- """Display processing status with color coding."""
190
- return obj.processing_status, obj.get_processing_status_display()
188
+ variant = type_variants.get(obj.archive_type.lower(), 'secondary')
189
+
190
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.FOLDER_ZIP)
191
+ return StatusBadge.create(
192
+ text=obj.archive_type.upper(),
193
+ variant=variant,
194
+ config=config
195
+ )
191
196
 
192
- @display(description="File Size", ordering="file_size")
193
- def file_size_display(self, obj):
194
- """Display file size in human readable format."""
195
- size = obj.file_size
196
- for unit in ['B', 'KB', 'MB', 'GB']:
197
- if size < 1024.0:
198
- return f"{size:.1f} {unit}"
199
- size /= 1024.0
200
- return f"{size:.1f} TB"
197
+ @display(description="Status")
198
+ def status_display(self, obj):
199
+ """Display processing status."""
200
+ status_config = StatusBadgeConfig(
201
+ custom_mappings={
202
+ 'pending': 'warning',
203
+ 'processing': 'info',
204
+ 'completed': 'success',
205
+ 'failed': 'danger',
206
+ 'cancelled': 'secondary'
207
+ },
208
+ show_icons=True,
209
+ icon=Icons.CHECK_CIRCLE if obj.processing_status == 'completed' else Icons.ERROR if obj.processing_status == 'failed' else Icons.SCHEDULE
210
+ )
211
+ return self.display_status_auto(obj, 'processing_status', status_config)
201
212
 
202
213
  @display(description="Items", ordering="total_items")
203
214
  def items_count(self, obj):
204
- """Display items count with link."""
205
- count = obj.total_items
206
- if count > 0:
207
- url = f"/admin/knowbase/archiveitem/?archive__id__exact={obj.id}"
208
- return format_html(
209
- '<a href="{}" style="text-decoration: none;">{} items</a>',
210
- url, count
211
- )
212
- return "0 items"
215
+ """Display items count."""
216
+ total = obj.total_items or 0
217
+ processed = obj.processed_items or 0
218
+ return f"{processed}/{total} items"
213
219
 
214
220
  @display(description="Chunks", ordering="total_chunks")
215
221
  def chunks_count(self, obj):
216
- """Display chunks count with link."""
217
- count = obj.total_chunks
218
- if count > 0:
219
- url = f"/admin/knowbase/archiveitemchunk/?archive__id__exact={obj.id}"
220
- return format_html(
221
- '<a href="{}" style="text-decoration: none;">{} chunks</a>',
222
- url, count
223
- )
224
- return "0 chunks"
225
-
226
- @display(
227
- description="Vectorization",
228
- label={
229
- 'completed': 'success', # green for 100%
230
- 'partial': 'warning', # orange for partial
231
- 'none': 'danger', # red for 0%
232
- 'no_chunks': 'info' # blue for no chunks
233
- }
234
- )
222
+ """Display chunks count."""
223
+ total = obj.total_chunks or 0
224
+ vectorized = obj.vectorized_chunks or 0
225
+ return f"{vectorized}/{total} chunks"
226
+
227
+ @display(description="Vectorization")
235
228
  def vectorization_progress(self, obj):
236
- """Display vectorization progress with color coding."""
237
- try:
238
- # Check processing status first
239
- if obj.processing_status == 'pending':
240
- return 'no_chunks', 'Pending'
241
- elif obj.processing_status == 'processing':
242
- return 'partial', 'Processing...'
243
- elif obj.processing_status == 'failed':
244
- return 'none', 'Failed'
245
-
246
- progress = DocumentArchive.objects.get_vectorization_progress(obj.id)
247
- total = progress['total']
248
- vectorized = progress['vectorized']
249
- percentage = progress['percentage']
250
-
251
- if total == 0:
252
- return 'no_chunks', 'No chunks'
253
- elif percentage == 100:
254
- return 'completed', f'{vectorized}/{total} (100%)'
255
- elif percentage > 0:
256
- return 'partial', f'{vectorized}/{total} ({percentage}%)'
257
- else:
258
- return 'none', f'{vectorized}/{total} (0%)'
259
- except Exception as e:
260
- # Log the error for debugging
261
- import logging
262
- logger = logging.getLogger(__name__)
263
- logger.error(f"Error getting vectorization progress for archive {obj.id}: {e}")
264
- return 'no_chunks', 'Not ready'
265
-
266
- @display(description="Overall Progress")
267
- def progress_display(self, obj):
268
- """Display overall progress including processing and vectorization."""
269
- from ..models.base import ProcessingStatus
229
+ """Display vectorization progress."""
230
+ total = obj.total_chunks or 0
231
+ vectorized = obj.vectorized_chunks or 0
270
232
 
271
- # Calculate overall progress: 50% for processing, 50% for vectorization
272
- processing_progress = obj.processing_progress
233
+ if total == 0:
234
+ return "No chunks"
273
235
 
274
- # Get vectorization progress
275
- try:
276
- vectorization_stats = DocumentArchive.objects.get_vectorization_progress(obj.id)
277
- vectorization_progress = vectorization_stats.get('percentage', 0)
278
- except Exception:
279
- vectorization_progress = 0
236
+ percentage = (vectorized / total) * 100
280
237
 
281
- # Overall progress: processing (50%) + vectorization (50%)
282
- overall_progress = (processing_progress * 0.5) + (vectorization_progress * 0.5)
283
-
284
- # Determine color based on status and progress
285
- if obj.processing_status == ProcessingStatus.COMPLETED and vectorization_progress == 100:
286
- color_class = "bg-green-500" # Fully complete
287
- elif obj.processing_status == ProcessingStatus.FAILED:
288
- color_class = "bg-red-500" # Failed
289
- elif overall_progress > 0:
290
- color_class = "bg-blue-500" # In progress
238
+ if percentage == 100:
239
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.CHECK_CIRCLE)
240
+ return StatusBadge.create(text="100%", variant="success", config=config)
241
+ elif percentage > 0:
242
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.SCHEDULE)
243
+ return StatusBadge.create(text=f"{percentage:.1f}%", variant="warning", config=config)
291
244
  else:
292
- color_class = "bg-gray-300" # Not started
293
-
294
- return format_html(
295
- '<div class="w-24 bg-gray-200 rounded-full h-2 dark:bg-gray-700">'
296
- '<div class="{} h-2 rounded-full transition-all duration-300" style="width: {}%"></div>'
297
- '</div>'
298
- '<span class="text-xs text-gray-600 dark:text-gray-400 ml-2">{}%</span>',
299
- color_class, overall_progress, int(overall_progress)
300
- )
245
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.ERROR)
246
+ return StatusBadge.create(text="0%", variant="danger", config=config)
301
247
 
302
- def vectorization_progress_display(self, obj: DocumentArchive) -> str:
303
- """Display vectorization progress with progress bar."""
304
- progress = obj.vectorization_progress
305
- color = 'green' if progress == 100 else 'blue' if progress > 0 else 'gray'
306
-
307
- return format_html(
308
- '<div style="width: 100px; background-color: #f0f0f0; border-radius: 3px;">'
309
- '<div style="width: {}%; height: 20px; background-color: {}; border-radius: 3px; '
310
- 'text-align: center; line-height: 20px; color: white; font-size: 12px;">'
311
- '{}%</div></div>',
312
- progress, color, round(progress, 1)
313
- )
314
- vectorization_progress_display.short_description = "Vectorization Progress"
315
-
316
- def items_link(self, obj: DocumentArchive) -> str:
317
- """Link to archive items."""
318
- if obj.pk:
319
- url = reverse('admin:django_cfg_knowbase_archiveitem_changelist')
320
- return format_html(
321
- '<a href="{}?archive__id__exact={}">View {} Items</a>',
322
- url, obj.pk, obj.total_items
323
- )
324
- return "No items yet"
325
- items_link.short_description = "Items"
326
-
327
- def chunks_link(self, obj: DocumentArchive) -> str:
328
- """Link to archive chunks."""
329
- if obj.pk:
330
- url = reverse('admin:django_cfg_knowbase_archiveitemchunk_changelist')
331
- return format_html(
332
- '<a href="{}?archive__id__exact={}">View {} Chunks</a>',
333
- url, obj.pk, obj.total_chunks
334
- )
335
- return "No chunks yet"
336
- chunks_link.short_description = "Chunks"
248
+ @display(description="File Size", ordering="file_size")
249
+ def file_size_display(self, obj):
250
+ """Display file size in human readable format."""
251
+ size = obj.file_size
252
+ for unit in ['B', 'KB', 'MB']:
253
+ if size < 1024.0:
254
+ return f"{size:.1f} {unit}"
255
+ size /= 1024.0
256
+ return f"{size:.1f} GB"
337
257
 
338
- def get_queryset(self, request):
339
- """Optimize queryset with select_related."""
340
- # Use all_users() to show all archives in admin, then filter by user if needed
341
- queryset = DocumentArchive.objects.all_users().select_related('user').prefetch_related('categories')
342
-
343
- # For non-superusers, filter by their own archives
344
- if not request.user.is_superuser:
345
- queryset = queryset.filter(user=request.user)
258
+ @display(description="Progress")
259
+ def progress_display(self, obj):
260
+ """Display overall progress."""
261
+ total_items = obj.total_items or 0
262
+ processed_items = obj.processed_items or 0
346
263
 
347
- return queryset
348
-
349
- def save_model(self, request, obj, form, change):
350
- """Automatically set user and file metadata when creating new archives."""
351
- if not change: # Only for new archives
352
- obj.user = request.user
353
-
354
- # Auto-populate metadata from uploaded file
355
- if obj.archive_file:
356
- import hashlib
357
- import os
358
-
359
- # Set original filename
360
- if not obj.original_filename:
361
- obj.original_filename = obj.archive_file.name
362
-
363
- # Set file size
364
- if not obj.file_size and hasattr(obj.archive_file, 'size'):
365
- obj.file_size = obj.archive_file.size
366
-
367
- # Check for duplicates using manager method
368
- is_duplicate, existing_archive = DocumentArchive.objects.check_duplicate_before_save(
369
- user=obj.user,
370
- title=obj.title,
371
- file_size=obj.file_size
372
- )
373
-
374
- if is_duplicate and existing_archive:
375
- messages.error(
376
- request,
377
- f'❌ An archive with the same title and file size already exists: "{existing_archive.title}" '
378
- f'(created {existing_archive.created_at.strftime("%Y-%m-%d %H:%M")}). '
379
- f'Please use a different title or check if this is a duplicate upload.'
380
- )
381
- # Don't save, just return - this will keep the form open with the error
382
- return
383
-
384
- # Set archive type based on file extension
385
- if not obj.archive_type:
386
- filename = obj.archive_file.name.lower()
387
-
388
- # ZIP formats
389
- if filename.endswith(('.zip', '.jar', '.war', '.ear')):
390
- obj.archive_type = 'zip'
391
- # TAR.GZ formats
392
- elif filename.endswith(('.tar.gz', '.tgz')):
393
- obj.archive_type = 'tar.gz'
394
- # TAR.BZ2 formats
395
- elif filename.endswith(('.tar.bz2', '.tbz2', '.tar.bzip2')):
396
- obj.archive_type = 'tar.bz2'
397
- # TAR formats
398
- elif filename.endswith('.tar'):
399
- obj.archive_type = 'tar'
400
- else:
401
- # Default to zip for unknown formats
402
- obj.archive_type = 'zip'
403
-
404
- # Generate content hash
405
- if not obj.content_hash and hasattr(obj.archive_file, 'read'):
406
- obj.archive_file.seek(0)
407
- content = obj.archive_file.read()
408
- obj.content_hash = hashlib.sha256(content).hexdigest()
409
- obj.archive_file.seek(0)
264
+ if total_items == 0:
265
+ return "No items"
410
266
 
411
- super().save_model(request, obj, form, change)
267
+ percentage = (processed_items / total_items) * 100
268
+ return f"{percentage:.1f}%"
269
+
270
+ @display(description="Created")
271
+ def created_at_display(self, obj):
272
+ """Created time with relative display."""
273
+ config = DateTimeDisplayConfig(show_relative=True)
274
+ return self.display_datetime_relative(obj, 'created_at', config)
275
+
276
+ @action(description="Reprocess archives", variant=ActionVariant.INFO)
277
+ def reprocess_archives(self, request, queryset):
278
+ """Reprocess selected archives."""
279
+ count = queryset.count()
280
+ messages.info(request, f"Reprocess functionality not implemented yet. {count} archives selected.")
281
+
282
+ @action(description="Mark as public", variant=ActionVariant.SUCCESS)
283
+ def mark_as_public(self, request, queryset):
284
+ """Mark selected archives as public."""
285
+ updated = queryset.update(is_public=True)
286
+ messages.success(request, f"Marked {updated} archives as public.")
287
+
288
+ @action(description="Mark as private", variant=ActionVariant.WARNING)
289
+ def mark_as_private(self, request, queryset):
290
+ """Mark selected archives as private."""
291
+ updated = queryset.update(is_public=False)
292
+ messages.warning(request, f"Marked {updated} archives as private.")
412
293
 
413
294
  def changelist_view(self, request, extra_context=None):
414
- """Add summary statistics to changelist."""
295
+ """Add archive statistics to changelist."""
415
296
  extra_context = extra_context or {}
416
297
 
417
- # Get summary statistics
418
298
  queryset = self.get_queryset(request)
419
299
  stats = queryset.aggregate(
420
300
  total_archives=Count('id'),
301
+ completed_archives=Count('id', filter=Q(processing_status='completed')),
421
302
  total_items=Sum('total_items'),
422
303
  total_chunks=Sum('total_chunks'),
423
304
  total_cost=Sum('total_cost_usd')
424
305
  )
425
306
 
426
- # Status breakdown
427
- status_counts = dict(
428
- queryset.values_list('processing_status').annotate(
429
- count=Count('id')
430
- )
431
- )
432
-
433
307
  extra_context['archive_stats'] = {
434
308
  'total_archives': stats['total_archives'] or 0,
309
+ 'completed_archives': stats['completed_archives'] or 0,
435
310
  'total_items': stats['total_items'] or 0,
436
311
  'total_chunks': stats['total_chunks'] or 0,
437
- 'total_cost': f"${(stats['total_cost'] or 0):.6f}",
438
- 'status_counts': status_counts
312
+ 'total_cost': f"${(stats['total_cost'] or 0):.6f}"
439
313
  }
440
314
 
441
315
  return super().changelist_view(request, extra_context)
442
-
443
- # Actions for detail view
444
- actions_detail = ["reprocess_archive"]
445
-
446
- @action(
447
- description="🔄 Reprocess Archive",
448
- icon="refresh",
449
- variant=ActionVariant.WARNING
450
- )
451
- def reprocess_archive(self, request, object_id):
452
- """Force reprocessing of the archive."""
453
- try:
454
- # Get the archive object to get its title for the message
455
- archive = self.get_object(request, object_id)
456
- if not archive:
457
- self.message_user(request, "Archive not found.", level='error')
458
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
459
-
460
- # Use custom manager method to reprocess
461
- DocumentArchive.objects.reprocess(object_id)
462
-
463
- self.message_user(
464
- request,
465
- f"Archive '{archive.title}' has been reset and queued for reprocessing.",
466
- level='success'
467
- )
468
-
469
- except ValueError as e:
470
- self.message_user(
471
- request,
472
- f"Error reprocessing archive: {e}",
473
- level='error'
474
- )
475
- except Exception as e:
476
- logger.exception(f"Unexpected error reprocessing archive {object_id}")
477
- self.message_user(
478
- request,
479
- f"Unexpected error reprocessing archive: {e}",
480
- level='error'
481
- )
482
-
483
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
484
316
 
485
317
 
486
318
  @admin.register(ArchiveItem)
487
- class ArchiveItemAdmin(ExportMixin, ModelAdmin):
488
- """Admin interface for ArchiveItem."""
319
+ class ArchiveItemAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ExportMixin):
320
+ """Admin interface for ArchiveItem using Django Admin Utilities."""
321
+
322
+ # Performance optimization
323
+ select_related_fields = ['archive', 'user']
489
324
 
490
325
  list_display = [
491
- 'item_name_display', 'archive_link', 'content_type_badge', 'language_badge',
492
- 'processable_badge', 'chunks_count_display', 'file_size_display', 'created_at'
326
+ 'item_name_display', 'archive_display', 'user_display', 'content_type_display',
327
+ 'file_size_display', 'processable_display', 'chunks_count_display', 'created_at_display'
493
328
  ]
494
- ordering = ['-created_at'] # Newest first
329
+ list_display_links = ['item_name_display']
330
+ ordering = ['-created_at']
495
331
  list_filter = [
496
- 'content_type', 'is_processable', 'language', 'created_at',
497
- 'archive__processing_status',
498
- ('archive', AutocompleteSelectFilter)
499
- ]
500
- search_fields = [
501
- 'item_name', 'relative_path', 'archive__title',
502
- 'language', 'archive__user__username'
332
+ 'content_type', 'is_processable', 'created_at',
333
+ ('archive', AutocompleteSelectFilter),
334
+ ('user', AutocompleteSelectFilter)
503
335
  ]
504
- autocomplete_fields = ['archive']
336
+ search_fields = ['item_name', 'content_type', 'archive__title', 'user__username']
337
+ autocomplete_fields = ['archive', 'user']
505
338
  readonly_fields = [
506
- 'id', 'user', 'content_hash', 'chunks_count', 'total_tokens',
507
- 'processing_cost', 'created_at', 'updated_at',
508
- 'archive_link', 'chunks_link', 'content_preview'
339
+ 'id', 'user', 'file_size', 'content_type', 'is_processable',
340
+ 'chunks_count', 'created_at', 'updated_at'
509
341
  ]
342
+
510
343
  fieldsets = (
511
- ('Basic Information', {
512
- 'fields': ('id', 'archive_link', 'user', 'relative_path', 'item_name')
513
- }),
514
- ('File Details', {
515
- 'fields': (
516
- 'item_type', 'content_type', 'file_size',
517
- 'content_hash', 'language', 'encoding'
518
- )
344
+ ('📄 Item Info', {
345
+ 'fields': ('id', 'item_name', 'archive', 'user'),
346
+ 'classes': ('tab',)
519
347
  }),
520
- ('Processing', {
521
- 'fields': (
522
- 'is_processable', 'chunks_count', 'total_tokens',
523
- 'processing_cost'
524
- )
525
- }),
526
- ('Content', {
527
- 'fields': ('content_preview',),
528
- 'classes': ('collapse',)
348
+ ('📁 File Details', {
349
+ 'fields': ('content_type', 'file_size', 'is_processable'),
350
+ 'classes': ('tab',)
529
351
  }),
530
- ('Metadata', {
531
- 'fields': ('metadata',),
532
- 'classes': ('collapse',)
352
+ ('📊 Processing', {
353
+ 'fields': ('chunks_count',),
354
+ 'classes': ('tab',)
533
355
  }),
534
- ('Related Data', {
535
- 'fields': ('chunks_link',)
536
- }),
537
- ('Timestamps', {
356
+ (' Timestamps', {
538
357
  'fields': ('created_at', 'updated_at'),
539
- 'classes': ('collapse',)
540
- }),
358
+ 'classes': ('tab', 'collapse')
359
+ })
541
360
  )
542
361
 
543
- # Unfold configuration
544
- compressed_fields = True
545
- warn_unsaved_form = True
546
-
547
- # Form field overrides
548
- formfield_overrides = {
549
- models.TextField: {
550
- "widget": WysiwygWidget,
551
- },
552
- JSONField: {
553
- "widget": JSONEditorWidget,
554
- }
555
- }
362
+ actions = ['mark_as_processable', 'mark_as_not_processable']
556
363
 
557
364
  @display(description="Item Name", ordering="item_name")
558
365
  def item_name_display(self, obj):
559
- """Display item name with truncation."""
560
- name = obj.item_name or "Unnamed Item"
561
- if len(name) > 40:
562
- name = name[:37] + "..."
563
- return format_html(
564
- '<div style="font-weight: 500;">{}</div>',
565
- name
366
+ """Display item name."""
367
+ name = obj.item_name
368
+ if len(name) > 50:
369
+ name = name[:47] + "..."
370
+
371
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.INSERT_DRIVE_FILE)
372
+ return StatusBadge.create(
373
+ text=name,
374
+ variant="primary",
375
+ config=config
566
376
  )
567
377
 
568
378
  @display(description="Archive", ordering="archive__title")
569
- def archive_link(self, obj):
570
- """Link to parent archive."""
571
- if obj.archive:
572
- url = reverse('admin:django_cfg_knowbase_documentarchive_change', args=[obj.archive.pk])
573
- title = obj.archive.title[:30] + "..." if len(obj.archive.title) > 30 else obj.archive.title
574
- return format_html('<a href="{}" style="text-decoration: none;">{}</a>', url, title)
575
- return "No archive"
576
-
577
- @display(
578
- description="Content Type",
579
- ordering="content_type",
580
- label={
581
- 'TEXT': 'info', # blue for text
582
- 'CODE': 'success', # green for code
583
- 'BINARY': 'warning', # orange for binary
584
- 'OTHER': 'secondary' # gray for other
585
- }
586
- )
587
- def content_type_badge(self, obj):
588
- """Display content type with color coding."""
589
- return obj.content_type, obj.get_content_type_display()
590
-
591
- @display(
592
- description="Language",
593
- ordering="language",
594
- label=True
595
- )
596
- def language_badge(self, obj):
597
- """Display language with badge."""
598
- return obj.language or "Unknown"
599
-
600
- @display(
601
- description="Processable",
602
- ordering="is_processable",
603
- label={
604
- True: 'success', # green for processable
605
- False: 'danger' # red for not processable
606
- }
607
- )
608
- def processable_badge(self, obj):
609
- """Display processable status."""
610
- return obj.is_processable, "Yes" if obj.is_processable else "No"
611
-
612
- @display(description="Chunks", ordering="chunks_count")
613
- def chunks_count_display(self, obj):
614
- """Display chunks count with link."""
615
- count = obj.chunks_count
616
- if count > 0:
617
- url = f"/admin/knowbase/archiveitemchunk/?item__id__exact={obj.id}"
618
- return format_html(
619
- '<a href="{}" style="text-decoration: none;">{} chunks</a>',
620
- url, count
621
- )
622
- return "0 chunks"
379
+ def archive_display(self, obj):
380
+ """Display archive title."""
381
+ return obj.archive.title or "Untitled Archive"
382
+
383
+ @display(description="User")
384
+ def user_display(self, obj):
385
+ """User display."""
386
+ if not obj.user:
387
+ return "—"
388
+ return self.display_user_simple(obj.user)
389
+
390
+ @display(description="Content Type")
391
+ def content_type_display(self, obj):
392
+ """Display content type with badge."""
393
+ if not obj.content_type:
394
+ return "—"
395
+
396
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.DESCRIPTION)
397
+ return StatusBadge.create(
398
+ text=obj.content_type,
399
+ variant="info",
400
+ config=config
401
+ )
623
402
 
624
403
  @display(description="File Size", ordering="file_size")
625
404
  def file_size_display(self, obj):
@@ -631,123 +410,112 @@ class ArchiveItemAdmin(ExportMixin, ModelAdmin):
631
410
  size /= 1024.0
632
411
  return f"{size:.1f} GB"
633
412
 
634
- def chunks_link(self, obj: ArchiveItem) -> str:
635
- """Link to item chunks."""
636
- if obj.pk:
637
- url = reverse('admin:django_cfg_knowbase_archiveitemchunk_changelist')
638
- return format_html(
639
- '<a href="{}?item__id__exact={}">View {} Chunks</a>',
640
- url, obj.pk, obj.chunks_count
641
- )
642
- return "No chunks yet"
643
- chunks_link.short_description = "Chunks"
644
-
645
- def content_preview(self, obj: ArchiveItem) -> str:
646
- """Show content preview."""
647
- if obj.raw_content:
648
- preview = obj.raw_content[:500]
649
- if len(obj.raw_content) > 500:
650
- preview += "..."
651
- return format_html('<pre style="white-space: pre-wrap;">{}</pre>', preview)
652
- return "No content"
653
- content_preview.short_description = "Content Preview"
654
-
655
- def get_queryset(self, request):
656
- """Optimize queryset with select_related."""
657
- # Use all_users() to show all archive items in admin, then filter by user if needed
658
- queryset = ArchiveItem.objects.all_users().select_related('archive', 'user')
659
-
660
- # For non-superusers, filter by their own items
661
- if not request.user.is_superuser:
662
- queryset = queryset.filter(user=request.user)
663
-
664
- return queryset
413
+ @display(description="Processable")
414
+ def processable_display(self, obj):
415
+ """Display processable status."""
416
+ if obj.is_processable:
417
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.CHECK_CIRCLE)
418
+ return StatusBadge.create(text="Yes", variant="success", config=config)
419
+ else:
420
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.CANCEL)
421
+ return StatusBadge.create(text="No", variant="danger", config=config)
665
422
 
666
- def save_model(self, request, obj, form, change):
667
- """Automatically set user to current user when creating new archive items."""
668
- if not change: # Only for new items
669
- obj.user = request.user
670
- super().save_model(request, obj, form, change)
423
+ @display(description="Chunks", ordering="chunks_count")
424
+ def chunks_count_display(self, obj):
425
+ """Display chunks count."""
426
+ count = obj.chunks_count or 0
427
+ return f"{count} chunks"
428
+
429
+ @display(description="Created")
430
+ def created_at_display(self, obj):
431
+ """Created time with relative display."""
432
+ config = DateTimeDisplayConfig(show_relative=True)
433
+ return self.display_datetime_relative(obj, 'created_at', config)
434
+
435
+ @action(description="Mark as processable", variant=ActionVariant.SUCCESS)
436
+ def mark_as_processable(self, request, queryset):
437
+ """Mark selected items as processable."""
438
+ updated = queryset.update(is_processable=True)
439
+ messages.success(request, f"Marked {updated} items as processable.")
440
+
441
+ @action(description="Mark as not processable", variant=ActionVariant.WARNING)
442
+ def mark_as_not_processable(self, request, queryset):
443
+ """Mark selected items as not processable."""
444
+ updated = queryset.update(is_processable=False)
445
+ messages.warning(request, f"Marked {updated} items as not processable.")
671
446
 
672
447
 
673
448
  @admin.register(ArchiveItemChunk)
674
- class ArchiveItemChunkAdmin(ModelAdmin):
675
- """Admin interface for ArchiveItemChunk."""
449
+ class ArchiveItemChunkAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ExportMixin):
450
+ """Admin interface for ArchiveItemChunk using Django Admin Utilities."""
451
+
452
+ # Performance optimization
453
+ select_related_fields = ['archive_item', 'user']
676
454
 
677
455
  list_display = [
678
- 'chunk_display', 'archive_link', 'item_link', 'chunk_type_badge',
679
- 'token_count_display', 'embedding_status', 'cost_display', 'created_at'
456
+ 'chunk_display', 'archive_item_display', 'user_display', 'token_count_display',
457
+ 'embedding_status', 'embedding_cost_display', 'created_at_display'
680
458
  ]
681
- ordering = ['-created_at'] # Newest first
459
+ list_display_links = ['chunk_display']
460
+ ordering = ['-created_at']
682
461
  list_filter = [
683
- 'chunk_type', 'created_at', 'embedding_model',
684
- 'item__content_type', 'item__language',
685
- ('item', AutocompleteSelectFilter),
686
- ('archive', AutocompleteSelectFilter)
462
+ 'embedding_model', 'created_at',
463
+ ('user', AutocompleteSelectFilter),
464
+ ('archive_item', AutocompleteSelectFilter)
687
465
  ]
688
- search_fields = [
689
- 'content', 'item__item_name', 'item__relative_path',
690
- 'archive__title', 'item__archive__user__username'
691
- ]
692
- autocomplete_fields = ['item', 'archive']
466
+ search_fields = ['archive_item__item_name', 'user__username', 'content']
467
+ autocomplete_fields = ['item', 'user']
693
468
  readonly_fields = [
694
- 'id', 'user', 'token_count', 'character_count', 'embedding_model',
695
- 'embedding_cost', 'created_at', 'updated_at',
696
- 'archive_link', 'item_link', 'content_preview',
697
- 'context_summary', 'embedding_status'
469
+ 'id', 'token_count', 'character_count', 'embedding_cost',
470
+ 'created_at', 'updated_at', 'content_preview'
698
471
  ]
472
+
699
473
  fieldsets = (
700
- ('Basic Information', {
701
- 'fields': ('id', 'archive_link', 'item_link', 'user', 'chunk_index', 'chunk_type')
474
+ ('📄 Chunk Info', {
475
+ 'fields': ('id', 'archive_item', 'user', 'chunk_index'),
476
+ 'classes': ('tab',)
702
477
  }),
703
- ('Content', {
704
- 'fields': ('content_preview',)
478
+ ('📝 Content', {
479
+ 'fields': ('content_preview', 'content'),
480
+ 'classes': ('tab',)
705
481
  }),
706
- ('Context', {
707
- 'fields': ('context_summary',),
708
- 'classes': ('collapse',)
482
+ ('🔗 Embedding', {
483
+ 'fields': ('embedding_model', 'token_count', 'character_count', 'embedding_cost'),
484
+ 'classes': ('tab',)
709
485
  }),
710
- ('Vectorization', {
711
- 'fields': (
712
- 'embedding_status', 'token_count', 'character_count',
713
- 'embedding_model', 'embedding_cost'
714
- )
486
+ ('🧠 Vector', {
487
+ 'fields': ('embedding',),
488
+ 'classes': ('tab', 'collapse')
715
489
  }),
716
- ('Timestamps', {
490
+ ('Timestamps', {
717
491
  'fields': ('created_at', 'updated_at'),
718
- 'classes': ('collapse',)
719
- }),
492
+ 'classes': ('tab', 'collapse')
493
+ })
720
494
  )
721
495
 
722
- # Unfold configuration
723
- compressed_fields = True
724
- warn_unsaved_form = True
725
-
726
- # Form field overrides
727
- formfield_overrides = {
728
- JSONField: {
729
- "widget": JSONEditorWidget,
730
- }
731
- }
496
+ actions = ['regenerate_embeddings', 'clear_embeddings']
732
497
 
733
498
  @display(description="Chunk", ordering="chunk_index")
734
499
  def chunk_display(self, obj):
735
500
  """Display chunk identifier."""
736
- return f"Chunk {obj.chunk_index + 1}"
737
-
738
- @display(
739
- description="Chunk Type",
740
- ordering="chunk_type",
741
- label={
742
- 'CONTENT': 'info', # blue for content
743
- 'HEADER': 'success', # green for header
744
- 'FOOTER': 'warning', # orange for footer
745
- 'METADATA': 'secondary' # gray for metadata
746
- }
747
- )
748
- def chunk_type_badge(self, obj):
749
- """Display chunk type with color coding."""
750
- return obj.chunk_type, obj.get_chunk_type_display()
501
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.ARTICLE)
502
+ return StatusBadge.create(
503
+ text=f"Chunk {obj.chunk_index + 1}",
504
+ variant="info",
505
+ config=config
506
+ )
507
+
508
+ @display(description="Archive Item", ordering="archive_item__item_name")
509
+ def archive_item_display(self, obj):
510
+ """Display archive item name."""
511
+ return obj.archive_item.item_name
512
+
513
+ @display(description="User")
514
+ def user_display(self, obj):
515
+ """User display."""
516
+ if not obj.user:
517
+ return "—"
518
+ return self.display_user_simple(obj.user)
751
519
 
752
520
  @display(description="Tokens", ordering="token_count")
753
521
  def token_count_display(self, obj):
@@ -757,102 +525,46 @@ class ArchiveItemChunkAdmin(ModelAdmin):
757
525
  return f"{tokens/1000:.1f}K"
758
526
  return str(tokens)
759
527
 
760
- @display(
761
- description="Embedding",
762
- label={
763
- True: 'success', # green for has embedding
764
- False: 'danger' # red for no embedding
765
- }
766
- )
528
+ @display(description="Embedding")
767
529
  def embedding_status(self, obj):
768
530
  """Display embedding status."""
769
531
  has_embedding = obj.embedding is not None and len(obj.embedding) > 0
770
- return has_embedding, "✓ Vectorized" if has_embedding else "✗ Not vectorized"
532
+ if has_embedding:
533
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.CHECK_CIRCLE)
534
+ return StatusBadge.create(text="✓ Vectorized", variant="success", config=config)
535
+ else:
536
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.ERROR)
537
+ return StatusBadge.create(text="✗ Not vectorized", variant="danger", config=config)
771
538
 
772
539
  @display(description="Cost (USD)", ordering="embedding_cost")
773
- def cost_display(self, obj):
540
+ def embedding_cost_display(self, obj):
774
541
  """Display embedding cost with currency formatting."""
775
- return f"${obj.embedding_cost:.6f}"
776
-
777
- def archive_link(self, obj: ArchiveItemChunk) -> str:
778
- """Link to parent archive."""
779
- if obj.archive:
780
- url = reverse('admin:django_cfg_knowbase_documentarchive_change', args=[obj.archive.pk])
781
- return format_html('<a href="{}">{}</a>', url, obj.archive.title)
782
- return "No archive"
783
- archive_link.short_description = "Archive"
784
-
785
- def item_link(self, obj: ArchiveItemChunk) -> str:
786
- """Link to parent item."""
787
- if obj.item:
788
- url = reverse('admin:django_cfg_knowbase_archiveitem_change', args=[obj.item.pk])
789
- return format_html('<a href="{}">{}</a>', url, obj.item.item_name)
790
- return "No item"
791
- item_link.short_description = "Item"
792
-
793
- def changelist_view(self, request, extra_context=None):
794
- """Add chunk statistics to changelist."""
795
- extra_context = extra_context or {}
796
-
797
- queryset = self.get_queryset(request)
798
- stats = queryset.aggregate(
799
- total_chunks=Count('id'),
800
- vectorized_chunks=Count('id', filter=Q(embedding__isnull=False)),
801
- total_tokens=Sum('token_count'),
802
- total_characters=Sum('character_count'),
803
- total_embedding_cost=Sum('embedding_cost'),
804
- avg_tokens_per_chunk=Avg('token_count')
805
- )
806
-
807
- # Type breakdown
808
- type_counts = dict(
809
- queryset.values_list('chunk_type').annotate(
810
- count=Count('id')
811
- )
812
- )
813
-
814
- extra_context['chunk_stats'] = {
815
- 'total_chunks': stats['total_chunks'] or 0,
816
- 'vectorized_chunks': stats['vectorized_chunks'] or 0,
817
- 'total_tokens': stats['total_tokens'] or 0,
818
- 'total_characters': stats['total_characters'] or 0,
819
- 'total_embedding_cost': f"${(stats['total_embedding_cost'] or 0):.6f}",
820
- 'avg_tokens_per_chunk': f"{(stats['avg_tokens_per_chunk'] or 0):.0f}",
821
- 'type_counts': type_counts
822
- }
823
-
824
- return super().changelist_view(request, extra_context)
825
-
826
- def content_preview(self, obj: ArchiveItemChunk) -> str:
827
- """Show content preview."""
828
- preview = obj.content[:300]
829
- if len(obj.content) > 300:
830
- preview += "..."
831
- return format_html('<pre style="white-space: pre-wrap;">{}</pre>', preview)
832
- content_preview.short_description = "Content Preview"
833
-
834
- def context_summary(self, obj: ArchiveItemChunk) -> str:
835
- """Show context summary."""
836
- context = obj.get_context_summary()
837
- return format_html('<pre>{}</pre>',
838
- '\n'.join(f"{k}: {v}" for k, v in context.items()))
839
- context_summary.short_description = "Context Summary"
840
-
841
- def get_queryset(self, request):
842
- """Optimize queryset with select_related."""
843
- # Use all_users() to show all archive item chunks in admin, then filter by user if needed
844
- queryset = ArchiveItemChunk.objects.all_users().select_related(
845
- 'archive', 'item', 'user'
542
+ config = MoneyDisplayConfig(
543
+ currency="USD",
544
+ decimal_places=6,
545
+ show_sign=False
846
546
  )
847
-
848
- # For non-superusers, filter by their own chunks
849
- if not request.user.is_superuser:
850
- queryset = queryset.filter(user=request.user)
851
-
852
- return queryset
853
-
854
- def save_model(self, request, obj, form, change):
855
- """Automatically set user to current user when creating new archive item chunks."""
856
- if not change: # Only for new chunks
857
- obj.user = request.user
858
- super().save_model(request, obj, form, change)
547
+ return self.display_money_amount(obj, 'embedding_cost', config)
548
+
549
+ @display(description="Created")
550
+ def created_at_display(self, obj):
551
+ """Created time with relative display."""
552
+ config = DateTimeDisplayConfig(show_relative=True)
553
+ return self.display_datetime_relative(obj, 'created_at', config)
554
+
555
+ @display(description="Content Preview")
556
+ def content_preview(self, obj):
557
+ """Display content preview with truncation."""
558
+ return obj.content[:200] + "..." if len(obj.content) > 200 else obj.content
559
+
560
+ @action(description="Regenerate embeddings", variant=ActionVariant.INFO)
561
+ def regenerate_embeddings(self, request, queryset):
562
+ """Regenerate embeddings for selected chunks."""
563
+ count = queryset.count()
564
+ messages.info(request, f"Regenerate embeddings functionality not implemented yet. {count} chunks selected.")
565
+
566
+ @action(description="Clear embeddings", variant=ActionVariant.WARNING)
567
+ def clear_embeddings(self, request, queryset):
568
+ """Clear embeddings for selected chunks."""
569
+ updated = queryset.update(embedding=None)
570
+ messages.warning(request, f"Cleared embeddings for {updated} chunks.")