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