Files
igny8/backend/igny8_core/modules/system/admin.py
IGNY8 VPS (Salman) c4de8994dd image gen mess
2026-01-03 22:31:30 +00:00

590 lines
22 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, AISettingsAdmin
)
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',
]