Merge branch 'main' of https://git.igny8.com/salman/igny8
This commit is contained in:
@@ -6,11 +6,11 @@ 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 (
|
||||
GlobalModuleSettings,
|
||||
GlobalIntegrationSettings,
|
||||
GlobalAIPrompt,
|
||||
GlobalAuthorProfile,
|
||||
GlobalStrategy,
|
||||
GlobalModuleSettings,
|
||||
)
|
||||
|
||||
from django.contrib import messages
|
||||
@@ -59,8 +59,8 @@ except ImportError:
|
||||
@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']
|
||||
list_display = ['id', 'prompt_type', 'account', 'is_active', 'updated_at']
|
||||
list_filter = ['prompt_type', 'is_active', 'account']
|
||||
search_fields = ['prompt_type']
|
||||
readonly_fields = ['created_at', 'updated_at', 'default_prompt']
|
||||
actions = [
|
||||
@@ -71,11 +71,10 @@ class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
'fields': ('account', 'prompt_type', 'is_active', 'is_customized')
|
||||
'fields': ('account', 'prompt_type', 'is_active')
|
||||
}),
|
||||
('Prompt Content', {
|
||||
'fields': ('prompt_value', 'default_prompt'),
|
||||
'description': 'Customize prompt_value or reset to default_prompt'
|
||||
'fields': ('prompt_value', 'default_prompt')
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at')
|
||||
@@ -102,14 +101,14 @@ class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
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()
|
||||
prompt.prompt_value = prompt.default_prompt
|
||||
prompt.save()
|
||||
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'
|
||||
self.message_user(request, f'{count} AI prompt(s) reset to default values.', messages.SUCCESS)
|
||||
bulk_reset_to_default.short_description = 'Reset to default values'
|
||||
|
||||
|
||||
class IntegrationSettingsResource(resources.ModelResource):
|
||||
@@ -122,42 +121,36 @@ class IntegrationSettingsResource(resources.ModelResource):
|
||||
|
||||
@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']
|
||||
search_fields = ['integration_type']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
'bulk_test_connection',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
'fields': ('account', 'integration_type', 'is_active')
|
||||
}),
|
||||
('Configuration Overrides', {
|
||||
('Configuration', {
|
||||
'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'
|
||||
)
|
||||
'description': 'JSON configuration containing API keys and settings. Example: {"apiKey": "sk-...", "model": "gpt-4.1", "enabled": true}'
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at')
|
||||
}),
|
||||
)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Make config readonly when viewing to prevent accidental exposure"""
|
||||
if obj: # Editing existing object
|
||||
return self.readonly_fields + ['config']
|
||||
return self.readonly_fields
|
||||
|
||||
def get_account_display(self, obj):
|
||||
"""Safely get account name"""
|
||||
try:
|
||||
@@ -329,9 +322,49 @@ class StrategyAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
bulk_clone.short_description = 'Clone selected strategies'
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@admin.register(GlobalModuleSettings)
|
||||
class GlobalModuleSettingsAdmin(ModelAdmin):
|
||||
"""Admin for Global Module Settings (Singleton)"""
|
||||
|
||||
list_display = [
|
||||
'id',
|
||||
'planner_enabled',
|
||||
'writer_enabled',
|
||||
'thinker_enabled',
|
||||
'automation_enabled',
|
||||
'site_builder_enabled',
|
||||
'linker_enabled',
|
||||
'optimizer_enabled',
|
||||
'publisher_enabled',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Module Toggles', {
|
||||
'fields': (
|
||||
'planner_enabled',
|
||||
'writer_enabled',
|
||||
'thinker_enabled',
|
||||
'automation_enabled',
|
||||
'site_builder_enabled',
|
||||
'linker_enabled',
|
||||
'optimizer_enabled',
|
||||
'publisher_enabled',
|
||||
),
|
||||
'description': 'Platform-wide module enable/disable controls. Changes affect all accounts immediately.'
|
||||
}),
|
||||
)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Only allow one instance (singleton)"""
|
||||
return not self.model.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Prevent deletion of singleton"""
|
||||
return False
|
||||
|
||||
# =====================================================================================
|
||||
# GLOBAL SETTINGS ADMIN - Platform-wide defaults
|
||||
# =============================================================================
|
||||
# =====================================================================================
|
||||
|
||||
@admin.register(GlobalIntegrationSettings)
|
||||
class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
|
||||
@@ -370,9 +403,10 @@ class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
|
||||
return not GlobalIntegrationSettings.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Dont allow deletion of singleton"""
|
||||
"""Don't allow deletion of singleton"""
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(GlobalAIPrompt)
|
||||
class GlobalAIPromptAdmin(ExportMixin, Igny8ModelAdmin):
|
||||
"""Admin for global AI prompt templates"""
|
||||
@@ -445,56 +479,3 @@ class GlobalStrategyAdmin(ImportExportMixin, Igny8ModelAdmin):
|
||||
"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',
|
||||
]
|
||||
|
||||
|
||||
@@ -7,6 +7,78 @@ from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class GlobalModuleSettings(models.Model):
|
||||
"""
|
||||
Global module enable/disable settings (platform-wide).
|
||||
|
||||
Singleton model - only one record exists (pk=1).
|
||||
Controls which modules are available across the entire platform.
|
||||
No per-account overrides allowed - this is admin-only control.
|
||||
"""
|
||||
planner_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Planner module platform-wide"
|
||||
)
|
||||
writer_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Writer module platform-wide"
|
||||
)
|
||||
thinker_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Thinker module platform-wide"
|
||||
)
|
||||
automation_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Automation module platform-wide"
|
||||
)
|
||||
site_builder_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Site Builder module platform-wide"
|
||||
)
|
||||
linker_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Linker module platform-wide"
|
||||
)
|
||||
optimizer_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Optimizer module platform-wide"
|
||||
)
|
||||
publisher_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Publisher module platform-wide"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, null=True)
|
||||
updated_at = models.DateTimeField(auto_now=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Global Module Settings"
|
||||
verbose_name_plural = "Global Module Settings"
|
||||
db_table = "igny8_global_module_settings"
|
||||
|
||||
def __str__(self):
|
||||
return "Global Module Settings"
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get or create the singleton instance"""
|
||||
obj, created = cls.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
|
||||
def is_module_enabled(self, module_name: str) -> bool:
|
||||
"""Check if a module is enabled"""
|
||||
field_name = f"{module_name}_enabled"
|
||||
return getattr(self, field_name, False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Enforce singleton pattern"""
|
||||
self.pk = 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Prevent deletion"""
|
||||
pass
|
||||
|
||||
|
||||
class GlobalIntegrationSettings(models.Model):
|
||||
"""
|
||||
Platform-wide API keys and default integration settings.
|
||||
@@ -14,26 +86,12 @@ class GlobalIntegrationSettings(models.Model):
|
||||
|
||||
IMPORTANT:
|
||||
- API keys stored here are used by ALL accounts (no exceptions)
|
||||
- Model selections and parameters are defaults
|
||||
- Model selections and parameters are defaults (linked to AIModelConfig)
|
||||
- Accounts can override model/params via IntegrationSettings model
|
||||
- Free plan: Cannot override, must use these defaults
|
||||
- Starter/Growth/Scale: Can override model, temperature, tokens, etc.
|
||||
"""
|
||||
|
||||
OPENAI_MODEL_CHOICES = [
|
||||
('gpt-4.1', 'GPT-4.1 - $2.00 / $8.00 per 1M tokens'),
|
||||
('gpt-4o-mini', 'GPT-4o mini - $0.15 / $0.60 per 1M tokens'),
|
||||
('gpt-4o', 'GPT-4o - $2.50 / $10.00 per 1M tokens'),
|
||||
('gpt-4-turbo-preview', 'GPT-4 Turbo Preview - $10.00 / $30.00 per 1M tokens'),
|
||||
('gpt-5.1', 'GPT-5.1 - $1.25 / $10.00 per 1M tokens (16K)'),
|
||||
('gpt-5.2', 'GPT-5.2 - $1.75 / $14.00 per 1M tokens (16K)'),
|
||||
]
|
||||
|
||||
DALLE_MODEL_CHOICES = [
|
||||
('dall-e-3', 'DALL·E 3 - $0.040 per image'),
|
||||
('dall-e-2', 'DALL·E 2 - $0.020 per image'),
|
||||
]
|
||||
|
||||
DALLE_SIZE_CHOICES = [
|
||||
('1024x1024', '1024x1024 (Square)'),
|
||||
('1792x1024', '1792x1024 (Landscape)'),
|
||||
@@ -41,22 +99,6 @@ class GlobalIntegrationSettings(models.Model):
|
||||
('512x512', '512x512 (Small Square)'),
|
||||
]
|
||||
|
||||
DALLE_QUALITY_CHOICES = [
|
||||
('standard', 'Standard'),
|
||||
('hd', 'HD'),
|
||||
]
|
||||
|
||||
DALLE_STYLE_CHOICES = [
|
||||
('vivid', 'Vivid'),
|
||||
('natural', 'Natural'),
|
||||
]
|
||||
|
||||
RUNWARE_MODEL_CHOICES = [
|
||||
('runware:97@1', 'Runware 97@1 - Versatile Model'),
|
||||
('runware:100@1', 'Runware 100@1 - High Quality'),
|
||||
('runware:101@1', 'Runware 101@1 - Fast Generation'),
|
||||
]
|
||||
|
||||
IMAGE_QUALITY_CHOICES = [
|
||||
('standard', 'Standard'),
|
||||
('hd', 'HD'),
|
||||
@@ -81,10 +123,13 @@ class GlobalIntegrationSettings(models.Model):
|
||||
blank=True,
|
||||
help_text="Platform OpenAI API key - used by ALL accounts"
|
||||
)
|
||||
openai_model = models.CharField(
|
||||
max_length=100,
|
||||
default='gpt-4o-mini',
|
||||
choices=OPENAI_MODEL_CHOICES,
|
||||
openai_model = models.ForeignKey(
|
||||
'billing.AIModelConfig',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='global_openai_text_model',
|
||||
limit_choices_to={'provider': 'openai', 'model_type': 'text', 'is_active': True},
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Default text generation model (accounts can override if plan allows)"
|
||||
)
|
||||
openai_temperature = models.FloatField(
|
||||
@@ -102,10 +147,13 @@ class GlobalIntegrationSettings(models.Model):
|
||||
blank=True,
|
||||
help_text="Platform DALL-E API key - used by ALL accounts (can be same as OpenAI)"
|
||||
)
|
||||
dalle_model = models.CharField(
|
||||
max_length=100,
|
||||
default='dall-e-3',
|
||||
choices=DALLE_MODEL_CHOICES,
|
||||
dalle_model = models.ForeignKey(
|
||||
'billing.AIModelConfig',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='global_dalle_model',
|
||||
limit_choices_to={'provider': 'openai', 'model_type': 'image', 'is_active': True},
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Default DALL-E model (accounts can override if plan allows)"
|
||||
)
|
||||
dalle_size = models.CharField(
|
||||
@@ -121,10 +169,13 @@ class GlobalIntegrationSettings(models.Model):
|
||||
blank=True,
|
||||
help_text="Platform Runware API key - used by ALL accounts"
|
||||
)
|
||||
runware_model = models.CharField(
|
||||
max_length=100,
|
||||
default='runware:97@1',
|
||||
choices=RUNWARE_MODEL_CHOICES,
|
||||
runware_model = models.ForeignKey(
|
||||
'billing.AIModelConfig',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='global_runware_model',
|
||||
limit_choices_to={'provider': 'runware', 'model_type': 'image', 'is_active': True},
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Default Runware model (accounts can override if plan allows)"
|
||||
)
|
||||
|
||||
@@ -345,74 +396,3 @@ class GlobalStrategy(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.get_category_display()})"
|
||||
|
||||
|
||||
class GlobalModuleSettings(models.Model):
|
||||
"""
|
||||
Global module enable/disable settings (platform-wide).
|
||||
Singleton model - only one record exists (pk=1).
|
||||
Controls which modules are available across the entire platform.
|
||||
No per-account overrides allowed - this is admin-only control.
|
||||
"""
|
||||
planner_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Planner module platform-wide"
|
||||
)
|
||||
writer_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Writer module platform-wide"
|
||||
)
|
||||
thinker_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Thinker module platform-wide"
|
||||
)
|
||||
automation_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Automation module platform-wide"
|
||||
)
|
||||
site_builder_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Site Builder module platform-wide"
|
||||
)
|
||||
linker_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Linker module platform-wide"
|
||||
)
|
||||
optimizer_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Optimizer module platform-wide"
|
||||
)
|
||||
publisher_enabled = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable Publisher module platform-wide"
|
||||
)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_global_module_settings'
|
||||
verbose_name = 'Global Module Settings'
|
||||
verbose_name_plural = 'Global Module Settings'
|
||||
|
||||
def __str__(self):
|
||||
return "Global Module Settings"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Enforce singleton pattern"""
|
||||
self.pk = 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Prevent deletion"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get or create the singleton instance"""
|
||||
obj, created = cls.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
|
||||
def is_module_enabled(self, module_name: str) -> bool:
|
||||
"""Check if a module is enabled"""
|
||||
field_name = f"{module_name}_enabled"
|
||||
return getattr(self, field_name, False)
|
||||
|
||||
@@ -10,7 +10,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from igny8_core.api.base import AccountModelViewSet
|
||||
from igny8_core.api.response import success_response, error_response
|
||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,12 +30,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
Following reference plugin pattern: WordPress uses update_option() for igny8_api_settings
|
||||
We store in IntegrationSettings model with account isolation
|
||||
|
||||
Integration settings configured through Django admin interface.
|
||||
Normal users can view settings but only Admin/Owner roles can modify.
|
||||
IMPORTANT:
|
||||
- GlobalIntegrationSettings (platform-wide API keys): Configured by admins only in Django admin
|
||||
- IntegrationSettings (per-account model preferences): Accessible to all authenticated users
|
||||
- Users can select which models to use but cannot configure API keys (those are platform-wide)
|
||||
|
||||
NOTE: Class-level permissions are [IsAuthenticatedAndActive, HasTenantAccess] only.
|
||||
Individual actions override with IsAdminOrOwner where needed (save, test).
|
||||
task_progress and get_image_generation_settings accessible to all authenticated users.
|
||||
NOTE: All authenticated users with tenant access can configure their integration settings.
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
|
||||
@@ -45,15 +45,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
def get_permissions(self):
|
||||
"""
|
||||
Override permissions based on action.
|
||||
- list, retrieve: authenticated users with tenant access (read-only)
|
||||
- update, save, test: Admin/Owner roles only (write operations)
|
||||
- task_progress, get_image_generation_settings: all authenticated users
|
||||
All authenticated users with tenant access can configure their integration settings.
|
||||
Note: Users can only select models (not configure API keys which are platform-wide in GlobalIntegrationSettings).
|
||||
"""
|
||||
if self.action in ['update', 'save_post', 'test_connection']:
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
|
||||
else:
|
||||
permission_classes = self.permission_classes
|
||||
return [permission() for permission in permission_classes]
|
||||
# All actions use base permissions: IsAuthenticatedAndActive, HasTenantAccess
|
||||
return [permission() for permission in self.permission_classes]
|
||||
|
||||
def list(self, request):
|
||||
"""List all integrations - for debugging URL patterns"""
|
||||
@@ -90,16 +86,73 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
pk = kwargs.get('pk')
|
||||
return self.save_settings(request, pk)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='available-models', url_name='available_models')
|
||||
def available_models(self, request):
|
||||
"""
|
||||
Get available AI models from AIModelConfig
|
||||
Returns models grouped by provider and type
|
||||
"""
|
||||
try:
|
||||
from igny8_core.business.billing.models import AIModelConfig
|
||||
|
||||
# Get all active models
|
||||
models = AIModelConfig.objects.filter(is_active=True).order_by('provider', 'model_type', 'model_name')
|
||||
|
||||
# Group by provider and type
|
||||
grouped_models = {
|
||||
'openai_text': [],
|
||||
'openai_image': [],
|
||||
'runware_image': [],
|
||||
}
|
||||
|
||||
for model in models:
|
||||
# Format display name with pricing
|
||||
if model.model_type == 'text':
|
||||
display_name = f"{model.model_name} - ${model.cost_per_1k_input_tokens:.2f} / ${model.cost_per_1k_output_tokens:.2f} per 1M tokens"
|
||||
else: # image
|
||||
# Calculate cost per image based on tokens_per_credit
|
||||
cost_per_image = (model.tokens_per_credit or 1) * (model.cost_per_1k_input_tokens or 0) / 1000
|
||||
display_name = f"{model.model_name} - ${cost_per_image:.4f} per image"
|
||||
|
||||
model_data = {
|
||||
'value': model.model_name,
|
||||
'label': display_name,
|
||||
'provider': model.provider,
|
||||
'model_type': model.model_type,
|
||||
}
|
||||
|
||||
# Add to appropriate group
|
||||
if model.provider == 'openai' and model.model_type == 'text':
|
||||
grouped_models['openai_text'].append(model_data)
|
||||
elif model.provider == 'openai' and model.model_type == 'image':
|
||||
grouped_models['openai_image'].append(model_data)
|
||||
elif model.provider == 'runware' and model.model_type == 'image':
|
||||
grouped_models['runware_image'].append(model_data)
|
||||
|
||||
return success_response(
|
||||
data=grouped_models,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting available models: {e}", exc_info=True)
|
||||
return error_response(
|
||||
error=f'Failed to get available models: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='test', url_name='test',
|
||||
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner])
|
||||
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess])
|
||||
def test_connection(self, request, pk=None):
|
||||
"""
|
||||
Test API connection using platform API keys.
|
||||
Tests OpenAI or Runware with current model selection.
|
||||
Test API connection for OpenAI or Runware
|
||||
Supports two modes:
|
||||
- with_response=false: Simple connection test (GET /v1/models)
|
||||
- with_response=true: Full response test with ping message
|
||||
"""
|
||||
integration_type = pk # 'openai', 'runware'
|
||||
|
||||
logger.info(f"[test_connection] Called for integration_type={integration_type}")
|
||||
logger.info(f"[test_connection] Called for integration_type={integration_type}, user={getattr(request, 'user', None)}, account={getattr(request, 'account', None)}")
|
||||
|
||||
if not integration_type:
|
||||
return error_response(
|
||||
@@ -108,43 +161,79 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get API key and config from request or saved settings
|
||||
config = request.data.get('config', {}) if isinstance(request.data.get('config'), dict) else {}
|
||||
api_key = request.data.get('apiKey') or config.get('apiKey')
|
||||
|
||||
# Merge request.data with config if config is a dict
|
||||
if not isinstance(config, dict):
|
||||
config = {}
|
||||
|
||||
if not api_key:
|
||||
# Try to get from saved settings (account-specific override)
|
||||
# CRITICAL FIX: Always use user.account directly, never request.account or default account
|
||||
user = getattr(request, 'user', None)
|
||||
account = None
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
logger.info(f"[test_connection] Account from user.account: {account.id if account else None}")
|
||||
|
||||
if account:
|
||||
try:
|
||||
from .models import IntegrationSettings
|
||||
logger.info(f"[test_connection] Looking for saved settings for account {account.id}")
|
||||
saved_settings = IntegrationSettings.objects.get(
|
||||
integration_type=integration_type,
|
||||
account=account
|
||||
)
|
||||
api_key = saved_settings.config.get('apiKey')
|
||||
logger.info(f"[test_connection] Found saved settings, has_apiKey={bool(api_key)}")
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
logger.info(f"[test_connection] No account settings found, will try global settings")
|
||||
pass
|
||||
|
||||
# If still no API key, get from GlobalIntegrationSettings
|
||||
if not api_key:
|
||||
logger.info(f"[test_connection] No API key in request or account settings, checking GlobalIntegrationSettings")
|
||||
try:
|
||||
from .global_settings_models import GlobalIntegrationSettings
|
||||
global_settings = GlobalIntegrationSettings.objects.first()
|
||||
if global_settings:
|
||||
if integration_type == 'openai':
|
||||
api_key = global_settings.openai_api_key
|
||||
elif integration_type == 'runware':
|
||||
api_key = global_settings.runware_api_key
|
||||
logger.info(f"[test_connection] Got API key from GlobalIntegrationSettings, has_key={bool(api_key)}")
|
||||
else:
|
||||
logger.warning(f"[test_connection] No GlobalIntegrationSettings found")
|
||||
except Exception as e:
|
||||
logger.error(f"[test_connection] Error getting global settings: {e}")
|
||||
|
||||
if not api_key:
|
||||
logger.error(f"[test_connection] No API key found in request or saved settings")
|
||||
return error_response(
|
||||
error='API key is required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
logger.info(f"[test_connection] Testing {integration_type} connection with API key (length={len(api_key) if api_key else 0})")
|
||||
try:
|
||||
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
|
||||
|
||||
# Get platform API keys
|
||||
global_settings = GlobalIntegrationSettings.get_instance()
|
||||
|
||||
# Get config from request (model selection)
|
||||
config = request.data.get('config', {}) if isinstance(request.data.get('config'), dict) else {}
|
||||
|
||||
if integration_type == 'openai':
|
||||
api_key = global_settings.openai_api_key
|
||||
if not api_key:
|
||||
return error_response(
|
||||
error='Platform OpenAI API key not configured. Please contact administrator.',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
return self._test_openai(api_key, config, request)
|
||||
|
||||
elif integration_type == 'runware':
|
||||
api_key = global_settings.runware_api_key
|
||||
if not api_key:
|
||||
return error_response(
|
||||
error='Platform Runware API key not configured. Please contact administrator.',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
return self._test_runware(api_key, request)
|
||||
|
||||
else:
|
||||
return error_response(
|
||||
error=f'Testing not supported for {integration_type}',
|
||||
error=f'Validation not supported for {integration_type}',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing {integration_type} connection: {str(e)}", exc_info=True)
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
logger.error(f"Full traceback: {error_trace}")
|
||||
return error_response(
|
||||
error=str(e),
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -323,19 +412,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
from igny8_core.utils.ai_processor import AIProcessor
|
||||
|
||||
# Get account from request
|
||||
account = getattr(request, 'account', None)
|
||||
if not account:
|
||||
user = getattr(request, 'user', None)
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
# Fallback to default account
|
||||
if not account:
|
||||
from igny8_core.auth.models import Account
|
||||
try:
|
||||
account = Account.objects.first()
|
||||
except Exception:
|
||||
pass
|
||||
# Get account from user directly
|
||||
# CRITICAL FIX: Always use user.account, never request.account or default account
|
||||
user = getattr(request, 'user', None)
|
||||
account = None
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
try:
|
||||
# EXACT match to reference plugin: core/admin/ajax.php line 4946-5003
|
||||
@@ -471,24 +553,14 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get account
|
||||
logger.info("[generate_image] Step 1: Getting account")
|
||||
account = getattr(request, 'account', None)
|
||||
if not account:
|
||||
user = getattr(request, 'user', None)
|
||||
logger.info(f"[generate_image] No account in request, checking user: {user}")
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
logger.info(f"[generate_image] Got account from user: {account}")
|
||||
if not account:
|
||||
logger.info("[generate_image] No account found, trying to get first account from DB")
|
||||
from igny8_core.auth.models import Account
|
||||
try:
|
||||
account = Account.objects.first()
|
||||
logger.info(f"[generate_image] Got first account from DB: {account}")
|
||||
except Exception as e:
|
||||
logger.error(f"[generate_image] Error getting account from DB: {e}")
|
||||
pass
|
||||
# Get account from user directly
|
||||
# CRITICAL FIX: Always use user.account, never request.account or default account
|
||||
logger.info("[generate_image] Step 1: Getting account from user")
|
||||
user = getattr(request, 'user', None)
|
||||
account = None
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
logger.info(f"[generate_image] Got account from user: {account}")
|
||||
|
||||
if not account:
|
||||
logger.error("[generate_image] ERROR: No account found, returning error response")
|
||||
@@ -633,15 +705,18 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
return self.save_settings(request, integration_type)
|
||||
|
||||
def save_settings(self, request, pk=None):
|
||||
"""
|
||||
Save integration settings (account overrides only).
|
||||
- Saves model/parameter overrides to IntegrationSettings
|
||||
- NEVER saves API keys (those are platform-wide)
|
||||
- Free plan: Should be blocked at frontend level
|
||||
"""
|
||||
integration_type = pk
|
||||
"""Save integration settings"""
|
||||
integration_type = pk # 'openai', 'runware', 'gsc'
|
||||
|
||||
logger.info(f"[save_settings] Called for integration_type={integration_type}, user={getattr(request, 'user', None)}, account={getattr(request, 'account', None)}")
|
||||
# DEBUG: Log everything about the request
|
||||
logger.info(f"[save_settings] === START DEBUG ===")
|
||||
logger.info(f"[save_settings] integration_type={integration_type}")
|
||||
logger.info(f"[save_settings] request.user={getattr(request, 'user', None)}")
|
||||
logger.info(f"[save_settings] request.user.id={getattr(getattr(request, 'user', None), 'id', None)}")
|
||||
logger.info(f"[save_settings] request.account={getattr(request, 'account', None)}")
|
||||
logger.info(f"[save_settings] request.account.id={getattr(getattr(request, 'account', None), 'id', None) if hasattr(request, 'account') and request.account else 'NO ACCOUNT'}")
|
||||
logger.info(f"[save_settings] request.account.name={getattr(getattr(request, 'account', None), 'name', None) if hasattr(request, 'account') and request.account else 'NO ACCOUNT'}")
|
||||
logger.info(f"[save_settings] === END DEBUG ===")
|
||||
|
||||
if not integration_type:
|
||||
return error_response(
|
||||
@@ -654,117 +729,258 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
config = dict(request.data) if hasattr(request.data, 'dict') else (request.data if isinstance(request.data, dict) else {})
|
||||
logger.info(f"[save_settings] Config keys: {list(config.keys()) if isinstance(config, dict) else 'Not a dict'}")
|
||||
|
||||
# Remove any API keys from config (security - they shouldn't be sent but just in case)
|
||||
config.pop('apiKey', None)
|
||||
config.pop('api_key', None)
|
||||
config.pop('openai_api_key', None)
|
||||
config.pop('dalle_api_key', None)
|
||||
config.pop('runware_api_key', None)
|
||||
config.pop('anthropic_api_key', None)
|
||||
|
||||
try:
|
||||
# Get account
|
||||
account = getattr(request, 'account', None)
|
||||
logger.info(f"[save_settings] Account from request: {account.id if account else None}")
|
||||
|
||||
if not account:
|
||||
user = getattr(request, 'user', None)
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
try:
|
||||
account = getattr(user, 'account', None)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting account from user: {e}")
|
||||
account = None
|
||||
|
||||
if not account:
|
||||
logger.error(f"[save_settings] No account found")
|
||||
# CRITICAL FIX: Always get account from authenticated user, not from request.account
|
||||
# request.account can be manipulated or set incorrectly by middleware/auth
|
||||
# The user's account relationship is the source of truth for their integration settings
|
||||
user = getattr(request, 'user', None)
|
||||
if not user or not hasattr(user, 'is_authenticated') or not user.is_authenticated:
|
||||
logger.error(f"[save_settings] User not authenticated")
|
||||
return error_response(
|
||||
error='Account not found. Please ensure you are logged in.',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
error='Authentication required',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
logger.info(f"[save_settings] Using account: {account.id} ({account.name})")
|
||||
# Get account directly from user.account relationship
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
# TODO: Check if Free plan - they shouldn't be able to save overrides
|
||||
# This should be blocked at frontend level, but add backend check too
|
||||
# CRITICAL SECURITY CHECK: Prevent saving to system accounts
|
||||
if account and account.slug in ['aws-admin', 'system']:
|
||||
logger.error(f"[save_settings] BLOCKED: Attempt to save to system account {account.slug} by user {user.id}")
|
||||
logger.error(f"[save_settings] This indicates the user's account field is incorrectly set to a system account")
|
||||
return error_response(
|
||||
error=f'Cannot save integration settings: Your user account is incorrectly linked to system account "{account.slug}". Please contact administrator.',
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
request=request
|
||||
)
|
||||
|
||||
logger.info(f"[save_settings] Account from user.account: {account.id if account else None}")
|
||||
|
||||
# CRITICAL: Require valid account - do NOT allow saving without proper account
|
||||
if not account:
|
||||
logger.error(f"[save_settings] No account found for user {user.id} ({user.email})")
|
||||
return error_response(
|
||||
error='Account not found. Please ensure your user has an account assigned.',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
logger.info(f"[save_settings] Using account: {account.id} ({account.name}, slug={account.slug}, status={account.status})")
|
||||
|
||||
# Store integration settings in a simple model or settings table
|
||||
# For now, we'll use a simple approach - store in IntegrationSettings model
|
||||
# or use Django settings/database
|
||||
|
||||
# Import IntegrationSettings model
|
||||
from .models import IntegrationSettings
|
||||
|
||||
# Build clean config with only allowed overrides
|
||||
clean_config = {}
|
||||
|
||||
if integration_type == 'openai':
|
||||
# Only allow model, temperature, max_tokens overrides
|
||||
if 'model' in config:
|
||||
clean_config['model'] = config['model']
|
||||
if 'temperature' in config:
|
||||
clean_config['temperature'] = config['temperature']
|
||||
if 'max_tokens' in config:
|
||||
clean_config['max_tokens'] = config['max_tokens']
|
||||
|
||||
elif integration_type == 'image_generation':
|
||||
# For image_generation, ensure provider is set correctly
|
||||
if integration_type == 'image_generation':
|
||||
# Map service to provider if service is provided
|
||||
if 'service' in config:
|
||||
clean_config['service'] = config['service']
|
||||
clean_config['provider'] = config['service']
|
||||
if 'provider' in config:
|
||||
clean_config['provider'] = config['provider']
|
||||
clean_config['service'] = config['provider']
|
||||
|
||||
# Model selection (service-specific)
|
||||
if 'model' in config:
|
||||
clean_config['model'] = config['model']
|
||||
if 'imageModel' in config:
|
||||
clean_config['imageModel'] = config['imageModel']
|
||||
clean_config['model'] = config['imageModel'] # Also store in 'model' for consistency
|
||||
if 'runwareModel' in config:
|
||||
clean_config['runwareModel'] = config['runwareModel']
|
||||
|
||||
# Universal image settings (applies to all providers)
|
||||
for key in ['image_type', 'image_quality', 'image_style', 'max_in_article_images', 'image_format',
|
||||
'desktop_enabled', 'mobile_enabled', 'featured_image_size', 'desktop_image_size']:
|
||||
if key in config:
|
||||
clean_config[key] = config[key]
|
||||
if 'service' in config and 'provider' not in config:
|
||||
config['provider'] = config['service']
|
||||
# Ensure provider is set
|
||||
if 'provider' not in config:
|
||||
config['provider'] = config.get('service', 'openai')
|
||||
# Set model based on provider
|
||||
if config.get('provider') == 'openai' and 'model' not in config:
|
||||
config['model'] = config.get('imageModel', 'dall-e-3')
|
||||
elif config.get('provider') == 'runware' and 'model' not in config:
|
||||
config['model'] = config.get('runwareModel', 'runware:97@1')
|
||||
# Ensure all image settings have defaults (except max_in_article_images which must be explicitly set)
|
||||
config.setdefault('image_type', 'realistic')
|
||||
config.setdefault('image_format', 'webp')
|
||||
config.setdefault('desktop_enabled', True)
|
||||
config.setdefault('mobile_enabled', True)
|
||||
|
||||
# Set default image sizes based on provider/model
|
||||
provider = config.get('provider', 'openai')
|
||||
model = config.get('model', 'dall-e-3')
|
||||
|
||||
if not config.get('featured_image_size'):
|
||||
if provider == 'runware':
|
||||
config['featured_image_size'] = '1280x832'
|
||||
else: # openai
|
||||
config['featured_image_size'] = '1024x1024'
|
||||
|
||||
if not config.get('desktop_image_size'):
|
||||
config['desktop_image_size'] = '1024x1024'
|
||||
|
||||
# Get or create integration settings
|
||||
logger.info(f"[save_settings] Saving clean config: {clean_config}")
|
||||
integration_settings, created = IntegrationSettings.objects.get_or_create(
|
||||
integration_type=integration_type,
|
||||
account=account,
|
||||
defaults={'config': clean_config, 'is_active': True}
|
||||
)
|
||||
logger.info(f"[save_settings] Result: created={created}, id={integration_settings.id}")
|
||||
# Check if user is changing from global defaults
|
||||
# Only save IntegrationSettings if config differs from global defaults
|
||||
global_defaults = self._get_global_defaults(integration_type)
|
||||
|
||||
if not created:
|
||||
integration_settings.config = clean_config
|
||||
integration_settings.is_active = True
|
||||
integration_settings.save()
|
||||
logger.info(f"[save_settings] Updated existing settings")
|
||||
# Compare config with global defaults (excluding 'enabled' and 'id' fields)
|
||||
config_without_metadata = {k: v for k, v in config.items() if k not in ['enabled', 'id']}
|
||||
defaults_without_keys = {k: v for k, v in global_defaults.items() if k not in ['apiKey', 'id']}
|
||||
|
||||
logger.info(f"[save_settings] Successfully saved overrides for {integration_type}")
|
||||
# Check if user is actually changing model or other settings from defaults
|
||||
is_custom_config = False
|
||||
for key, value in config_without_metadata.items():
|
||||
default_value = defaults_without_keys.get(key)
|
||||
if default_value is not None and str(value) != str(default_value):
|
||||
is_custom_config = True
|
||||
logger.info(f"[save_settings] Custom value detected: {key}={value} (default={default_value})")
|
||||
break
|
||||
|
||||
# Get global enabled status
|
||||
from .global_settings_models import GlobalIntegrationSettings
|
||||
global_settings_obj = GlobalIntegrationSettings.objects.first()
|
||||
global_enabled = False
|
||||
if global_settings_obj:
|
||||
if integration_type == 'openai':
|
||||
global_enabled = bool(global_settings_obj.openai_api_key)
|
||||
elif integration_type == 'runware':
|
||||
global_enabled = bool(global_settings_obj.runware_api_key)
|
||||
elif integration_type == 'image_generation':
|
||||
global_enabled = bool(global_settings_obj.openai_api_key or global_settings_obj.runware_api_key)
|
||||
|
||||
user_enabled = config.get('enabled', False)
|
||||
|
||||
# Save enable/disable state in IntegrationState model (single record per account)
|
||||
from igny8_core.ai.models import IntegrationState
|
||||
|
||||
# Map integration_type to field name
|
||||
field_map = {
|
||||
'openai': 'is_openai_enabled',
|
||||
'runware': 'is_runware_enabled',
|
||||
'image_generation': 'is_image_generation_enabled',
|
||||
}
|
||||
|
||||
field_name = field_map.get(integration_type)
|
||||
if not field_name:
|
||||
logger.error(f"[save_settings] Unknown integration_type: {integration_type}")
|
||||
else:
|
||||
logger.info(f"[save_settings] === CRITICAL DEBUG START ===")
|
||||
logger.info(f"[save_settings] About to save IntegrationState for integration_type={integration_type}")
|
||||
logger.info(f"[save_settings] Field name to update: {field_name}")
|
||||
logger.info(f"[save_settings] Account being used: ID={account.id}, Name={account.name}, Slug={account.slug}")
|
||||
logger.info(f"[save_settings] User enabled value: {user_enabled}")
|
||||
logger.info(f"[save_settings] Request user: ID={request.user.id}, Email={request.user.email}")
|
||||
logger.info(f"[save_settings] Request user account: ID={request.user.account.id if request.user.account else None}")
|
||||
|
||||
integration_state, created = IntegrationState.objects.get_or_create(
|
||||
account=account,
|
||||
defaults={
|
||||
'is_openai_enabled': True,
|
||||
'is_runware_enabled': True,
|
||||
'is_image_generation_enabled': True,
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"[save_settings] IntegrationState {'CREATED' if created else 'RETRIEVED'}")
|
||||
logger.info(f"[save_settings] IntegrationState.account: ID={integration_state.account.id}, Name={integration_state.account.name}")
|
||||
logger.info(f"[save_settings] Before update: {field_name}={getattr(integration_state, field_name)}")
|
||||
|
||||
# Update the specific field
|
||||
setattr(integration_state, field_name, user_enabled)
|
||||
integration_state.save()
|
||||
|
||||
logger.info(f"[save_settings] After update: {field_name}={getattr(integration_state, field_name)}")
|
||||
logger.info(f"[save_settings] IntegrationState saved to database")
|
||||
logger.info(f"[save_settings] === CRITICAL DEBUG END ===")
|
||||
|
||||
# Save custom config only if different from global defaults
|
||||
if is_custom_config:
|
||||
# User has custom settings (different model, etc.) - save override
|
||||
logger.info(f"[save_settings] User has custom config, saving IntegrationSettings")
|
||||
integration_settings, created = IntegrationSettings.objects.get_or_create(
|
||||
integration_type=integration_type,
|
||||
account=account,
|
||||
defaults={'config': config_without_metadata, 'is_active': True}
|
||||
)
|
||||
|
||||
if not created:
|
||||
integration_settings.config = config_without_metadata
|
||||
integration_settings.save()
|
||||
logger.info(f"[save_settings] Updated IntegrationSettings config")
|
||||
else:
|
||||
logger.info(f"[save_settings] Created new IntegrationSettings for custom config")
|
||||
else:
|
||||
# Config matches global defaults - delete any existing override
|
||||
logger.info(f"[save_settings] User settings match global defaults, removing any account override")
|
||||
deleted_count, _ = IntegrationSettings.objects.filter(
|
||||
integration_type=integration_type,
|
||||
account=account
|
||||
).delete()
|
||||
if deleted_count > 0:
|
||||
logger.info(f"[save_settings] Deleted {deleted_count} IntegrationSettings override(s)")
|
||||
|
||||
logger.info(f"[save_settings] Successfully saved settings for {integration_type}")
|
||||
return success_response(
|
||||
data={'config': clean_config},
|
||||
data={'config': config},
|
||||
message=f'{integration_type.upper()} settings saved successfully',
|
||||
request=request
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving integration settings for {integration_type}: {str(e)}", exc_info=True)
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
logger.error(f"Full traceback: {error_trace}")
|
||||
return error_response(
|
||||
error=f'Failed to save settings: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
def _get_global_defaults(self, integration_type):
|
||||
"""Get global defaults from GlobalIntegrationSettings"""
|
||||
try:
|
||||
from .global_settings_models import GlobalIntegrationSettings
|
||||
global_settings = GlobalIntegrationSettings.objects.first()
|
||||
|
||||
if not global_settings:
|
||||
return {}
|
||||
|
||||
defaults = {}
|
||||
|
||||
# Map integration_type to GlobalIntegrationSettings fields
|
||||
if integration_type == 'openai':
|
||||
defaults = {
|
||||
'apiKey': global_settings.openai_api_key or '',
|
||||
'model': global_settings.openai_model.model_name if global_settings.openai_model else 'gpt-4o-mini',
|
||||
'temperature': float(global_settings.openai_temperature or 0.7),
|
||||
'maxTokens': int(global_settings.openai_max_tokens or 8192),
|
||||
}
|
||||
elif integration_type == 'runware':
|
||||
defaults = {
|
||||
'apiKey': global_settings.runware_api_key or '',
|
||||
'model': global_settings.runware_model.model_name if global_settings.runware_model else 'runware:97@1',
|
||||
}
|
||||
elif integration_type == 'image_generation':
|
||||
provider = global_settings.default_image_service or 'openai'
|
||||
# Get model based on provider
|
||||
if provider == 'openai':
|
||||
model = global_settings.dalle_model.model_name if global_settings.dalle_model else 'dall-e-3'
|
||||
else: # runware
|
||||
model = global_settings.runware_model.model_name if global_settings.runware_model else 'runware:97@1'
|
||||
|
||||
defaults = {
|
||||
'provider': provider,
|
||||
'service': provider, # Alias
|
||||
'model': model,
|
||||
'imageModel': model if provider == 'openai' else None,
|
||||
'runwareModel': model if provider == 'runware' else None,
|
||||
'image_type': global_settings.image_style or 'vivid',
|
||||
'image_quality': global_settings.image_quality or 'standard',
|
||||
'max_in_article_images': global_settings.max_in_article_images or 5,
|
||||
'desktop_image_size': global_settings.desktop_image_size or '1024x1024',
|
||||
'mobile_image_size': global_settings.mobile_image_size or '512x512',
|
||||
'featured_image_size': global_settings.desktop_image_size or '1024x1024',
|
||||
'desktop_enabled': True,
|
||||
'mobile_enabled': True,
|
||||
}
|
||||
|
||||
logger.info(f"[_get_global_defaults] {integration_type} defaults: {defaults}")
|
||||
return defaults
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting global defaults for {integration_type}: {e}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def get_settings(self, request, pk=None):
|
||||
"""
|
||||
Get integration settings for frontend.
|
||||
Returns:
|
||||
- Global defaults (model, temperature, etc.)
|
||||
- Account overrides if they exist
|
||||
- NO API keys (platform-wide only)
|
||||
"""
|
||||
"""Get integration settings - merges global defaults with account-specific overrides"""
|
||||
integration_type = pk
|
||||
|
||||
if not integration_type:
|
||||
@@ -775,127 +991,128 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
)
|
||||
|
||||
try:
|
||||
# Get account
|
||||
account = getattr(request, 'account', None)
|
||||
# CRITICAL FIX: Always get account from authenticated user, not from request.account
|
||||
# Match the pattern used in save_settings() for consistency
|
||||
user = getattr(request, 'user', None)
|
||||
if not user or not hasattr(user, 'is_authenticated') or not user.is_authenticated:
|
||||
logger.error(f"[get_settings] User not authenticated")
|
||||
return error_response(
|
||||
error='Authentication required',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
if not account:
|
||||
user = getattr(request, 'user', None)
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
try:
|
||||
account = getattr(user, 'account', None)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting account from user: {e}")
|
||||
account = None
|
||||
# Get account directly from user.account relationship
|
||||
account = getattr(user, 'account', None)
|
||||
logger.info(f"[get_settings] Account from user.account: {account.id if account else None}")
|
||||
|
||||
from .models import IntegrationSettings
|
||||
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings
|
||||
|
||||
# Get global defaults
|
||||
global_settings = GlobalIntegrationSettings.get_instance()
|
||||
# Start with global defaults
|
||||
global_defaults = self._get_global_defaults(integration_type)
|
||||
|
||||
# Build response with global defaults
|
||||
if integration_type == 'openai':
|
||||
response_data = {
|
||||
'id': 'openai',
|
||||
'enabled': True, # Always enabled (platform-wide)
|
||||
'model': global_settings.openai_model,
|
||||
'temperature': global_settings.openai_temperature,
|
||||
'max_tokens': global_settings.openai_max_tokens,
|
||||
'using_global': True, # Flag to show it's using global
|
||||
}
|
||||
|
||||
# Check for account overrides
|
||||
if account:
|
||||
try:
|
||||
integration_settings = IntegrationSettings.objects.get(
|
||||
integration_type=integration_type,
|
||||
account=account,
|
||||
is_active=True
|
||||
)
|
||||
config = integration_settings.config or {}
|
||||
if config.get('model'):
|
||||
response_data['model'] = config['model']
|
||||
response_data['using_global'] = False
|
||||
if config.get('temperature') is not None:
|
||||
response_data['temperature'] = config['temperature']
|
||||
if config.get('max_tokens'):
|
||||
response_data['max_tokens'] = config['max_tokens']
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
pass
|
||||
|
||||
elif integration_type == 'runware':
|
||||
response_data = {
|
||||
'id': 'runware',
|
||||
'enabled': True, # Always enabled (platform-wide)
|
||||
'using_global': True,
|
||||
}
|
||||
|
||||
elif integration_type == 'image_generation':
|
||||
# Get default service and model based on global settings
|
||||
default_service = global_settings.default_image_service
|
||||
default_model = global_settings.dalle_model if default_service == 'openai' else global_settings.runware_model
|
||||
|
||||
response_data = {
|
||||
'id': 'image_generation',
|
||||
'enabled': True,
|
||||
'service': default_service, # From global settings
|
||||
'provider': default_service, # Alias for service
|
||||
'model': default_model, # Service-specific default model
|
||||
'imageModel': global_settings.dalle_model, # OpenAI model
|
||||
'runwareModel': global_settings.runware_model, # Runware model
|
||||
'image_type': global_settings.image_style, # Use image_style as default
|
||||
'image_quality': global_settings.image_quality, # Universal quality
|
||||
'image_style': global_settings.image_style, # Universal style
|
||||
'max_in_article_images': global_settings.max_in_article_images,
|
||||
'image_format': 'webp',
|
||||
'desktop_enabled': True,
|
||||
'mobile_enabled': True,
|
||||
'featured_image_size': global_settings.dalle_size,
|
||||
'desktop_image_size': global_settings.desktop_image_size,
|
||||
'mobile_image_size': global_settings.mobile_image_size,
|
||||
'using_global': True,
|
||||
}
|
||||
|
||||
# Check for account overrides
|
||||
if account:
|
||||
try:
|
||||
integration_settings = IntegrationSettings.objects.get(
|
||||
integration_type=integration_type,
|
||||
account=account,
|
||||
is_active=True
|
||||
)
|
||||
config = integration_settings.config or {}
|
||||
# Override with account settings
|
||||
if config:
|
||||
response_data['using_global'] = False
|
||||
# Service/provider
|
||||
if 'service' in config:
|
||||
response_data['service'] = config['service']
|
||||
response_data['provider'] = config['service']
|
||||
if 'provider' in config:
|
||||
response_data['provider'] = config['provider']
|
||||
response_data['service'] = config['provider']
|
||||
# Models
|
||||
if 'model' in config:
|
||||
response_data['model'] = config['model']
|
||||
if 'imageModel' in config:
|
||||
response_data['imageModel'] = config['imageModel']
|
||||
if 'runwareModel' in config:
|
||||
response_data['runwareModel'] = config['runwareModel']
|
||||
# Universal image settings
|
||||
for key in ['image_type', 'image_quality', 'image_style', 'max_in_article_images', 'image_format',
|
||||
'desktop_enabled', 'mobile_enabled', 'featured_image_size', 'desktop_image_size']:
|
||||
if key in config:
|
||||
response_data[key] = config[key]
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
pass
|
||||
# Get account-specific settings and merge
|
||||
# Get account-specific enabled state from IntegrationState (single record)
|
||||
from igny8_core.ai.models import IntegrationState
|
||||
|
||||
# Map integration_type to field name
|
||||
field_map = {
|
||||
'openai': 'is_openai_enabled',
|
||||
'runware': 'is_runware_enabled',
|
||||
'image_generation': 'is_image_generation_enabled',
|
||||
}
|
||||
|
||||
account_enabled = None
|
||||
if account:
|
||||
try:
|
||||
integration_state = IntegrationState.objects.get(account=account)
|
||||
field_name = field_map.get(integration_type)
|
||||
if field_name:
|
||||
account_enabled = getattr(integration_state, field_name)
|
||||
logger.info(f"[get_settings] Found IntegrationState.{field_name}={account_enabled}")
|
||||
except IntegrationState.DoesNotExist:
|
||||
logger.info(f"[get_settings] No IntegrationState found, will use global default")
|
||||
|
||||
# Try to get account-specific config overrides
|
||||
if account:
|
||||
try:
|
||||
integration_settings = IntegrationSettings.objects.get(
|
||||
integration_type=integration_type,
|
||||
account=account
|
||||
)
|
||||
# Merge: global defaults + account overrides
|
||||
merged_config = {**global_defaults, **integration_settings.config}
|
||||
|
||||
# Use account-specific enabled state if available, otherwise use global
|
||||
if account_enabled is not None:
|
||||
enabled_state = account_enabled
|
||||
else:
|
||||
# Fall back to global enabled logic
|
||||
try:
|
||||
from .global_settings_models import GlobalIntegrationSettings
|
||||
global_settings = GlobalIntegrationSettings.objects.first()
|
||||
if global_settings:
|
||||
if integration_type == 'openai':
|
||||
enabled_state = bool(global_settings.openai_api_key)
|
||||
elif integration_type == 'runware':
|
||||
enabled_state = bool(global_settings.runware_api_key)
|
||||
elif integration_type == 'image_generation':
|
||||
enabled_state = bool(global_settings.openai_api_key or global_settings.runware_api_key)
|
||||
else:
|
||||
enabled_state = False
|
||||
else:
|
||||
enabled_state = False
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking global enabled status: {e}")
|
||||
enabled_state = False
|
||||
|
||||
response_data = {
|
||||
'id': integration_settings.integration_type,
|
||||
'enabled': enabled_state,
|
||||
**merged_config
|
||||
}
|
||||
logger.info(f"[get_settings] Merged settings for {integration_type}: enabled={enabled_state}")
|
||||
return success_response(
|
||||
data=response_data,
|
||||
request=request
|
||||
)
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
logger.info(f"[get_settings] No account settings, returning global defaults for {integration_type}")
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting account-specific settings: {e}", exc_info=True)
|
||||
|
||||
# Return global defaults with account-specific enabled state if available
|
||||
# Determine if integration is "enabled" based on IntegrationState or global configuration
|
||||
if account_enabled is not None:
|
||||
is_enabled = account_enabled
|
||||
logger.info(f"[get_settings] Using account IntegrationState: enabled={is_enabled}")
|
||||
else:
|
||||
# Other integration types - return empty
|
||||
response_data = {
|
||||
'id': integration_type,
|
||||
'enabled': False,
|
||||
}
|
||||
try:
|
||||
from .global_settings_models import GlobalIntegrationSettings
|
||||
global_settings = GlobalIntegrationSettings.objects.first()
|
||||
|
||||
# Check if global API keys are configured
|
||||
is_enabled = False
|
||||
if global_settings:
|
||||
if integration_type == 'openai':
|
||||
is_enabled = bool(global_settings.openai_api_key)
|
||||
elif integration_type == 'runware':
|
||||
is_enabled = bool(global_settings.runware_api_key)
|
||||
elif integration_type == 'image_generation':
|
||||
# Image generation is enabled if either OpenAI or Runware is configured
|
||||
is_enabled = bool(global_settings.openai_api_key or global_settings.runware_api_key)
|
||||
|
||||
logger.info(f"[get_settings] Using global enabled status: enabled={is_enabled} (no account override)")
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking global enabled status: {e}")
|
||||
is_enabled = False
|
||||
|
||||
response_data = {
|
||||
'id': integration_type,
|
||||
'enabled': is_enabled,
|
||||
**global_defaults
|
||||
}
|
||||
return success_response(
|
||||
data=response_data,
|
||||
request=request
|
||||
@@ -910,23 +1127,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='image_generation', url_name='image_generation_settings')
|
||||
def get_image_generation_settings(self, request):
|
||||
"""Get image generation settings for current account
|
||||
Normal users fallback to system account (aws-admin) settings
|
||||
"""
|
||||
account = getattr(request, 'account', None)
|
||||
|
||||
if not account:
|
||||
# Fallback to user's account
|
||||
user = getattr(request, 'user', None)
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
# Fallback to default account
|
||||
if not account:
|
||||
from igny8_core.auth.models import Account
|
||||
try:
|
||||
account = Account.objects.first()
|
||||
except Exception:
|
||||
pass
|
||||
"""Get image generation settings for current account - merges global defaults with account overrides"""
|
||||
# CRITICAL FIX: Always use user.account directly, never request.account or default account
|
||||
user = getattr(request, 'user', None)
|
||||
account = None
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
if not account:
|
||||
return error_response(
|
||||
@@ -937,42 +1143,26 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
|
||||
try:
|
||||
from .models import IntegrationSettings
|
||||
from igny8_core.auth.models import Account
|
||||
|
||||
# Try to get settings for user's account first
|
||||
# Start with global defaults
|
||||
global_defaults = self._get_global_defaults('image_generation')
|
||||
|
||||
# Try to get account-specific settings
|
||||
try:
|
||||
integration = IntegrationSettings.objects.get(
|
||||
account=account,
|
||||
integration_type='image_generation',
|
||||
is_active=True
|
||||
)
|
||||
logger.info(f"[get_image_generation_settings] Found settings for account {account.id}")
|
||||
config = {**global_defaults, **(integration.config or {})}
|
||||
logger.info(f"[get_image_generation_settings] Found account settings, merged with globals")
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
# Fallback to system account (aws-admin) settings - normal users use centralized settings
|
||||
logger.info(f"[get_image_generation_settings] No settings for account {account.id}, falling back to system account")
|
||||
try:
|
||||
system_account = Account.objects.get(slug='aws-admin')
|
||||
integration = IntegrationSettings.objects.get(
|
||||
account=system_account,
|
||||
integration_type='image_generation',
|
||||
is_active=True
|
||||
)
|
||||
logger.info(f"[get_image_generation_settings] Using system account (aws-admin) settings")
|
||||
except (Account.DoesNotExist, IntegrationSettings.DoesNotExist):
|
||||
logger.error("[get_image_generation_settings] No image generation settings found in aws-admin account")
|
||||
return error_response(
|
||||
error='Image generation settings not configured in aws-admin account',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
config = integration.config or {}
|
||||
# Use global defaults only
|
||||
config = global_defaults
|
||||
logger.info(f"[get_image_generation_settings] No account settings, using global defaults")
|
||||
|
||||
# Debug: Log what's actually in the config
|
||||
logger.info(f"[get_image_generation_settings] Full config: {config}")
|
||||
logger.info(f"[get_image_generation_settings] Config keys: {list(config.keys())}")
|
||||
logger.info(f"[get_image_generation_settings] model field: {config.get('model')}")
|
||||
logger.info(f"[get_image_generation_settings] imageModel field: {config.get('imageModel')}")
|
||||
logger.info(f"[get_image_generation_settings] Final config: {config}")
|
||||
|
||||
# Get model - try 'model' first, then 'imageModel' as fallback
|
||||
model = config.get('model') or config.get('imageModel') or 'dall-e-3'
|
||||
@@ -997,12 +1187,6 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
},
|
||||
request=request
|
||||
)
|
||||
except IntegrationSettings.DoesNotExist:
|
||||
return error_response(
|
||||
error='Image generation settings not configured',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[get_image_generation_settings] Error: {str(e)}", exc_info=True)
|
||||
return error_response(
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-23 05:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0019_add_ai_model_config'),
|
||||
('system', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='integrationsettings',
|
||||
name='default_image_model',
|
||||
field=models.ForeignKey(blank=True, help_text='Default AI model for image generation operations', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='image_integrations', to='billing.aimodelconfig'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='integrationsettings',
|
||||
name='default_text_model',
|
||||
field=models.ForeignKey(blank=True, help_text='Default AI model for text generation operations', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='text_integrations', to='billing.aimodelconfig'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django on 2025-12-23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0002_add_model_fk_to_integrations'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GlobalModuleSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('planner_enabled', models.BooleanField(default=True, help_text='Enable Planner module platform-wide')),
|
||||
('writer_enabled', models.BooleanField(default=True, help_text='Enable Writer module platform-wide')),
|
||||
('thinker_enabled', models.BooleanField(default=True, help_text='Enable Thinker module platform-wide')),
|
||||
('automation_enabled', models.BooleanField(default=True, help_text='Enable Automation module platform-wide')),
|
||||
('site_builder_enabled', models.BooleanField(default=True, help_text='Enable Site Builder module platform-wide')),
|
||||
('linker_enabled', models.BooleanField(default=True, help_text='Enable Linker module platform-wide')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global Module Settings',
|
||||
'verbose_name_plural': 'Global Module Settings',
|
||||
'db_table': 'igny8_global_module_settings',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,106 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-23 08:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0003_globalmodulesettings'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GlobalAIPrompt',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('prompt_type', models.CharField(choices=[('clustering', 'Clustering'), ('ideas', 'Ideas Generation'), ('content_generation', 'Content Generation'), ('image_prompt_extraction', 'Image Prompt Extraction'), ('image_prompt_template', 'Image Prompt Template'), ('negative_prompt', 'Negative Prompt'), ('site_structure_generation', 'Site Structure Generation'), ('product_generation', 'Product Content Generation'), ('service_generation', 'Service Page Generation'), ('taxonomy_generation', 'Taxonomy Generation')], help_text='Type of AI operation this prompt is for', max_length=50, unique=True)),
|
||||
('prompt_value', models.TextField(help_text='Default prompt template')),
|
||||
('description', models.TextField(blank=True, help_text='Description of what this prompt does')),
|
||||
('variables', models.JSONField(blank=True, default=list, help_text='Optional: List of variables used in the prompt (e.g., {keyword}, {industry})')),
|
||||
('is_active', models.BooleanField(db_index=True, default=True)),
|
||||
('version', models.IntegerField(default=1, help_text='Prompt version for tracking changes')),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global AI Prompt',
|
||||
'verbose_name_plural': 'Global AI Prompts',
|
||||
'db_table': 'igny8_global_ai_prompts',
|
||||
'ordering': ['prompt_type'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GlobalAuthorProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text="Profile name (e.g., 'SaaS B2B Professional')", max_length=255, unique=True)),
|
||||
('description', models.TextField(help_text='Description of the writing style')),
|
||||
('tone', models.CharField(help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical')", max_length=100)),
|
||||
('language', models.CharField(default='en', help_text='Language code', max_length=50)),
|
||||
('structure_template', models.JSONField(default=dict, help_text='Structure template defining content sections')),
|
||||
('category', models.CharField(choices=[('saas', 'SaaS/B2B'), ('ecommerce', 'E-commerce'), ('blog', 'Blog/Publishing'), ('technical', 'Technical'), ('creative', 'Creative'), ('news', 'News/Media'), ('academic', 'Academic')], help_text='Profile category', max_length=50)),
|
||||
('is_active', models.BooleanField(db_index=True, default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global Author Profile',
|
||||
'verbose_name_plural': 'Global Author Profiles',
|
||||
'db_table': 'igny8_global_author_profiles',
|
||||
'ordering': ['category', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GlobalStrategy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Strategy name', max_length=255, unique=True)),
|
||||
('description', models.TextField(help_text='Description of the content strategy')),
|
||||
('prompt_types', models.JSONField(default=list, help_text='List of prompt types to use')),
|
||||
('section_logic', models.JSONField(default=dict, help_text='Section logic configuration')),
|
||||
('category', models.CharField(choices=[('blog', 'Blog Content'), ('ecommerce', 'E-commerce'), ('saas', 'SaaS/B2B'), ('news', 'News/Media'), ('technical', 'Technical Documentation'), ('marketing', 'Marketing Content')], help_text='Strategy category', max_length=50)),
|
||||
('is_active', models.BooleanField(db_index=True, default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global Strategy',
|
||||
'verbose_name_plural': 'Global Strategies',
|
||||
'db_table': 'igny8_global_strategies',
|
||||
'ordering': ['category', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GlobalIntegrationSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('openai_api_key', models.CharField(blank=True, help_text='Platform OpenAI API key - used by ALL accounts', max_length=500)),
|
||||
('openai_model', models.CharField(choices=[('gpt-4.1', 'GPT-4.1 - $2.00 / $8.00 per 1M tokens'), ('gpt-4o-mini', 'GPT-4o mini - $0.15 / $0.60 per 1M tokens'), ('gpt-4o', 'GPT-4o - $2.50 / $10.00 per 1M tokens'), ('gpt-4-turbo-preview', 'GPT-4 Turbo Preview - $10.00 / $30.00 per 1M tokens'), ('gpt-5.1', 'GPT-5.1 - $1.25 / $10.00 per 1M tokens (16K)'), ('gpt-5.2', 'GPT-5.2 - $1.75 / $14.00 per 1M tokens (16K)')], default='gpt-4o-mini', help_text='Default text generation model (accounts can override if plan allows)', max_length=100)),
|
||||
('openai_temperature', models.FloatField(default=0.7, help_text='Default temperature 0.0-2.0 (accounts can override if plan allows)')),
|
||||
('openai_max_tokens', models.IntegerField(default=8192, help_text='Default max tokens for responses (accounts can override if plan allows)')),
|
||||
('dalle_api_key', models.CharField(blank=True, help_text='Platform DALL-E API key - used by ALL accounts (can be same as OpenAI)', max_length=500)),
|
||||
('dalle_model', models.CharField(choices=[('dall-e-3', 'DALL·E 3 - $0.040 per image'), ('dall-e-2', 'DALL·E 2 - $0.020 per image')], default='dall-e-3', help_text='Default DALL-E model (accounts can override if plan allows)', max_length=100)),
|
||||
('dalle_size', models.CharField(choices=[('1024x1024', '1024x1024 (Square)'), ('1792x1024', '1792x1024 (Landscape)'), ('1024x1792', '1024x1792 (Portrait)'), ('512x512', '512x512 (Small Square)')], default='1024x1024', help_text='Default image size (accounts can override if plan allows)', max_length=20)),
|
||||
('runware_api_key', models.CharField(blank=True, help_text='Platform Runware API key - used by ALL accounts', max_length=500)),
|
||||
('runware_model', models.CharField(choices=[('runware:97@1', 'Runware 97@1 - Versatile Model'), ('runware:100@1', 'Runware 100@1 - High Quality'), ('runware:101@1', 'Runware 101@1 - Fast Generation')], default='runware:97@1', help_text='Default Runware model (accounts can override if plan allows)', max_length=100)),
|
||||
('default_image_service', models.CharField(choices=[('openai', 'OpenAI DALL-E'), ('runware', 'Runware')], default='openai', help_text='Default image generation service for all accounts (openai=DALL-E, runware=Runware)', max_length=20)),
|
||||
('image_quality', models.CharField(choices=[('standard', 'Standard'), ('hd', 'HD')], default='standard', help_text='Default image quality for all providers (accounts can override if plan allows)', max_length=20)),
|
||||
('image_style', models.CharField(choices=[('vivid', 'Vivid'), ('natural', 'Natural'), ('realistic', 'Realistic'), ('artistic', 'Artistic'), ('cartoon', 'Cartoon')], default='realistic', help_text='Default image style for all providers (accounts can override if plan allows)', max_length=20)),
|
||||
('max_in_article_images', models.IntegerField(default=2, help_text='Default maximum images to generate per article (1-5, accounts can override if plan allows)')),
|
||||
('desktop_image_size', models.CharField(default='1024x1024', help_text='Default desktop image size (accounts can override if plan allows)', max_length=20)),
|
||||
('mobile_image_size', models.CharField(default='512x512', help_text='Default mobile image size (accounts can override if plan allows)', max_length=20)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='global_settings_updates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global Integration Settings',
|
||||
'verbose_name_plural': 'Global Integration Settings',
|
||||
'db_table': 'igny8_global_integration_settings',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,183 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-23 (custom data migration)
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def migrate_model_strings_to_fks(apps, schema_editor):
|
||||
"""Convert CharField model identifiers to ForeignKey references"""
|
||||
GlobalIntegrationSettings = apps.get_model('system', 'GlobalIntegrationSettings')
|
||||
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
|
||||
|
||||
# Get the singleton GlobalIntegrationSettings instance
|
||||
try:
|
||||
settings = GlobalIntegrationSettings.objects.first()
|
||||
if not settings:
|
||||
print(" No GlobalIntegrationSettings found, skipping data migration")
|
||||
return
|
||||
|
||||
# Map openai_model string to AIModelConfig FK
|
||||
if settings.openai_model_old:
|
||||
model_name = settings.openai_model_old
|
||||
# Try to find matching model
|
||||
openai_model = AIModelConfig.objects.filter(
|
||||
model_name=model_name,
|
||||
provider='openai',
|
||||
model_type='text'
|
||||
).first()
|
||||
if openai_model:
|
||||
settings.openai_model_new = openai_model
|
||||
print(f" ✓ Mapped openai_model: {model_name} → {openai_model.id}")
|
||||
else:
|
||||
# Try gpt-4o-mini as fallback
|
||||
openai_model = AIModelConfig.objects.filter(
|
||||
model_name='gpt-4o-mini',
|
||||
provider='openai',
|
||||
model_type='text'
|
||||
).first()
|
||||
if openai_model:
|
||||
settings.openai_model_new = openai_model
|
||||
print(f" ⚠ Could not find {model_name}, using fallback: gpt-4o-mini")
|
||||
|
||||
# Map dalle_model string to AIModelConfig FK
|
||||
if settings.dalle_model_old:
|
||||
model_name = settings.dalle_model_old
|
||||
dalle_model = AIModelConfig.objects.filter(
|
||||
model_name=model_name,
|
||||
provider='openai',
|
||||
model_type='image'
|
||||
).first()
|
||||
if dalle_model:
|
||||
settings.dalle_model_new = dalle_model
|
||||
print(f" ✓ Mapped dalle_model: {model_name} → {dalle_model.id}")
|
||||
else:
|
||||
# Try dall-e-3 as fallback
|
||||
dalle_model = AIModelConfig.objects.filter(
|
||||
model_name='dall-e-3',
|
||||
provider='openai',
|
||||
model_type='image'
|
||||
).first()
|
||||
if dalle_model:
|
||||
settings.dalle_model_new = dalle_model
|
||||
print(f" ⚠ Could not find {model_name}, using fallback: dall-e-3")
|
||||
|
||||
# Map runware_model string to AIModelConfig FK
|
||||
if settings.runware_model_old:
|
||||
model_name = settings.runware_model_old
|
||||
# Runware models might have different naming
|
||||
runware_model = AIModelConfig.objects.filter(
|
||||
provider='runware',
|
||||
model_type='image'
|
||||
).first() # Just get first active Runware model
|
||||
if runware_model:
|
||||
settings.runware_model_new = runware_model
|
||||
print(f" ✓ Mapped runware_model: {model_name} → {runware_model.id}")
|
||||
|
||||
settings.save()
|
||||
print(" ✅ Data migration complete")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠ Error during data migration: {e}")
|
||||
# Don't fail the migration, let admin fix it manually
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0019_add_ai_model_config'),
|
||||
('system', '0004_add_global_integration_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Step 1: Add new FK fields with temporary names
|
||||
migrations.AddField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='openai_model_new',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Default text generation model (accounts can override if plan allows)',
|
||||
limit_choices_to={'is_active': True, 'model_type': 'text', 'provider': 'openai'},
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='global_openai_text_model_new',
|
||||
to='billing.aimodelconfig'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_model_new',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Default DALL-E model (accounts can override if plan allows)',
|
||||
limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'openai'},
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='global_dalle_model_new',
|
||||
to='billing.aimodelconfig'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='runware_model_new',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Default Runware model (accounts can override if plan allows)',
|
||||
limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'runware'},
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='global_runware_model_new',
|
||||
to='billing.aimodelconfig'
|
||||
),
|
||||
),
|
||||
|
||||
# Step 2: Rename old CharField fields
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='openai_model',
|
||||
new_name='openai_model_old',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='dalle_model',
|
||||
new_name='dalle_model_old',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='runware_model',
|
||||
new_name='runware_model_old',
|
||||
),
|
||||
|
||||
# Step 3: Run data migration
|
||||
migrations.RunPython(migrate_model_strings_to_fks, migrations.RunPython.noop),
|
||||
|
||||
# Step 4: Remove old CharField fields
|
||||
migrations.RemoveField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='openai_model_old',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_model_old',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='runware_model_old',
|
||||
),
|
||||
|
||||
# Step 5: Rename new FK fields to final names
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='openai_model_new',
|
||||
new_name='openai_model',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='dalle_model_new',
|
||||
new_name='dalle_model',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='globalintegrationsettings',
|
||||
old_name='runware_model_new',
|
||||
new_name='runware_model',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-23 14:24
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0020_add_optimizer_publisher_timestamps'),
|
||||
('system', '0005_link_global_settings_to_aimodelconfig'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='globalmodulesettings',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='globalmodulesettings',
|
||||
name='optimizer_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Enable Optimizer module platform-wide'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='globalmodulesettings',
|
||||
name='publisher_enabled',
|
||||
field=models.BooleanField(default=True, help_text='Enable Publisher module platform-wide'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='globalmodulesettings',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_model',
|
||||
field=models.ForeignKey(blank=True, help_text='Default DALL-E model (accounts can override if plan allows)', limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'openai'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='global_dalle_model', to='billing.aimodelconfig'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='openai_model',
|
||||
field=models.ForeignKey(blank=True, help_text='Default text generation model (accounts can override if plan allows)', limit_choices_to={'is_active': True, 'model_type': 'text', 'provider': 'openai'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='global_openai_text_model', to='billing.aimodelconfig'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='runware_model',
|
||||
field=models.ForeignKey(blank=True, help_text='Default Runware model (accounts can override if plan allows)', limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'runware'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='global_runware_model', to='billing.aimodelconfig'),
|
||||
),
|
||||
]
|
||||
@@ -137,6 +137,25 @@ class IntegrationSettings(AccountBaseModel):
|
||||
)
|
||||
)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
# AI Model Selection (NEW)
|
||||
default_text_model = models.ForeignKey(
|
||||
'billing.AIModelConfig',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='text_integrations',
|
||||
help_text="Default AI model for text generation operations"
|
||||
)
|
||||
default_image_model = models.ForeignKey(
|
||||
'billing.AIModelConfig',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='image_integrations',
|
||||
help_text="Default AI model for image generation operations"
|
||||
)
|
||||
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user