AI AUtomtaion, Schudelign and publishign fromt and backe end refoactr
This commit is contained in:
@@ -839,6 +839,7 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
'provider_badge',
|
||||
'credit_display',
|
||||
'quality_tier',
|
||||
'is_testing_icon',
|
||||
'is_active_icon',
|
||||
'is_default_icon',
|
||||
'updated_at',
|
||||
@@ -848,6 +849,7 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
'model_type',
|
||||
'provider',
|
||||
'quality_tier',
|
||||
'is_testing',
|
||||
'is_active',
|
||||
'is_default',
|
||||
]
|
||||
@@ -884,7 +886,8 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Status', {
|
||||
'fields': ('is_active', 'is_default'),
|
||||
'fields': ('is_active', 'is_default', 'is_testing'),
|
||||
'description': 'is_testing: Mark as cheap testing model (one per model_type)'
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
@@ -969,8 +972,19 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
)
|
||||
is_default_icon.short_description = 'Default'
|
||||
|
||||
def is_testing_icon(self, obj):
|
||||
"""Testing status icon - shows ⚡ for testing models"""
|
||||
if obj.is_testing:
|
||||
return format_html(
|
||||
'<span style="color: #f39c12; font-size: 18px;" title="Testing Model (cheap, for testing)">⚡</span>'
|
||||
)
|
||||
return format_html(
|
||||
'<span style="color: #2ecc71; font-size: 14px;" title="Live Model">●</span>'
|
||||
)
|
||||
is_testing_icon.short_description = 'Testing/Live'
|
||||
|
||||
# Admin actions
|
||||
actions = ['bulk_activate', 'bulk_deactivate', 'set_as_default']
|
||||
actions = ['bulk_activate', 'bulk_deactivate', 'set_as_default', 'set_as_testing', 'unset_testing']
|
||||
|
||||
def bulk_activate(self, request, queryset):
|
||||
"""Enable selected models"""
|
||||
@@ -1005,3 +1019,34 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
messages.SUCCESS
|
||||
)
|
||||
set_as_default.short_description = 'Set as default model'
|
||||
|
||||
def set_as_testing(self, request, queryset):
|
||||
"""Set one model as testing model for its type"""
|
||||
if queryset.count() != 1:
|
||||
self.message_user(request, 'Select exactly one model.', messages.ERROR)
|
||||
return
|
||||
|
||||
model = queryset.first()
|
||||
|
||||
# Unset any existing testing model for this type
|
||||
AIModelConfig.objects.filter(
|
||||
model_type=model.model_type,
|
||||
is_testing=True,
|
||||
is_active=True
|
||||
).exclude(pk=model.pk).update(is_testing=False)
|
||||
|
||||
model.is_testing = True
|
||||
model.save()
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f'{model.model_name} is now the TESTING {model.get_model_type_display()} model.',
|
||||
messages.SUCCESS
|
||||
)
|
||||
set_as_testing.short_description = 'Set as testing model (cheap, for testing)'
|
||||
|
||||
def unset_testing(self, request, queryset):
|
||||
"""Remove testing flag from selected models"""
|
||||
count = queryset.update(is_testing=False)
|
||||
self.message_user(request, f'{count} model(s) unmarked as testing.', messages.SUCCESS)
|
||||
unset_testing.short_description = 'Unset testing flag'
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-17 14:37
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0034_backfill_credit_usage_log_site'),
|
||||
('igny8_core_auth', '0031_drop_all_blueprint_tables'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aimodelconfig',
|
||||
name='is_testing',
|
||||
field=models.BooleanField(db_index=True, default=False, help_text='Testing model (cheap, for testing only). Only one per model_type can be is_testing=True.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalaimodelconfig',
|
||||
name='is_testing',
|
||||
field=models.BooleanField(db_index=True, default=False, help_text='Testing model (cheap, for testing only). Only one per model_type can be is_testing=True.'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SiteAIBudgetAllocation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ai_function', models.CharField(choices=[('clustering', 'Keyword Clustering (Stage 1)'), ('idea_generation', 'Ideas Generation (Stage 2)'), ('content_generation', 'Content Generation (Stage 4)'), ('image_prompt', 'Image Prompt Extraction (Stage 5)'), ('image_generation', 'Image Generation (Stage 6)')], help_text='AI function to allocate budget for', max_length=50)),
|
||||
('allocation_percentage', models.PositiveIntegerField(default=20, help_text='Percentage of credit budget allocated to this function (0-100)', validators=[django.core.validators.MinValueValidator(0)])),
|
||||
('is_enabled', models.BooleanField(default=True, help_text='Whether this function is enabled for automation')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
|
||||
('site', models.ForeignKey(help_text='Site this allocation belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='ai_budget_allocations', to='igny8_core_auth.site')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Site AI Budget Allocation',
|
||||
'verbose_name_plural': 'Site AI Budget Allocations',
|
||||
'db_table': 'igny8_site_ai_budget_allocations',
|
||||
'ordering': ['site', 'ai_function'],
|
||||
'indexes': [models.Index(fields=['site', 'is_enabled'], name='igny8_site__site_id_36b0d0_idx'), models.Index(fields=['account', 'site'], name='igny8_site__tenant__853b16_idx')],
|
||||
'unique_together': {('site', 'ai_function')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -8,7 +8,8 @@ from .views import (
|
||||
CreditUsageViewSet,
|
||||
CreditTransactionViewSet,
|
||||
BillingOverviewViewSet,
|
||||
AdminBillingViewSet
|
||||
AdminBillingViewSet,
|
||||
SiteAIBudgetAllocationViewSet
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
@@ -31,5 +32,7 @@ urlpatterns = [
|
||||
path('admin/billing/stats/', AdminBillingViewSet.as_view({'get': 'stats'}), name='admin-billing-stats'),
|
||||
path('admin/users/', AdminBillingViewSet.as_view({'get': 'list_users'}), name='admin-users-list'),
|
||||
path('admin/credit-costs/', AdminBillingViewSet.as_view({'get': 'credit_costs'}), name='admin-credit-costs'),
|
||||
# Site AI budget allocation
|
||||
path('sites/<int:site_id>/ai-budget/', SiteAIBudgetAllocationViewSet.as_view({'get': 'list', 'post': 'create'}), name='site-ai-budget'),
|
||||
]
|
||||
|
||||
|
||||
@@ -840,3 +840,177 @@ class AIModelConfigViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Site AI Budget Allocation ViewSet
|
||||
# ==============================================================================
|
||||
|
||||
from rest_framework import serializers as drf_serializers
|
||||
|
||||
|
||||
class SiteAIBudgetAllocationSerializer(drf_serializers.Serializer):
|
||||
"""Serializer for SiteAIBudgetAllocation model"""
|
||||
id = drf_serializers.IntegerField(read_only=True)
|
||||
ai_function = drf_serializers.CharField()
|
||||
ai_function_display = drf_serializers.SerializerMethodField()
|
||||
allocation_percentage = drf_serializers.IntegerField(min_value=0, max_value=100)
|
||||
is_enabled = drf_serializers.BooleanField()
|
||||
|
||||
def get_ai_function_display(self, obj):
|
||||
display_map = {
|
||||
'clustering': 'Keyword Clustering (Stage 1)',
|
||||
'idea_generation': 'Ideas Generation (Stage 2)',
|
||||
'content_generation': 'Content Generation (Stage 4)',
|
||||
'image_prompt': 'Image Prompt Extraction (Stage 5)',
|
||||
'image_generation': 'Image Generation (Stage 6)',
|
||||
}
|
||||
if hasattr(obj, 'ai_function'):
|
||||
return display_map.get(obj.ai_function, obj.ai_function)
|
||||
return display_map.get(obj.get('ai_function', ''), '')
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(tags=['Billing'], summary='Get AI budget allocations for a site'),
|
||||
create=extend_schema(tags=['Billing'], summary='Update AI budget allocations for a site'),
|
||||
)
|
||||
class SiteAIBudgetAllocationViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
ViewSet for managing Site AI Budget Allocations.
|
||||
|
||||
GET /api/v1/billing/sites/{site_id}/ai-budget/
|
||||
POST /api/v1/billing/sites/{site_id}/ai-budget/
|
||||
|
||||
Allows configuring what percentage of the site's credit budget
|
||||
can be used for each AI function during automation runs.
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
authentication_classes = [JWTAuthentication]
|
||||
throttle_scope = 'billing'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
|
||||
def _get_site(self, site_id, request):
|
||||
"""Get site and verify user has access"""
|
||||
from igny8_core.auth.models import Site
|
||||
try:
|
||||
site = Site.objects.get(id=int(site_id))
|
||||
account = getattr(request, 'account', None)
|
||||
if account and site.account != account:
|
||||
return None
|
||||
return site
|
||||
except (Site.DoesNotExist, ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def list(self, request, site_id=None):
|
||||
"""
|
||||
Get AI budget allocations for a site.
|
||||
Creates default allocations if they don't exist.
|
||||
"""
|
||||
from igny8_core.business.billing.models import SiteAIBudgetAllocation
|
||||
|
||||
site = self._get_site(site_id, request)
|
||||
if not site:
|
||||
return error_response(
|
||||
message='Site not found or access denied',
|
||||
errors=None,
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
account = getattr(request, 'account', None) or site.account
|
||||
|
||||
# Get or create default allocations
|
||||
allocations = SiteAIBudgetAllocation.get_or_create_defaults_for_site(site, account)
|
||||
|
||||
# Calculate total allocation
|
||||
total_percentage = sum(a.allocation_percentage for a in allocations if a.is_enabled)
|
||||
|
||||
serializer = SiteAIBudgetAllocationSerializer(allocations, many=True)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'allocations': serializer.data,
|
||||
'total_percentage': total_percentage,
|
||||
'is_valid': total_percentage <= 100,
|
||||
},
|
||||
message='AI budget allocations retrieved'
|
||||
)
|
||||
|
||||
def create(self, request, site_id=None):
|
||||
"""
|
||||
Update AI budget allocations for a site.
|
||||
|
||||
Body:
|
||||
{
|
||||
"allocations": [
|
||||
{"ai_function": "clustering", "allocation_percentage": 15, "is_enabled": true},
|
||||
{"ai_function": "idea_generation", "allocation_percentage": 10, "is_enabled": true},
|
||||
{"ai_function": "content_generation", "allocation_percentage": 40, "is_enabled": true},
|
||||
{"ai_function": "image_prompt", "allocation_percentage": 5, "is_enabled": true},
|
||||
{"ai_function": "image_generation", "allocation_percentage": 30, "is_enabled": true}
|
||||
]
|
||||
}
|
||||
"""
|
||||
from igny8_core.business.billing.models import SiteAIBudgetAllocation
|
||||
|
||||
site = self._get_site(site_id, request)
|
||||
if not site:
|
||||
return error_response(
|
||||
message='Site not found or access denied',
|
||||
errors=None,
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
account = getattr(request, 'account', None) or site.account
|
||||
allocations_data = request.data.get('allocations', [])
|
||||
|
||||
if not allocations_data:
|
||||
return error_response(
|
||||
message='No allocations provided',
|
||||
errors={'allocations': ['This field is required']},
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Validate total percentage
|
||||
total_percentage = sum(
|
||||
a.get('allocation_percentage', 0)
|
||||
for a in allocations_data
|
||||
if a.get('is_enabled', True)
|
||||
)
|
||||
|
||||
if total_percentage > 100:
|
||||
return error_response(
|
||||
message='Total allocation exceeds 100%',
|
||||
errors={'total_percentage': [f'Total is {total_percentage}%, must be <= 100%']},
|
||||
status_code=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Update or create allocations
|
||||
valid_functions = ['clustering', 'idea_generation', 'content_generation', 'image_prompt', 'image_generation']
|
||||
updated = []
|
||||
|
||||
for alloc_data in allocations_data:
|
||||
ai_function = alloc_data.get('ai_function')
|
||||
if ai_function not in valid_functions:
|
||||
continue
|
||||
|
||||
allocation, _ = SiteAIBudgetAllocation.objects.update_or_create(
|
||||
account=account,
|
||||
site=site,
|
||||
ai_function=ai_function,
|
||||
defaults={
|
||||
'allocation_percentage': alloc_data.get('allocation_percentage', 20),
|
||||
'is_enabled': alloc_data.get('is_enabled', True),
|
||||
}
|
||||
)
|
||||
updated.append(allocation)
|
||||
|
||||
serializer = SiteAIBudgetAllocationSerializer(updated, many=True)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'allocations': serializer.data,
|
||||
'total_percentage': total_percentage,
|
||||
},
|
||||
message='AI budget allocations updated successfully'
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from igny8_core.modules.integration.webhooks import (
|
||||
wordpress_status_webhook,
|
||||
wordpress_metadata_webhook,
|
||||
)
|
||||
from igny8_core.api.unified_settings import UnifiedSiteSettingsViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'integrations', IntegrationViewSet, basename='integration')
|
||||
@@ -21,12 +22,21 @@ publishing_settings_viewset = PublishingSettingsViewSet.as_view({
|
||||
'patch': 'partial_update',
|
||||
})
|
||||
|
||||
# Create Unified Settings ViewSet instance
|
||||
unified_settings_viewset = UnifiedSiteSettingsViewSet.as_view({
|
||||
'get': 'retrieve',
|
||||
'put': 'update',
|
||||
})
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
|
||||
# Site-level publishing settings
|
||||
path('sites/<int:site_id>/publishing-settings/', publishing_settings_viewset, name='publishing-settings'),
|
||||
|
||||
# Unified site settings (AI & Automation consolidated)
|
||||
path('sites/<int:site_id>/unified-settings/', unified_settings_viewset, name='unified-settings'),
|
||||
|
||||
# Webhook endpoints
|
||||
path('webhooks/wordpress/status/', wordpress_status_webhook, name='wordpress-status-webhook'),
|
||||
path('webhooks/wordpress/metadata/', wordpress_metadata_webhook, name='wordpress-metadata-webhook'),
|
||||
|
||||
@@ -966,11 +966,16 @@ class PublishingSettingsSerializer(serializers.ModelSerializer):
|
||||
'site',
|
||||
'auto_approval_enabled',
|
||||
'auto_publish_enabled',
|
||||
'scheduling_mode',
|
||||
'daily_publish_limit',
|
||||
'weekly_publish_limit',
|
||||
'monthly_publish_limit',
|
||||
'publish_days',
|
||||
'publish_time_slots',
|
||||
'stagger_start_time',
|
||||
'stagger_end_time',
|
||||
'stagger_interval_minutes',
|
||||
'queue_limit',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user