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