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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/admin/__init__.py +24 -8
- django_cfg/apps/accounts/admin/activity_admin.py +146 -0
- django_cfg/apps/accounts/admin/filters.py +98 -22
- django_cfg/apps/accounts/admin/group_admin.py +86 -0
- django_cfg/apps/accounts/admin/inlines.py +42 -13
- django_cfg/apps/accounts/admin/otp_admin.py +115 -0
- django_cfg/apps/accounts/admin/registration_admin.py +173 -0
- django_cfg/apps/accounts/admin/resources.py +123 -19
- django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
- django_cfg/apps/accounts/admin/user_admin.py +362 -0
- django_cfg/apps/agents/admin/__init__.py +17 -4
- django_cfg/apps/agents/admin/execution_admin.py +204 -183
- django_cfg/apps/agents/admin/registry_admin.py +230 -255
- django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
- django_cfg/apps/agents/core/__init__.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +221 -0
- django_cfg/apps/agents/core/exceptions.py +14 -0
- django_cfg/apps/agents/core/orchestrator.py +18 -3
- django_cfg/apps/knowbase/admin/__init__.py +1 -1
- django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
- django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
- django_cfg/apps/knowbase/admin/document_admin.py +269 -262
- django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
- django_cfg/apps/knowbase/config/settings.py +21 -4
- django_cfg/apps/knowbase/views/chat_views.py +3 -0
- django_cfg/apps/leads/admin/__init__.py +3 -1
- django_cfg/apps/leads/admin/leads_admin.py +235 -35
- django_cfg/apps/maintenance/admin/__init__.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
- django_cfg/apps/maintenance/admin/log_admin.py +143 -61
- django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
- django_cfg/apps/maintenance/admin/site_admin.py +213 -352
- django_cfg/apps/newsletter/admin/__init__.py +29 -2
- django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
- django_cfg/apps/payments/admin/__init__.py +18 -27
- django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
- django_cfg/apps/payments/admin/balance_admin.py +166 -632
- django_cfg/apps/payments/admin/currencies_admin.py +235 -607
- django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
- django_cfg/apps/payments/admin/filters.py +83 -3
- django_cfg/apps/payments/admin/networks_admin.py +258 -0
- django_cfg/apps/payments/admin/payments_admin.py +171 -461
- django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
- django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
- django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
- django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
- django_cfg/apps/payments/middleware/api_access.py +32 -6
- django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
- django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
- django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
- django_cfg/apps/payments/models/balance.py +12 -0
- django_cfg/apps/payments/models/currencies.py +106 -32
- django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
- django_cfg/apps/payments/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +1 -1
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +95 -39
- django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
- django_cfg/apps/payments/services/providers/models/base.py +122 -0
- django_cfg/apps/payments/services/providers/models/providers.py +87 -0
- django_cfg/apps/payments/services/providers/models/universal.py +48 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
- django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
- django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -32
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
- django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
- django_cfg/apps/payments/tasks/__init__.py +39 -0
- django_cfg/apps/payments/tasks/types.py +73 -0
- django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
- django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
- django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
- django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
- django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
- django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
- django_cfg/apps/payments/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +5 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +4 -3
- django_cfg/apps/support/admin/__init__.py +10 -1
- django_cfg/apps/support/admin/support_admin.py +338 -141
- django_cfg/apps/tasks/admin/__init__.py +11 -0
- django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/app_agent_diagnose.py +470 -0
- django_cfg/management/commands/app_agent_generate.py +342 -0
- django_cfg/management/commands/app_agent_info.py +308 -0
- django_cfg/management/commands/migrate_all.py +9 -3
- django_cfg/management/commands/migrator.py +11 -6
- django_cfg/management/commands/rundramatiq.py +3 -2
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- django_cfg/modules/django_admin/__init__.py +64 -0
- django_cfg/modules/django_admin/decorators/__init__.py +13 -0
- django_cfg/modules/django_admin/decorators/actions.py +106 -0
- django_cfg/modules/django_admin/decorators/display.py +106 -0
- django_cfg/modules/django_admin/mixins/__init__.py +14 -0
- django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
- django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
- django_cfg/modules/django_admin/models/__init__.py +20 -0
- django_cfg/modules/django_admin/models/action_models.py +33 -0
- django_cfg/modules/django_admin/models/badge_models.py +20 -0
- django_cfg/modules/django_admin/models/base.py +26 -0
- django_cfg/modules/django_admin/models/display_models.py +31 -0
- django_cfg/modules/django_admin/utils/badges.py +159 -0
- django_cfg/modules/django_admin/utils/displays.py +247 -0
- django_cfg/modules/django_app_agent/__init__.py +87 -0
- django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
- django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
- django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
- django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
- django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
- django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
- django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
- django_cfg/modules/django_app_agent/core/__init__.py +33 -0
- django_cfg/modules/django_app_agent/core/config.py +300 -0
- django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
- django_cfg/modules/django_app_agent/models/__init__.py +71 -0
- django_cfg/modules/django_app_agent/models/base.py +283 -0
- django_cfg/modules/django_app_agent/models/context.py +496 -0
- django_cfg/modules/django_app_agent/models/enums.py +481 -0
- django_cfg/modules/django_app_agent/models/requests.py +500 -0
- django_cfg/modules/django_app_agent/models/responses.py +585 -0
- django_cfg/modules/django_app_agent/pytest.ini +6 -0
- django_cfg/modules/django_app_agent/services/__init__.py +42 -0
- django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
- django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
- django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
- django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
- django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
- django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
- django_cfg/modules/django_app_agent/services/base.py +437 -0
- django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
- django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
- django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
- django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
- django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
- django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
- django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
- django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
- django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
- django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
- django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
- django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
- django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
- django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
- django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
- django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
- django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
- django_cfg/modules/django_app_agent/services/report_service.py +332 -0
- django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
- django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
- django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
- django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
- django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
- django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
- django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
- django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
- django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
- django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
- django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
- django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
- django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
- django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
- django_cfg/modules/django_app_agent/ui/cli.py +419 -0
- django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
- django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
- django_cfg/modules/django_app_agent/utils/logging.py +360 -0
- django_cfg/modules/django_app_agent/utils/validation.py +417 -0
- django_cfg/modules/django_currency/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
- django_cfg/modules/django_currency/core/converter.py +12 -12
- django_cfg/modules/django_currency/database/__init__.py +2 -2
- django_cfg/modules/django_currency/database/database_loader.py +93 -42
- django_cfg/modules/django_llm/llm/client.py +10 -2
- django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
- django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
- django_cfg/modules/django_unfold/dashboard.py +14 -13
- django_cfg/modules/django_unfold/models/config.py +1 -1
- django_cfg/registry/core.py +3 -0
- django_cfg/registry/third_party.py +2 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/RECORD +223 -117
- django_cfg/apps/accounts/admin/activity.py +0 -96
- django_cfg/apps/accounts/admin/group.py +0 -17
- django_cfg/apps/accounts/admin/otp.py +0 -59
- django_cfg/apps/accounts/admin/registration_source.py +0 -97
- django_cfg/apps/accounts/admin/twilio_response.py +0 -227
- django_cfg/apps/accounts/admin/user.py +0 -300
- django_cfg/apps/agents/core/agent.py +0 -281
- django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
- django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
- django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
- django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
- django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
- django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
- django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
- django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
- django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
- django_cfg/apps/tasks/admin.py +0 -320
- django_cfg/middleware/static_nocache.py +0 -55
- django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
- /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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
|
36
|
-
can_delete = False
|
37
|
-
show_change_link = True
|
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
|
-
|
61
|
-
|
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(
|
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', '
|
95
|
+
'title_display', 'user_display', 'archive_type_display', 'status_display',
|
85
96
|
'items_count', 'chunks_count', 'vectorization_progress', 'file_size_display',
|
86
|
-
'progress_display', '
|
97
|
+
'progress_display', 'created_at_display'
|
87
98
|
]
|
88
|
-
|
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', '
|
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
|
-
('
|
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
|
-
('
|
110
|
-
'fields': (
|
111
|
-
|
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'
|
119
|
-
|
120
|
-
)
|
128
|
+
'processing_error'
|
129
|
+
),
|
130
|
+
'classes': ('tab',)
|
121
131
|
}),
|
122
|
-
('Statistics', {
|
123
|
-
'fields': (
|
124
|
-
|
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
|
-
|
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
|
157
|
+
"""Display archive title."""
|
155
158
|
title = obj.title or "Untitled Archive"
|
156
159
|
if len(title) > 50:
|
157
160
|
title = title[:47] + "..."
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
189
|
-
|
190
|
-
return
|
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="
|
193
|
-
def
|
194
|
-
"""Display
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
237
|
-
|
238
|
-
|
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
|
-
|
272
|
-
|
233
|
+
if total == 0:
|
234
|
+
return "No chunks"
|
273
235
|
|
274
|
-
|
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
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
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
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
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
|
-
|
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
|
-
|
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
|
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(
|
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', '
|
492
|
-
'
|
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
|
-
|
329
|
+
list_display_links = ['item_name_display']
|
330
|
+
ordering = ['-created_at']
|
495
331
|
list_filter = [
|
496
|
-
'content_type', 'is_processable', '
|
497
|
-
'
|
498
|
-
('
|
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
|
-
|
336
|
+
search_fields = ['item_name', 'content_type', 'archive__title', 'user__username']
|
337
|
+
autocomplete_fields = ['archive', 'user']
|
505
338
|
readonly_fields = [
|
506
|
-
'id', 'user', '
|
507
|
-
'
|
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
|
-
('
|
512
|
-
'fields': ('id', '
|
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
|
-
('
|
521
|
-
'fields': (
|
522
|
-
|
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
|
-
('
|
531
|
-
'fields': ('
|
532
|
-
'classes': ('
|
352
|
+
('📊 Processing', {
|
353
|
+
'fields': ('chunks_count',),
|
354
|
+
'classes': ('tab',)
|
533
355
|
}),
|
534
|
-
('
|
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
|
-
|
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
|
560
|
-
name = obj.item_name
|
561
|
-
if len(name) >
|
562
|
-
name = name[:
|
563
|
-
|
564
|
-
|
565
|
-
|
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
|
570
|
-
"""
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
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
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
)
|
642
|
-
|
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
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
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', '
|
679
|
-
'
|
456
|
+
'chunk_display', 'archive_item_display', 'user_display', 'token_count_display',
|
457
|
+
'embedding_status', 'embedding_cost_display', 'created_at_display'
|
680
458
|
]
|
681
|
-
|
459
|
+
list_display_links = ['chunk_display']
|
460
|
+
ordering = ['-created_at']
|
682
461
|
list_filter = [
|
683
|
-
'
|
684
|
-
'
|
685
|
-
('
|
686
|
-
('archive', AutocompleteSelectFilter)
|
462
|
+
'embedding_model', 'created_at',
|
463
|
+
('user', AutocompleteSelectFilter),
|
464
|
+
('archive_item', AutocompleteSelectFilter)
|
687
465
|
]
|
688
|
-
search_fields = [
|
689
|
-
|
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', '
|
695
|
-
'
|
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
|
-
('
|
701
|
-
'fields': ('id', '
|
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
|
-
('
|
707
|
-
'fields': ('
|
708
|
-
'classes': ('
|
482
|
+
('🔗 Embedding', {
|
483
|
+
'fields': ('embedding_model', 'token_count', 'character_count', 'embedding_cost'),
|
484
|
+
'classes': ('tab',)
|
709
485
|
}),
|
710
|
-
('
|
711
|
-
'fields': (
|
712
|
-
|
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
|
-
|
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
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
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
|
-
|
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
|
540
|
+
def embedding_cost_display(self, obj):
|
774
541
|
"""Display embedding cost with currency formatting."""
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
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
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
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.")
|