django admin Groups reorg, Frontend udpates for site settings, #Migration runs
This commit is contained in:
@@ -552,19 +552,18 @@ class AccountPaymentMethodAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
|
||||
@admin.register(CreditCostConfig)
|
||||
class CreditCostConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
"""
|
||||
Admin for Credit Cost Configuration.
|
||||
Per final-model-schemas.md - Fixed credits per operation type.
|
||||
"""
|
||||
list_display = [
|
||||
'operation_type',
|
||||
'display_name',
|
||||
'tokens_per_credit_display',
|
||||
'price_per_credit_usd',
|
||||
'min_credits',
|
||||
'is_active',
|
||||
'cost_change_indicator',
|
||||
'updated_at',
|
||||
'updated_by'
|
||||
'base_credits_display',
|
||||
'is_active_icon',
|
||||
]
|
||||
|
||||
list_filter = ['is_active', 'updated_at']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['operation_type', 'display_name', 'description']
|
||||
actions = ['bulk_activate', 'bulk_deactivate']
|
||||
|
||||
@@ -572,60 +571,30 @@ class CreditCostConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
('Operation', {
|
||||
'fields': ('operation_type', 'display_name', 'description')
|
||||
}),
|
||||
('Token-to-Credit Configuration', {
|
||||
'fields': ('tokens_per_credit', 'min_credits', 'price_per_credit_usd', 'is_active'),
|
||||
'description': 'Configure how tokens are converted to credits for this operation'
|
||||
}),
|
||||
('Audit Trail', {
|
||||
'fields': ('previous_tokens_per_credit', 'updated_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
('Credits', {
|
||||
'fields': ('base_credits', 'is_active'),
|
||||
'description': 'Fixed credits charged per operation'
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at', 'previous_tokens_per_credit']
|
||||
|
||||
def tokens_per_credit_display(self, obj):
|
||||
"""Show token ratio with color coding"""
|
||||
if obj.tokens_per_credit <= 50:
|
||||
color = 'red' # Expensive (low tokens per credit)
|
||||
elif obj.tokens_per_credit <= 100:
|
||||
color = 'orange'
|
||||
else:
|
||||
color = 'green' # Cheap (high tokens per credit)
|
||||
def base_credits_display(self, obj):
|
||||
"""Show base credits with formatting"""
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold;">{} tokens/credit</span>',
|
||||
color,
|
||||
obj.tokens_per_credit
|
||||
'<span style="font-weight: bold;">{} credits</span>',
|
||||
obj.base_credits
|
||||
)
|
||||
tokens_per_credit_display.short_description = 'Token Ratio'
|
||||
base_credits_display.short_description = 'Credits'
|
||||
|
||||
def cost_change_indicator(self, obj):
|
||||
"""Show if token ratio changed recently"""
|
||||
if obj.previous_tokens_per_credit is not None:
|
||||
if obj.tokens_per_credit < obj.previous_tokens_per_credit:
|
||||
icon = '📈' # More expensive (fewer tokens per credit)
|
||||
color = 'red'
|
||||
elif obj.tokens_per_credit > obj.previous_tokens_per_credit:
|
||||
icon = '📉' # Cheaper (more tokens per credit)
|
||||
color = 'green'
|
||||
else:
|
||||
icon = '➡️' # Same
|
||||
color = 'gray'
|
||||
|
||||
def is_active_icon(self, obj):
|
||||
"""Active status icon"""
|
||||
if obj.is_active:
|
||||
return format_html(
|
||||
'{} <span style="color: {};">({} → {})</span>',
|
||||
icon,
|
||||
color,
|
||||
obj.previous_tokens_per_credit,
|
||||
obj.tokens_per_credit
|
||||
'<span style="color: green; font-size: 18px;" title="Active">●</span>'
|
||||
)
|
||||
return '—'
|
||||
cost_change_indicator.short_description = 'Recent Change'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Track who made the change"""
|
||||
obj.updated_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
return format_html(
|
||||
'<span style="color: red; font-size: 18px;" title="Inactive">●</span>'
|
||||
)
|
||||
is_active_icon.short_description = 'Active'
|
||||
|
||||
@admin.action(description='Activate selected configurations')
|
||||
def bulk_activate(self, request, queryset):
|
||||
@@ -763,67 +732,60 @@ class BillingConfigurationAdmin(Igny8ModelAdmin):
|
||||
@admin.register(AIModelConfig)
|
||||
class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
"""
|
||||
Admin for AI Model Configuration - Database-driven model pricing
|
||||
Replaces hardcoded MODEL_RATES and IMAGE_MODEL_RATES
|
||||
Admin for AI Model Configuration - Single Source of Truth for Models.
|
||||
Per final-model-schemas.md
|
||||
"""
|
||||
list_display = [
|
||||
'model_name',
|
||||
'display_name_short',
|
||||
'model_type_badge',
|
||||
'provider_badge',
|
||||
'pricing_display',
|
||||
'credit_display',
|
||||
'quality_tier',
|
||||
'is_active_icon',
|
||||
'is_default_icon',
|
||||
'sort_order',
|
||||
'updated_at',
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'model_type',
|
||||
'provider',
|
||||
'quality_tier',
|
||||
'is_active',
|
||||
'is_default',
|
||||
'supports_json_mode',
|
||||
'supports_vision',
|
||||
'supports_function_calling',
|
||||
]
|
||||
|
||||
search_fields = ['model_name', 'display_name', 'description']
|
||||
search_fields = ['model_name', 'display_name']
|
||||
|
||||
ordering = ['model_type', 'sort_order', 'model_name']
|
||||
ordering = ['model_type', 'model_name']
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at', 'updated_by']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('model_name', 'display_name', 'model_type', 'provider', 'description'),
|
||||
'description': 'Core model identification and classification'
|
||||
'fields': ('model_name', 'model_type', 'provider', 'display_name'),
|
||||
'description': 'Core model identification'
|
||||
}),
|
||||
('Text Model Pricing', {
|
||||
'fields': ('input_cost_per_1m', 'output_cost_per_1m', 'context_window', 'max_output_tokens'),
|
||||
'description': 'Pricing and limits for TEXT models only (leave blank for image models)',
|
||||
'fields': ('cost_per_1k_input', 'cost_per_1k_output', 'tokens_per_credit', 'max_tokens', 'context_window'),
|
||||
'description': 'For TEXT models only',
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Image Model Pricing', {
|
||||
'fields': ('cost_per_image', 'valid_sizes'),
|
||||
'description': 'Pricing and configuration for IMAGE models only (leave blank for text models)',
|
||||
'fields': ('credits_per_image', 'quality_tier'),
|
||||
'description': 'For IMAGE models only',
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Capabilities', {
|
||||
'fields': ('supports_json_mode', 'supports_vision', 'supports_function_calling'),
|
||||
'description': 'Model features and capabilities'
|
||||
}),
|
||||
('Status & Display', {
|
||||
'fields': ('is_active', 'is_default', 'sort_order'),
|
||||
'description': 'Control model availability and ordering in dropdowns'
|
||||
}),
|
||||
('Lifecycle', {
|
||||
'fields': ('release_date', 'deprecation_date'),
|
||||
'description': 'Model release and deprecation dates',
|
||||
'fields': ('capabilities',),
|
||||
'description': 'JSON: vision, function_calling, json_mode, etc.',
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Audit Trail', {
|
||||
'fields': ('created_at', 'updated_at', 'updated_by'),
|
||||
('Status', {
|
||||
'fields': ('is_active', 'is_default'),
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
@@ -831,8 +793,8 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
# Custom display methods
|
||||
def display_name_short(self, obj):
|
||||
"""Truncated display name for list view"""
|
||||
if len(obj.display_name) > 50:
|
||||
return obj.display_name[:47] + '...'
|
||||
if len(obj.display_name) > 40:
|
||||
return obj.display_name[:37] + '...'
|
||||
return obj.display_name
|
||||
display_name_short.short_description = 'Display Name'
|
||||
|
||||
@@ -841,7 +803,6 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
colors = {
|
||||
'text': '#3498db', # Blue
|
||||
'image': '#e74c3c', # Red
|
||||
'embedding': '#2ecc71', # Green
|
||||
}
|
||||
color = colors.get(obj.model_type, '#95a5a6')
|
||||
return format_html(
|
||||
@@ -855,10 +816,10 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
def provider_badge(self, obj):
|
||||
"""Colored badge for provider"""
|
||||
colors = {
|
||||
'openai': '#10a37f', # OpenAI green
|
||||
'anthropic': '#d97757', # Anthropic orange
|
||||
'runware': '#6366f1', # Purple
|
||||
'google': '#4285f4', # Google blue
|
||||
'openai': '#10a37f',
|
||||
'anthropic': '#d97757',
|
||||
'runware': '#6366f1',
|
||||
'google': '#4285f4',
|
||||
}
|
||||
color = colors.get(obj.provider, '#95a5a6')
|
||||
return format_html(
|
||||
@@ -869,23 +830,20 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
)
|
||||
provider_badge.short_description = 'Provider'
|
||||
|
||||
def pricing_display(self, obj):
|
||||
"""Format pricing based on model type"""
|
||||
if obj.model_type == 'text':
|
||||
def credit_display(self, obj):
|
||||
"""Format credit info based on model type"""
|
||||
if obj.model_type == 'text' and obj.tokens_per_credit:
|
||||
return format_html(
|
||||
'<span style="color: #2c3e50; font-family: monospace;">'
|
||||
'${} / ${} per 1M</span>',
|
||||
obj.input_cost_per_1m,
|
||||
obj.output_cost_per_1m
|
||||
'<span style="font-family: monospace;">{} tokens/credit</span>',
|
||||
obj.tokens_per_credit
|
||||
)
|
||||
elif obj.model_type == 'image':
|
||||
elif obj.model_type == 'image' and obj.credits_per_image:
|
||||
return format_html(
|
||||
'<span style="color: #2c3e50; font-family: monospace;">'
|
||||
'${} per image</span>',
|
||||
obj.cost_per_image
|
||||
'<span style="font-family: monospace;">{} credits/image</span>',
|
||||
obj.credits_per_image
|
||||
)
|
||||
return '-'
|
||||
pricing_display.short_description = 'Pricing'
|
||||
credit_display.short_description = 'Credits'
|
||||
|
||||
def is_active_icon(self, obj):
|
||||
"""Active status icon"""
|
||||
@@ -915,41 +873,27 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
def bulk_activate(self, request, queryset):
|
||||
"""Enable selected models"""
|
||||
count = queryset.update(is_active=True)
|
||||
self.message_user(
|
||||
request,
|
||||
f'{count} model(s) activated successfully.',
|
||||
messages.SUCCESS
|
||||
)
|
||||
self.message_user(request, f'{count} model(s) activated.', messages.SUCCESS)
|
||||
bulk_activate.short_description = 'Activate selected models'
|
||||
|
||||
def bulk_deactivate(self, request, queryset):
|
||||
"""Disable selected models"""
|
||||
count = queryset.update(is_active=False)
|
||||
self.message_user(
|
||||
request,
|
||||
f'{count} model(s) deactivated successfully.',
|
||||
messages.WARNING
|
||||
)
|
||||
self.message_user(request, f'{count} model(s) deactivated.', messages.WARNING)
|
||||
bulk_deactivate.short_description = 'Deactivate selected models'
|
||||
|
||||
def set_as_default(self, request, queryset):
|
||||
"""Set one model as default for its type"""
|
||||
if queryset.count() != 1:
|
||||
self.message_user(
|
||||
request,
|
||||
'Please select exactly one model to set as default.',
|
||||
messages.ERROR
|
||||
)
|
||||
self.message_user(request, 'Select exactly one model.', messages.ERROR)
|
||||
return
|
||||
|
||||
model = queryset.first()
|
||||
# Unset other defaults for same type
|
||||
AIModelConfig.objects.filter(
|
||||
model_type=model.model_type,
|
||||
is_default=True
|
||||
).exclude(pk=model.pk).update(is_default=False)
|
||||
|
||||
# Set this as default
|
||||
model.is_default = True
|
||||
model.save()
|
||||
|
||||
@@ -958,9 +902,4 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
f'{model.model_name} is now the default {model.get_model_type_display()} model.',
|
||||
messages.SUCCESS
|
||||
)
|
||||
set_as_default.short_description = 'Set as default model (for its type)'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Track who made the change"""
|
||||
obj.updated_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
set_as_default.short_description = 'Set as default model'
|
||||
|
||||
Reference in New Issue
Block a user