Files
igny8/backend/igny8_core/api/unified_settings.py
2026-01-17 17:47:16 +00:00

392 lines
18 KiB
Python

"""
Unified Site Settings API
Consolidates AI & Automation settings into a single endpoint.
Per SETTINGS-CONSOLIDATION-PLAN.md:
GET/PUT /api/v1/sites/{site_id}/unified-settings/
"""
import logging
from rest_framework import viewsets, status
from rest_framework.response import Response
from rest_framework.decorators import action
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.auth.models import Site
from igny8_core.business.automation.models import AutomationConfig
from igny8_core.business.integration.models import PublishingSettings
from igny8_core.business.billing.models import AIModelConfig
logger = logging.getLogger(__name__)
# Default stage configuration
DEFAULT_STAGE_CONFIG = {
'1': {'enabled': True, 'batch_size': 50, 'per_run_limit': 0, 'use_testing': False, 'budget_pct': 15},
'2': {'enabled': True, 'batch_size': 1, 'per_run_limit': 10, 'use_testing': False, 'budget_pct': 10},
'3': {'enabled': True, 'batch_size': 20, 'per_run_limit': 0}, # No AI
'4': {'enabled': True, 'batch_size': 1, 'per_run_limit': 5, 'use_testing': False, 'budget_pct': 40},
'5': {'enabled': True, 'batch_size': 1, 'per_run_limit': 5, 'use_testing': False, 'budget_pct': 5},
'6': {'enabled': True, 'batch_size': 1, 'per_run_limit': 20, 'use_testing': False, 'budget_pct': 30},
'7': {'enabled': True, 'per_run_limit': 10}, # No AI
}
STAGE_INFO = [
{'number': 1, 'name': 'Keywords → Clusters', 'has_ai': True},
{'number': 2, 'name': 'Clusters → Ideas', 'has_ai': True},
{'number': 3, 'name': 'Ideas → Tasks', 'has_ai': False},
{'number': 4, 'name': 'Tasks → Content', 'has_ai': True},
{'number': 5, 'name': 'Content → Prompts', 'has_ai': True},
{'number': 6, 'name': 'Prompts → Images', 'has_ai': True},
{'number': 7, 'name': 'Review → Approved', 'has_ai': False},
]
@extend_schema_view(
retrieve=extend_schema(
tags=['Site Settings'],
summary='Get unified site settings',
description='Get all AI & Automation settings for a site in one response',
parameters=[
OpenApiParameter(name='site_id', location='path', type=int, required=True),
]
),
update=extend_schema(
tags=['Site Settings'],
summary='Update unified site settings',
description='Update all AI & Automation settings for a site atomically',
parameters=[
OpenApiParameter(name='site_id', location='path', type=int, required=True),
]
),
)
class UnifiedSiteSettingsViewSet(viewsets.ViewSet):
"""
Unified API for all site AI & automation settings.
GET /api/v1/sites/{site_id}/unified-settings/
PUT /api/v1/sites/{site_id}/unified-settings/
"""
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'settings'
throttle_classes = [DebugScopedRateThrottle]
def retrieve(self, request, site_id=None):
"""Get all settings for a site in one response"""
site = get_object_or_404(Site, id=site_id, account=request.user.account)
# Get or create AutomationConfig
automation_config, _ = AutomationConfig.objects.get_or_create(
site=site,
defaults={
'account': site.account,
'is_enabled': False,
'frequency': 'daily',
'scheduled_time': '02:00',
}
)
# Get or create PublishingSettings
publishing_settings, _ = PublishingSettings.get_or_create_for_site(site)
# Get available models (Testing vs Live)
text_testing = AIModelConfig.get_testing_model('text')
text_live = AIModelConfig.get_live_model('text')
image_testing = AIModelConfig.get_testing_model('image')
image_live = AIModelConfig.get_live_model('image')
# Build stage configuration from AutomationConfig
stage_config = self._build_stage_config_from_automation(automation_config)
# Handle scheduled_time which might be a string or time object
scheduled_time = automation_config.scheduled_time
if scheduled_time:
if hasattr(scheduled_time, 'strftime'):
time_str = scheduled_time.strftime('%H:%M')
else:
time_str = str(scheduled_time)[:5] # Get HH:MM from string
else:
time_str = '02:00'
response_data = {
'site_id': site.id,
'site_name': site.name,
'automation': {
'enabled': automation_config.is_enabled,
'frequency': automation_config.frequency,
'time': time_str,
'last_run_at': automation_config.last_run_at.isoformat() if automation_config.last_run_at else None,
'next_run_at': automation_config.next_run_at.isoformat() if automation_config.next_run_at else None,
},
'stages': self._build_stage_matrix(stage_config),
'delays': {
'within_stage': automation_config.within_stage_delay,
'between_stage': automation_config.between_stage_delay,
},
'publishing': {
'auto_approval_enabled': publishing_settings.auto_approval_enabled,
'auto_publish_enabled': publishing_settings.auto_publish_enabled,
'publish_days': publishing_settings.publish_days,
'time_slots': publishing_settings.publish_time_slots,
# Calculated capacity (read-only)
'daily_capacity': publishing_settings.daily_capacity,
'weekly_capacity': publishing_settings.weekly_capacity,
'monthly_capacity': publishing_settings.monthly_capacity,
},
'available_models': {
'text': {
'testing': {
'id': text_testing.id if text_testing else None,
'name': text_testing.display_name if text_testing else None,
'model_name': text_testing.model_name if text_testing else None,
} if text_testing else None,
'live': {
'id': text_live.id if text_live else None,
'name': text_live.display_name if text_live else None,
'model_name': text_live.model_name if text_live else None,
} if text_live else None,
},
'image': {
'testing': {
'id': image_testing.id if image_testing else None,
'name': image_testing.display_name if image_testing else None,
'model_name': image_testing.model_name if image_testing else None,
} if image_testing else None,
'live': {
'id': image_live.id if image_live else None,
'name': image_live.display_name if image_live else None,
'model_name': image_live.model_name if image_live else None,
} if image_live else None,
},
},
}
return success_response(response_data, request=request)
def update(self, request, site_id=None):
"""Update all settings for a site atomically"""
site = get_object_or_404(Site, id=site_id, account=request.user.account)
data = request.data
logger.info(f"[UnifiedSettings] Received update for site {site_id}: {data}")
try:
# Get or create AutomationConfig
automation_config, _ = AutomationConfig.objects.get_or_create(
site=site,
defaults={'account': site.account}
)
# Get or create PublishingSettings
publishing_settings, _ = PublishingSettings.get_or_create_for_site(site)
# Update automation settings
if 'automation' in data:
auto = data['automation']
if 'enabled' in auto:
automation_config.is_enabled = auto['enabled']
if 'frequency' in auto:
automation_config.frequency = auto['frequency']
if 'time' in auto:
from datetime import datetime
automation_config.scheduled_time = datetime.strptime(auto['time'], '%H:%M').time()
# Update stage configuration
if 'stages' in data:
logger.info(f"[UnifiedSettings] Updating stages: {data['stages']}")
self._update_stage_config(automation_config, data['stages'])
# Update delays
if 'delays' in data:
delays = data['delays']
if 'within_stage' in delays:
automation_config.within_stage_delay = delays['within_stage']
if 'between_stage' in delays:
automation_config.between_stage_delay = delays['between_stage']
automation_config.save()
logger.info(f"[UnifiedSettings] AutomationConfig saved for site {site_id}")
# Update publishing settings
if 'publishing' in data:
pub = data['publishing']
if 'auto_approval_enabled' in pub:
publishing_settings.auto_approval_enabled = pub['auto_approval_enabled']
if 'auto_publish_enabled' in pub:
publishing_settings.auto_publish_enabled = pub['auto_publish_enabled']
if 'publish_days' in pub:
publishing_settings.publish_days = pub['publish_days']
if 'time_slots' in pub:
publishing_settings.publish_time_slots = pub['time_slots']
publishing_settings.save()
# Return the updated settings
return self.retrieve(request, site_id)
except Exception as e:
logger.exception(f"Error updating unified settings for site {site_id}")
return error_response(
f"Failed to update settings: {str(e)}",
None,
status.HTTP_400_BAD_REQUEST,
request
)
def _build_stage_config_from_automation(self, automation_config):
"""Build stage config dict from AutomationConfig model fields"""
return {
'1': {
'enabled': automation_config.stage_1_enabled,
'batch_size': automation_config.stage_1_batch_size,
'per_run_limit': automation_config.max_keywords_per_run,
'use_testing': automation_config.stage_1_use_testing,
'budget_pct': automation_config.stage_1_budget_pct,
},
'2': {
'enabled': automation_config.stage_2_enabled,
'batch_size': automation_config.stage_2_batch_size,
'per_run_limit': automation_config.max_clusters_per_run,
'use_testing': automation_config.stage_2_use_testing,
'budget_pct': automation_config.stage_2_budget_pct,
},
'3': {
'enabled': automation_config.stage_3_enabled,
'batch_size': automation_config.stage_3_batch_size,
'per_run_limit': automation_config.max_ideas_per_run,
},
'4': {
'enabled': automation_config.stage_4_enabled,
'batch_size': automation_config.stage_4_batch_size,
'per_run_limit': automation_config.max_tasks_per_run,
'use_testing': automation_config.stage_4_use_testing,
'budget_pct': automation_config.stage_4_budget_pct,
},
'5': {
'enabled': automation_config.stage_5_enabled,
'batch_size': automation_config.stage_5_batch_size,
'per_run_limit': automation_config.max_content_per_run,
'use_testing': automation_config.stage_5_use_testing,
'budget_pct': automation_config.stage_5_budget_pct,
},
'6': {
'enabled': automation_config.stage_6_enabled,
'batch_size': automation_config.stage_6_batch_size,
'per_run_limit': automation_config.max_images_per_run,
'use_testing': automation_config.stage_6_use_testing,
'budget_pct': automation_config.stage_6_budget_pct,
},
'7': {
'enabled': automation_config.stage_7_enabled,
'per_run_limit': automation_config.max_approvals_per_run,
},
}
def _build_stage_matrix(self, stage_config):
"""Build stage configuration matrix for frontend"""
result = []
for stage in STAGE_INFO:
num = str(stage['number'])
config = stage_config.get(num, DEFAULT_STAGE_CONFIG.get(num, {}))
stage_data = {
'number': stage['number'],
'name': stage['name'],
'has_ai': stage['has_ai'],
'enabled': config.get('enabled', True),
'batch_size': config.get('batch_size', 1),
'per_run_limit': config.get('per_run_limit', 0),
}
# Only include AI-related fields for stages that use AI
if stage['has_ai']:
stage_data['use_testing'] = config.get('use_testing', False)
stage_data['budget_pct'] = config.get('budget_pct', 20)
result.append(stage_data)
return result
def _update_stage_config(self, automation_config, stages):
"""Update AutomationConfig from stages array"""
for stage in stages:
num = stage.get('number')
if not num:
logger.warning(f"[UnifiedSettings] Stage missing 'number': {stage}")
continue
logger.info(f"[UnifiedSettings] Processing stage {num}: enabled={stage.get('enabled')}, batch_size={stage.get('batch_size')}, per_run_limit={stage.get('per_run_limit')}, use_testing={stage.get('use_testing')}, budget_pct={stage.get('budget_pct')}")
if num == 1:
if 'enabled' in stage:
automation_config.stage_1_enabled = stage['enabled']
if 'batch_size' in stage:
automation_config.stage_1_batch_size = stage['batch_size']
if 'per_run_limit' in stage:
automation_config.max_keywords_per_run = stage['per_run_limit']
if 'use_testing' in stage:
automation_config.stage_1_use_testing = stage['use_testing']
if 'budget_pct' in stage:
automation_config.stage_1_budget_pct = stage['budget_pct']
elif num == 2:
if 'enabled' in stage:
automation_config.stage_2_enabled = stage['enabled']
if 'batch_size' in stage:
automation_config.stage_2_batch_size = stage['batch_size']
if 'per_run_limit' in stage:
automation_config.max_clusters_per_run = stage['per_run_limit']
if 'use_testing' in stage:
automation_config.stage_2_use_testing = stage['use_testing']
if 'budget_pct' in stage:
automation_config.stage_2_budget_pct = stage['budget_pct']
elif num == 3:
if 'enabled' in stage:
automation_config.stage_3_enabled = stage['enabled']
if 'batch_size' in stage:
automation_config.stage_3_batch_size = stage['batch_size']
if 'per_run_limit' in stage:
automation_config.max_ideas_per_run = stage['per_run_limit']
elif num == 4:
if 'enabled' in stage:
automation_config.stage_4_enabled = stage['enabled']
if 'batch_size' in stage:
automation_config.stage_4_batch_size = stage['batch_size']
if 'per_run_limit' in stage:
automation_config.max_tasks_per_run = stage['per_run_limit']
if 'use_testing' in stage:
automation_config.stage_4_use_testing = stage['use_testing']
if 'budget_pct' in stage:
automation_config.stage_4_budget_pct = stage['budget_pct']
elif num == 5:
if 'enabled' in stage:
automation_config.stage_5_enabled = stage['enabled']
if 'batch_size' in stage:
automation_config.stage_5_batch_size = stage['batch_size']
if 'per_run_limit' in stage:
automation_config.max_content_per_run = stage['per_run_limit']
if 'use_testing' in stage:
automation_config.stage_5_use_testing = stage['use_testing']
if 'budget_pct' in stage:
automation_config.stage_5_budget_pct = stage['budget_pct']
elif num == 6:
if 'enabled' in stage:
automation_config.stage_6_enabled = stage['enabled']
if 'batch_size' in stage:
automation_config.stage_6_batch_size = stage['batch_size']
if 'per_run_limit' in stage:
automation_config.max_images_per_run = stage['per_run_limit']
if 'use_testing' in stage:
automation_config.stage_6_use_testing = stage['use_testing']
if 'budget_pct' in stage:
automation_config.stage_6_budget_pct = stage['budget_pct']
elif num == 7:
if 'enabled' in stage:
automation_config.stage_7_enabled = stage['enabled']
if 'per_run_limit' in stage:
automation_config.max_approvals_per_run = stage['per_run_limit']
logger.info(f"[UnifiedSettings] After update - stage_1_batch_size={automation_config.stage_1_batch_size}, max_keywords_per_run={automation_config.max_keywords_per_run}")