702 lines
26 KiB
Python
702 lines
26 KiB
Python
"""
|
|
System Module Admin
|
|
"""
|
|
from django.contrib import admin
|
|
from django import forms
|
|
from unfold.admin import ModelAdmin
|
|
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
|
from .models import AIPrompt, IntegrationSettings, AuthorProfile, Strategy
|
|
from .global_settings_models import (
|
|
GlobalIntegrationSettings,
|
|
GlobalAIPrompt,
|
|
GlobalAuthorProfile,
|
|
GlobalStrategy,
|
|
GlobalModuleSettings,
|
|
)
|
|
|
|
from django.contrib import messages
|
|
from import_export.admin import ExportMixin, ImportExportMixin
|
|
from import_export import resources
|
|
|
|
|
|
class AIPromptResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting AI Prompts"""
|
|
class Meta:
|
|
model = AIPrompt
|
|
fields = ('id', 'account__name', 'prompt_type', 'prompt_value', 'is_active', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
# Import settings admin
|
|
from .settings_admin import (
|
|
SystemSettingsAdmin, AccountSettingsAdmin, UserSettingsAdmin,
|
|
ModuleSettingsAdmin
|
|
)
|
|
|
|
try:
|
|
from .models import SystemLog, SystemStatus
|
|
|
|
@admin.register(SystemLog)
|
|
class SystemLogAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
|
list_display = ['id', 'account', 'module', 'level', 'action', 'message', 'created_at']
|
|
list_filter = ['module', 'level', 'created_at', 'account']
|
|
search_fields = ['message', 'action']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
date_hierarchy = 'created_at'
|
|
|
|
|
|
@admin.register(SystemStatus)
|
|
class SystemStatusAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
|
list_display = ['component', 'account', 'status', 'message', 'last_check']
|
|
list_filter = ['status', 'component', 'account']
|
|
search_fields = ['component', 'message']
|
|
readonly_fields = ['last_check']
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
@admin.register(AIPrompt)
|
|
class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|
resource_class = AIPromptResource
|
|
list_display = ['id', 'prompt_type', 'account', 'is_customized', 'is_active', 'updated_at']
|
|
list_filter = ['prompt_type', 'is_active', 'is_customized', 'account']
|
|
search_fields = ['prompt_type']
|
|
readonly_fields = ['created_at', 'updated_at', 'default_prompt']
|
|
actions = [
|
|
'bulk_activate',
|
|
'bulk_deactivate',
|
|
'bulk_reset_to_default',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('account', 'prompt_type', 'is_active', 'is_customized')
|
|
}),
|
|
('Prompt Content', {
|
|
'fields': ('prompt_value', 'default_prompt'),
|
|
'description': 'Customize prompt_value or reset to default_prompt'
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at')
|
|
}),
|
|
)
|
|
|
|
def get_account_display(self, obj):
|
|
"""Safely get account name"""
|
|
try:
|
|
account = getattr(obj, 'account', None)
|
|
return account.name if account else '-'
|
|
except:
|
|
return '-'
|
|
get_account_display.short_description = 'Account'
|
|
|
|
def bulk_activate(self, request, queryset):
|
|
updated = queryset.update(is_active=True)
|
|
self.message_user(request, f'{updated} AI prompt(s) activated.', messages.SUCCESS)
|
|
bulk_activate.short_description = 'Activate selected prompts'
|
|
|
|
def bulk_deactivate(self, request, queryset):
|
|
updated = queryset.update(is_active=False)
|
|
self.message_user(request, f'{updated} AI prompt(s) deactivated.', messages.SUCCESS)
|
|
bulk_deactivate.short_description = 'Deactivate selected prompts'
|
|
|
|
def bulk_reset_to_default(self, request, queryset):
|
|
"""Reset selected prompts to their global defaults"""
|
|
count = 0
|
|
for prompt in queryset:
|
|
if prompt.default_prompt:
|
|
prompt.reset_to_default()
|
|
count += 1
|
|
self.message_user(request, f'{count} prompt(s) reset to default.', messages.SUCCESS)
|
|
bulk_reset_to_default.short_description = 'Reset selected prompts to global default'
|
|
|
|
|
|
class IntegrationSettingsResource(resources.ModelResource):
|
|
"""Resource class for exporting Integration Settings (config masked)"""
|
|
class Meta:
|
|
model = IntegrationSettings
|
|
fields = ('id', 'account__name', 'integration_type', 'is_active', 'created_at')
|
|
export_order = fields
|
|
|
|
|
|
@admin.register(IntegrationSettings)
|
|
class IntegrationSettingsAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|
"""
|
|
Admin for per-account integration setting overrides.
|
|
|
|
IMPORTANT: This stores ONLY model selection and parameter overrides.
|
|
API keys come from GlobalIntegrationSettings and cannot be overridden.
|
|
Free plan users cannot create these - they must use global defaults.
|
|
"""
|
|
resource_class = IntegrationSettingsResource
|
|
list_display = ['id', 'integration_type', 'account', 'is_active', 'updated_at']
|
|
list_filter = ['integration_type', 'is_active', 'account']
|
|
search_fields = ['integration_type', 'account__name']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
actions = [
|
|
'bulk_activate',
|
|
'bulk_deactivate',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('account', 'integration_type', 'is_active')
|
|
}),
|
|
('Configuration Overrides', {
|
|
'fields': ('config',),
|
|
'description': (
|
|
'JSON overrides for model/parameter selection. '
|
|
'Fields: model, temperature, max_tokens, image_size, image_quality, etc. '
|
|
'Leave null to use global defaults. '
|
|
'Example: {"model": "gpt-4", "temperature": 0.8, "max_tokens": 4000} '
|
|
'WARNING: NEVER store API keys here - they come from GlobalIntegrationSettings'
|
|
)
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at')
|
|
}),
|
|
)
|
|
|
|
def get_account_display(self, obj):
|
|
"""Safely get account name"""
|
|
try:
|
|
account = getattr(obj, 'account', None)
|
|
return account.name if account else '-'
|
|
except:
|
|
return '-'
|
|
get_account_display.short_description = 'Account'
|
|
|
|
def bulk_activate(self, request, queryset):
|
|
updated = queryset.update(is_active=True)
|
|
self.message_user(request, f'{updated} integration setting(s) activated.', messages.SUCCESS)
|
|
bulk_activate.short_description = 'Activate selected integrations'
|
|
|
|
def bulk_deactivate(self, request, queryset):
|
|
updated = queryset.update(is_active=False)
|
|
self.message_user(request, f'{updated} integration setting(s) deactivated.', messages.SUCCESS)
|
|
bulk_deactivate.short_description = 'Deactivate selected integrations'
|
|
|
|
def bulk_test_connection(self, request, queryset):
|
|
"""Test connection for selected integration settings"""
|
|
count = queryset.filter(is_active=True).count()
|
|
self.message_user(
|
|
request,
|
|
f'{count} integration(s) queued for connection test. (Test logic to be implemented)',
|
|
messages.INFO
|
|
)
|
|
bulk_test_connection.short_description = 'Test connections'
|
|
|
|
|
|
class AuthorProfileResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Author Profiles"""
|
|
class Meta:
|
|
model = AuthorProfile
|
|
fields = ('id', 'name', 'account__name', 'tone', 'language', 'is_active', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
@admin.register(AuthorProfile)
|
|
class AuthorProfileAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|
resource_class = AuthorProfileResource
|
|
list_display = ['name', 'account', 'tone', 'language', 'is_active', 'created_at']
|
|
list_filter = ['is_active', 'tone', 'language', 'account']
|
|
search_fields = ['name', 'description', 'tone']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
actions = [
|
|
'bulk_activate',
|
|
'bulk_deactivate',
|
|
'bulk_clone',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('account', 'name', 'description', 'is_active')
|
|
}),
|
|
('Writing Style', {
|
|
'fields': ('tone', 'language', 'structure_template')
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at')
|
|
}),
|
|
)
|
|
|
|
def get_account_display(self, obj):
|
|
"""Safely get account name"""
|
|
try:
|
|
account = getattr(obj, 'account', None)
|
|
return account.name if account else '-'
|
|
except:
|
|
return '-'
|
|
get_account_display.short_description = 'Account'
|
|
|
|
def bulk_activate(self, request, queryset):
|
|
updated = queryset.update(is_active=True)
|
|
self.message_user(request, f'{updated} author profile(s) activated.', messages.SUCCESS)
|
|
bulk_activate.short_description = 'Activate selected profiles'
|
|
|
|
def bulk_deactivate(self, request, queryset):
|
|
updated = queryset.update(is_active=False)
|
|
self.message_user(request, f'{updated} author profile(s) deactivated.', messages.SUCCESS)
|
|
bulk_deactivate.short_description = 'Deactivate selected profiles'
|
|
|
|
def bulk_clone(self, request, queryset):
|
|
count = 0
|
|
for profile in queryset:
|
|
profile_copy = AuthorProfile.objects.get(pk=profile.pk)
|
|
profile_copy.pk = None
|
|
profile_copy.name = f"{profile.name} (Copy)"
|
|
profile_copy.is_active = False
|
|
profile_copy.save()
|
|
count += 1
|
|
self.message_user(request, f'{count} author profile(s) cloned.', messages.SUCCESS)
|
|
bulk_clone.short_description = 'Clone selected profiles'
|
|
|
|
|
|
class StrategyResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Strategies"""
|
|
class Meta:
|
|
model = Strategy
|
|
fields = ('id', 'name', 'account__name', 'sector__name', 'is_active', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
@admin.register(Strategy)
|
|
class StrategyAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|
resource_class = StrategyResource
|
|
list_display = ['name', 'account', 'sector', 'is_active', 'created_at']
|
|
list_filter = ['is_active', 'account']
|
|
search_fields = ['name', 'description']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
actions = [
|
|
'bulk_activate',
|
|
'bulk_deactivate',
|
|
'bulk_clone',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('account', 'name', 'description', 'sector', 'is_active')
|
|
}),
|
|
('Strategy Configuration', {
|
|
'fields': ('prompt_types', 'section_logic')
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at')
|
|
}),
|
|
)
|
|
|
|
def get_account_display(self, obj):
|
|
"""Safely get account name"""
|
|
try:
|
|
account = getattr(obj, 'account', None)
|
|
return account.name if account else '-'
|
|
except:
|
|
return '-'
|
|
get_account_display.short_description = 'Account'
|
|
|
|
def get_sector_display(self, obj):
|
|
"""Safely get sector name"""
|
|
try:
|
|
return obj.sector.name if obj.sector else 'Global'
|
|
except:
|
|
return 'Global'
|
|
get_sector_display.short_description = 'Sector'
|
|
def bulk_activate(self, request, queryset):
|
|
updated = queryset.update(is_active=True)
|
|
self.message_user(request, f'{updated} strategy/strategies activated.', messages.SUCCESS)
|
|
bulk_activate.short_description = 'Activate selected strategies'
|
|
|
|
def bulk_deactivate(self, request, queryset):
|
|
updated = queryset.update(is_active=False)
|
|
self.message_user(request, f'{updated} strategy/strategies deactivated.', messages.SUCCESS)
|
|
bulk_deactivate.short_description = 'Deactivate selected strategies'
|
|
|
|
def bulk_clone(self, request, queryset):
|
|
count = 0
|
|
for strategy in queryset:
|
|
strategy_copy = Strategy.objects.get(pk=strategy.pk)
|
|
strategy_copy.pk = None
|
|
strategy_copy.name = f"{strategy.name} (Copy)"
|
|
strategy_copy.is_active = False
|
|
strategy_copy.save()
|
|
count += 1
|
|
self.message_user(request, f'{count} strategy/strategies cloned.', messages.SUCCESS)
|
|
bulk_clone.short_description = 'Clone selected strategies'
|
|
|
|
|
|
# =============================================================================
|
|
# GLOBAL SETTINGS ADMIN - Platform-wide defaults
|
|
# =============================================================================
|
|
|
|
class GlobalIntegrationSettingsForm(forms.ModelForm):
|
|
"""Custom form for GlobalIntegrationSettings with dynamic choices from AIModelConfig"""
|
|
|
|
class Meta:
|
|
model = GlobalIntegrationSettings
|
|
fields = '__all__'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Load choices dynamically from AIModelConfig
|
|
from igny8_core.modules.system.global_settings_models import (
|
|
get_text_model_choices,
|
|
get_image_model_choices,
|
|
get_provider_choices,
|
|
)
|
|
|
|
# OpenAI text model choices
|
|
openai_choices = get_text_model_choices()
|
|
openai_text_choices = [(m, d) for m, d in openai_choices if 'gpt' in m.lower() or 'openai' in m.lower()]
|
|
if openai_text_choices:
|
|
self.fields['openai_model'].choices = openai_text_choices
|
|
|
|
# DALL-E image model choices
|
|
dalle_choices = get_image_model_choices(provider='openai')
|
|
if dalle_choices:
|
|
self.fields['dalle_model'].choices = dalle_choices
|
|
|
|
# Runware image model choices
|
|
runware_choices = get_image_model_choices(provider='runware')
|
|
if runware_choices:
|
|
self.fields['runware_model'].choices = runware_choices
|
|
|
|
# Image service provider choices (only OpenAI and Runware for now)
|
|
image_providers = get_provider_choices(model_type='image')
|
|
# Filter to only OpenAI and Runware
|
|
allowed_image_providers = [
|
|
(p, d) for p, d in image_providers
|
|
if p in ('openai', 'runware')
|
|
]
|
|
if allowed_image_providers:
|
|
self.fields['default_image_service'].choices = allowed_image_providers
|
|
|
|
|
|
@admin.register(GlobalIntegrationSettings)
|
|
class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
|
|
"""Admin for global integration settings (singleton)"""
|
|
form = GlobalIntegrationSettingsForm
|
|
list_display = ["id", "is_active", "last_updated", "updated_by"]
|
|
readonly_fields = ["last_updated", "openai_max_tokens", "anthropic_max_tokens"]
|
|
|
|
fieldsets = (
|
|
("OpenAI Settings", {
|
|
"fields": ("openai_api_key", "openai_model", "openai_temperature", "openai_max_tokens"),
|
|
"description": "Global OpenAI configuration used by all accounts (unless overridden). Max tokens is loaded from AI Model Configuration."
|
|
}),
|
|
("Image Generation - Default Service", {
|
|
"fields": ("default_image_service",),
|
|
"description": "Choose which image generation service is used by default for all accounts"
|
|
}),
|
|
("Image Generation - DALL-E", {
|
|
"fields": ("dalle_api_key", "dalle_model", "dalle_size"),
|
|
"description": "Global DALL-E (OpenAI) image generation configuration"
|
|
}),
|
|
("Image Generation - Runware", {
|
|
"fields": ("runware_api_key", "runware_model"),
|
|
"description": "Global Runware image generation configuration"
|
|
}),
|
|
("Universal Image Settings", {
|
|
"fields": ("image_quality", "image_style", "max_in_article_images", "desktop_image_size"),
|
|
"description": "Image quality, style, and sizing settings that apply to ALL providers (DALL-E, Runware, etc.)"
|
|
}),
|
|
("Status", {
|
|
"fields": ("is_active", "last_updated", "updated_by")
|
|
}),
|
|
)
|
|
|
|
def get_readonly_fields(self, request, obj=None):
|
|
"""Make max_tokens fields readonly - they are populated from AI Model Configuration"""
|
|
readonly = list(super().get_readonly_fields(request, obj))
|
|
if 'openai_max_tokens' not in readonly:
|
|
readonly.append('openai_max_tokens')
|
|
if 'anthropic_max_tokens' not in readonly:
|
|
readonly.append('anthropic_max_tokens')
|
|
return readonly
|
|
|
|
def openai_max_tokens(self, obj):
|
|
"""Display max tokens from the selected OpenAI model's configuration"""
|
|
from igny8_core.modules.system.global_settings_models import get_model_max_tokens
|
|
max_tokens = get_model_max_tokens(obj.openai_model) if obj else None
|
|
if max_tokens:
|
|
return f"{max_tokens:,} (from AI Model Configuration)"
|
|
return obj.openai_max_tokens if obj else "8192 (default)"
|
|
openai_max_tokens.short_description = "Max Output Tokens"
|
|
|
|
def anthropic_max_tokens(self, obj):
|
|
"""Display max tokens from the selected Anthropic model's configuration"""
|
|
from igny8_core.modules.system.global_settings_models import get_model_max_tokens
|
|
max_tokens = get_model_max_tokens(obj.anthropic_model) if obj else None
|
|
if max_tokens:
|
|
return f"{max_tokens:,} (from AI Model Configuration)"
|
|
return obj.anthropic_max_tokens if obj else "8192 (default)"
|
|
anthropic_max_tokens.short_description = "Max Output Tokens"
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
"""Update max_tokens from model config on save"""
|
|
from igny8_core.modules.system.global_settings_models import get_model_max_tokens
|
|
|
|
# Update OpenAI max tokens from model config
|
|
openai_max = get_model_max_tokens(obj.openai_model)
|
|
if openai_max:
|
|
obj.openai_max_tokens = openai_max
|
|
|
|
# Update Anthropic max tokens from model config
|
|
anthropic_max = get_model_max_tokens(obj.anthropic_model)
|
|
if anthropic_max:
|
|
obj.anthropic_max_tokens = anthropic_max
|
|
|
|
super().save_model(request, obj, form, change)
|
|
|
|
def has_add_permission(self, request):
|
|
"""Only allow one instance (singleton pattern)"""
|
|
return not GlobalIntegrationSettings.objects.exists()
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
"""Dont allow deletion of singleton"""
|
|
return False
|
|
|
|
@admin.register(GlobalAIPrompt)
|
|
class GlobalAIPromptAdmin(ExportMixin, Igny8ModelAdmin):
|
|
"""Admin for global AI prompt templates"""
|
|
list_display = ["prompt_type", "version", "is_active", "last_updated"]
|
|
list_filter = ["is_active", "prompt_type", "version"]
|
|
search_fields = ["prompt_type", "description"]
|
|
readonly_fields = ["last_updated", "created_at"]
|
|
|
|
fieldsets = (
|
|
("Basic Info", {
|
|
"fields": ("prompt_type", "description", "is_active", "version")
|
|
}),
|
|
("Prompt Content", {
|
|
"fields": ("prompt_value", "variables"),
|
|
"description": "Variables should be a list of variable names used in the prompt"
|
|
}),
|
|
("Timestamps", {
|
|
"fields": ("created_at", "last_updated")
|
|
}),
|
|
)
|
|
|
|
actions = ["increment_version"]
|
|
|
|
def increment_version(self, request, queryset):
|
|
"""Increment version for selected prompts"""
|
|
for prompt in queryset:
|
|
prompt.version += 1
|
|
prompt.save()
|
|
self.message_user(request, f"{queryset.count()} prompt(s) version incremented.", messages.SUCCESS)
|
|
increment_version.short_description = "Increment version"
|
|
|
|
|
|
@admin.register(GlobalAuthorProfile)
|
|
class GlobalAuthorProfileAdmin(ImportExportMixin, Igny8ModelAdmin):
|
|
"""Admin for global author profile templates"""
|
|
list_display = ["name", "category", "tone", "language", "is_active", "created_at"]
|
|
list_filter = ["is_active", "category", "tone", "language"]
|
|
search_fields = ["name", "description"]
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
fieldsets = (
|
|
("Basic Info", {
|
|
"fields": ("name", "description", "category", "is_active")
|
|
}),
|
|
("Writing Style", {
|
|
"fields": ("tone", "language", "structure_template")
|
|
}),
|
|
("Timestamps", {
|
|
"fields": ("created_at", "updated_at")
|
|
}),
|
|
)
|
|
|
|
|
|
@admin.register(GlobalStrategy)
|
|
class GlobalStrategyAdmin(ImportExportMixin, Igny8ModelAdmin):
|
|
"""Admin for global strategy templates"""
|
|
list_display = ["name", "category", "is_active", "created_at"]
|
|
list_filter = ["is_active", "category"]
|
|
search_fields = ["name", "description"]
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
fieldsets = (
|
|
("Basic Info", {
|
|
"fields": ("name", "description", "category", "is_active")
|
|
}),
|
|
("Strategy Configuration", {
|
|
"fields": ("prompt_types", "section_logic")
|
|
}),
|
|
("Timestamps", {
|
|
"fields": ("created_at", "updated_at")
|
|
}),
|
|
)
|
|
|
|
|
|
@admin.register(GlobalModuleSettings)
|
|
class GlobalModuleSettingsAdmin(Igny8ModelAdmin):
|
|
"""
|
|
Admin for global module enable/disable settings.
|
|
Singleton model - only one record exists.
|
|
Controls which modules are available platform-wide.
|
|
"""
|
|
|
|
def has_add_permission(self, request):
|
|
"""Only allow one instance"""
|
|
return not GlobalModuleSettings.objects.exists()
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
"""Prevent deletion of singleton"""
|
|
return False
|
|
|
|
fieldsets = (
|
|
('Module Availability (Platform-Wide)', {
|
|
'fields': (
|
|
'planner_enabled',
|
|
'writer_enabled',
|
|
'thinker_enabled',
|
|
'automation_enabled',
|
|
'site_builder_enabled',
|
|
'linker_enabled',
|
|
'optimizer_enabled',
|
|
'publisher_enabled',
|
|
),
|
|
'description': 'Control which modules are available across the entire platform. Disabled modules will not load for ANY user.'
|
|
}),
|
|
('Metadata', {
|
|
'fields': ('created_at', 'updated_at'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
|
|
list_display = [
|
|
'id',
|
|
'planner_enabled',
|
|
'writer_enabled',
|
|
'thinker_enabled',
|
|
'automation_enabled',
|
|
'site_builder_enabled',
|
|
'linker_enabled',
|
|
'optimizer_enabled',
|
|
'publisher_enabled',
|
|
'updated_at',
|
|
]
|
|
|
|
|
|
# IntegrationProvider Admin (centralized API keys)
|
|
from .models import IntegrationProvider
|
|
|
|
|
|
@admin.register(IntegrationProvider)
|
|
class IntegrationProviderAdmin(Igny8ModelAdmin):
|
|
"""
|
|
Admin for IntegrationProvider - Centralized API key management.
|
|
Per final-model-schemas.md
|
|
"""
|
|
|
|
list_display = [
|
|
'provider_id',
|
|
'display_name',
|
|
'provider_type',
|
|
'is_active',
|
|
'is_sandbox',
|
|
'has_api_key',
|
|
'updated_at',
|
|
]
|
|
list_filter = ['provider_type', 'is_active', 'is_sandbox']
|
|
search_fields = ['provider_id', 'display_name']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
|
|
fieldsets = (
|
|
('Provider Info', {
|
|
'fields': ('provider_id', 'display_name', 'provider_type')
|
|
}),
|
|
('API Configuration', {
|
|
'fields': ('api_key', 'api_secret', 'webhook_secret', 'api_endpoint'),
|
|
'description': 'Enter API keys and endpoints. These are platform-wide.'
|
|
}),
|
|
('Extra Config', {
|
|
'fields': ('config',),
|
|
'classes': ('collapse',),
|
|
'description': 'JSON config for provider-specific settings'
|
|
}),
|
|
('Status', {
|
|
'fields': ('is_active', 'is_sandbox')
|
|
}),
|
|
('Metadata', {
|
|
'fields': ('updated_by', 'created_at', 'updated_at'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
def has_api_key(self, obj):
|
|
"""Show if API key is configured"""
|
|
return bool(obj.api_key)
|
|
has_api_key.boolean = True
|
|
has_api_key.short_description = 'API Key Set'
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
"""Set updated_by to current user"""
|
|
obj.updated_by = request.user
|
|
super().save_model(request, obj, form, change)
|
|
|
|
|
|
# SystemAISettings Admin (new simplified AI settings)
|
|
from .ai_settings import SystemAISettings
|
|
|
|
|
|
@admin.register(SystemAISettings)
|
|
class SystemAISettingsAdmin(Igny8ModelAdmin):
|
|
"""
|
|
Admin for SystemAISettings - System-wide AI defaults (Singleton).
|
|
Per final-model-schemas.md
|
|
"""
|
|
|
|
list_display = [
|
|
'id',
|
|
'temperature',
|
|
'max_tokens',
|
|
'image_style',
|
|
'image_quality',
|
|
'max_images_per_article',
|
|
'updated_at',
|
|
]
|
|
readonly_fields = ['updated_at']
|
|
|
|
fieldsets = (
|
|
('AI Parameters', {
|
|
'fields': ('temperature', 'max_tokens'),
|
|
'description': 'System-wide defaults for AI text generation. Accounts can override via AccountSettings.'
|
|
}),
|
|
('Image Generation', {
|
|
'fields': ('image_style', 'image_quality', 'max_images_per_article', 'image_size'),
|
|
'description': 'System-wide defaults for image generation. Accounts can override via AccountSettings.'
|
|
}),
|
|
('Metadata', {
|
|
'fields': ('updated_by', 'updated_at'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
def has_add_permission(self, request):
|
|
"""Only allow one instance (singleton)"""
|
|
return not SystemAISettings.objects.exists()
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
"""Prevent deletion of singleton"""
|
|
return False
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
"""Set updated_by to current user"""
|
|
obj.updated_by = request.user
|
|
super().save_model(request, obj, form, change)
|
|
|
|
|
|
# Import Email Admin (EmailSettings, EmailTemplate, EmailLog)
|
|
from .email_admin import EmailSettingsAdmin, EmailTemplateAdmin, EmailLogAdmin
|