AI AUtomtaion, Schudelign and publishign fromt and backe end refoactr

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-17 15:52:46 +00:00
parent 0435a5cf70
commit d3b3e1c0d4
34 changed files with 4715 additions and 375 deletions

View File

@@ -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'

View File

@@ -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')},
},
),
]

View File

@@ -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'),
]

View File

@@ -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'
)

View File

@@ -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'),

View File

@@ -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',
]