autmation final reaftocrs and setitgns dafautls
This commit is contained in:
@@ -9,6 +9,7 @@ import logging
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.views import APIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
|
||||
|
||||
@@ -16,7 +17,7 @@ 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.automation.models import AutomationConfig, DefaultAutomationConfig
|
||||
from igny8_core.business.integration.models import PublishingSettings
|
||||
from igny8_core.business.billing.models import AIModelConfig
|
||||
|
||||
@@ -452,3 +453,101 @@ class UnifiedSiteSettingsViewSet(viewsets.ViewSet):
|
||||
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}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# DEFAULT SETTINGS API
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
class DefaultSettingsAPIView(APIView):
|
||||
"""
|
||||
API endpoint to fetch default settings for reset functionality.
|
||||
Reads from DefaultAutomationConfig singleton in backend.
|
||||
|
||||
GET /api/v1/settings/defaults/
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedAndActive]
|
||||
|
||||
@extend_schema(
|
||||
tags=['Site Settings'],
|
||||
summary='Get default settings',
|
||||
description='Fetch default automation, publishing, and stage settings from backend configuration. Used by frontend reset functionality.',
|
||||
)
|
||||
def get(self, request):
|
||||
"""Return default settings from DefaultAutomationConfig"""
|
||||
try:
|
||||
defaults = DefaultAutomationConfig.get_instance()
|
||||
|
||||
# Build stage defaults from the model
|
||||
stage_defaults = []
|
||||
for i in range(1, 8):
|
||||
stage_config = {
|
||||
'number': i,
|
||||
'enabled': getattr(defaults, f'stage_{i}_enabled', True),
|
||||
}
|
||||
|
||||
# Batch size (stages 1-6)
|
||||
if i <= 6:
|
||||
stage_config['batch_size'] = getattr(defaults, f'stage_{i}_batch_size', 1)
|
||||
|
||||
# Per-run limits
|
||||
limit_map = {
|
||||
1: 'max_keywords_per_run',
|
||||
2: 'max_clusters_per_run',
|
||||
3: 'max_ideas_per_run',
|
||||
4: 'max_tasks_per_run',
|
||||
5: 'max_content_per_run',
|
||||
6: 'max_images_per_run',
|
||||
7: 'max_approvals_per_run',
|
||||
}
|
||||
stage_config['per_run_limit'] = getattr(defaults, limit_map[i], 0)
|
||||
|
||||
# Use testing (AI stages only: 1, 2, 4, 5, 6)
|
||||
if i in [1, 2, 4, 5, 6]:
|
||||
stage_config['use_testing'] = getattr(defaults, f'stage_{i}_use_testing', False)
|
||||
stage_config['budget_pct'] = getattr(defaults, f'stage_{i}_budget_pct', 0)
|
||||
|
||||
stage_defaults.append(stage_config)
|
||||
|
||||
# Build publish days - ensure it's a list
|
||||
publish_days = defaults.publish_days
|
||||
if not publish_days:
|
||||
publish_days = ['mon', 'tue', 'wed', 'thu', 'fri']
|
||||
|
||||
# Build time slots - ensure it's a list
|
||||
time_slots = defaults.publish_time_slots
|
||||
if not time_slots:
|
||||
time_slots = ['09:00', '14:00', '18:00']
|
||||
|
||||
# Format scheduled_time from hour
|
||||
scheduled_hour = defaults.next_scheduled_hour
|
||||
scheduled_time = f"{scheduled_hour:02d}:00"
|
||||
|
||||
return success_response({
|
||||
'automation': {
|
||||
'enabled': defaults.is_enabled,
|
||||
'frequency': defaults.frequency,
|
||||
'time': scheduled_time,
|
||||
},
|
||||
'stages': stage_defaults,
|
||||
'delays': {
|
||||
'within_stage': defaults.within_stage_delay,
|
||||
'between_stage': defaults.between_stage_delay,
|
||||
},
|
||||
'publishing': {
|
||||
'auto_approval_enabled': defaults.auto_approval_enabled,
|
||||
'auto_publish_enabled': defaults.auto_publish_enabled,
|
||||
'daily_publish_limit': defaults.daily_publish_limit,
|
||||
'weekly_publish_limit': defaults.weekly_publish_limit,
|
||||
'monthly_publish_limit': defaults.monthly_publish_limit,
|
||||
'publish_days': publish_days,
|
||||
'time_slots': time_slots,
|
||||
},
|
||||
'images': {
|
||||
'style': defaults.image_style,
|
||||
'max_images': defaults.max_images_per_article,
|
||||
},
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"[DefaultSettings] Error fetching defaults: {e}")
|
||||
return error_response(str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@@ -5,13 +5,134 @@ from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
||||
from .models import AutomationConfig, AutomationRun
|
||||
from .models import AutomationConfig, AutomationRun, DefaultAutomationConfig
|
||||
|
||||
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# DEFAULT AUTOMATION CONFIG (Singleton)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
@admin.register(DefaultAutomationConfig)
|
||||
class DefaultAutomationConfigAdmin(Igny8ModelAdmin):
|
||||
"""Admin for the singleton Default Automation Config"""
|
||||
|
||||
list_display = ('__str__', 'is_enabled', 'frequency', 'next_scheduled_hour', 'updated_at')
|
||||
|
||||
fieldsets = (
|
||||
('Default Schedule Settings', {
|
||||
'fields': ('is_enabled', 'frequency'),
|
||||
'description': 'These defaults are applied when creating new sites.'
|
||||
}),
|
||||
('Auto-Increment Time Slot', {
|
||||
'fields': ('base_scheduled_hour', 'next_scheduled_hour'),
|
||||
'description': 'Each new site gets next_scheduled_hour as its run time, then it increments. Reset next_scheduled_hour to base_scheduled_hour to start over.'
|
||||
}),
|
||||
('Publishing Defaults', {
|
||||
'fields': (
|
||||
'auto_approval_enabled',
|
||||
'auto_publish_enabled',
|
||||
'daily_publish_limit',
|
||||
'weekly_publish_limit',
|
||||
'monthly_publish_limit',
|
||||
'publish_days',
|
||||
'publish_time_slots',
|
||||
),
|
||||
'description': 'Default publishing settings for new sites.',
|
||||
}),
|
||||
('Image Generation Defaults', {
|
||||
'fields': (
|
||||
'image_style',
|
||||
'max_images_per_article',
|
||||
),
|
||||
'description': 'Default image generation settings for new sites.',
|
||||
}),
|
||||
('Stage Enabled/Disabled', {
|
||||
'fields': (
|
||||
'stage_1_enabled',
|
||||
'stage_2_enabled',
|
||||
'stage_3_enabled',
|
||||
'stage_4_enabled',
|
||||
'stage_5_enabled',
|
||||
'stage_6_enabled',
|
||||
'stage_7_enabled',
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Batch Sizes', {
|
||||
'fields': (
|
||||
'stage_1_batch_size',
|
||||
'stage_2_batch_size',
|
||||
'stage_3_batch_size',
|
||||
'stage_4_batch_size',
|
||||
'stage_5_batch_size',
|
||||
'stage_6_batch_size',
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Use Testing Model', {
|
||||
'fields': (
|
||||
'stage_1_use_testing',
|
||||
'stage_2_use_testing',
|
||||
'stage_4_use_testing',
|
||||
'stage_5_use_testing',
|
||||
'stage_6_use_testing',
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Budget Allocation (%)', {
|
||||
'fields': (
|
||||
'stage_1_budget_pct',
|
||||
'stage_2_budget_pct',
|
||||
'stage_4_budget_pct',
|
||||
'stage_5_budget_pct',
|
||||
'stage_6_budget_pct',
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Per-Run Limits', {
|
||||
'fields': (
|
||||
'max_keywords_per_run',
|
||||
'max_clusters_per_run',
|
||||
'max_ideas_per_run',
|
||||
'max_tasks_per_run',
|
||||
'max_content_per_run',
|
||||
'max_images_per_run',
|
||||
'max_approvals_per_run',
|
||||
'max_credits_per_run',
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Delays', {
|
||||
'fields': ('within_stage_delay', 'between_stage_delay'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
# Only allow one instance (singleton)
|
||||
return not DefaultAutomationConfig.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
# Don't allow deleting the singleton
|
||||
return False
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
# Redirect to edit view if instance exists, otherwise show add form
|
||||
obj = DefaultAutomationConfig.objects.first()
|
||||
if obj:
|
||||
from django.shortcuts import redirect
|
||||
return redirect(f'admin:automation_defaultautomationconfig_change', obj.pk)
|
||||
return super().changelist_view(request, extra_context)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# AUTOMATION CONFIG (Per-Site)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
class AutomationConfigResource(resources.ModelResource):
|
||||
"""Resource class for exporting Automation Configs"""
|
||||
class Meta:
|
||||
@@ -24,26 +145,120 @@ class AutomationConfigResource(resources.ModelResource):
|
||||
@admin.register(AutomationConfig)
|
||||
class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = AutomationConfigResource
|
||||
list_display = ('site', 'is_enabled', 'frequency', 'scheduled_time', 'next_scheduled_run', 'last_run_at')
|
||||
list_filter = ('is_enabled', 'frequency')
|
||||
list_display = ('site', 'is_enabled', 'frequency', 'scheduled_time', 'next_scheduled_run', 'last_run_at', 'test_mode_status')
|
||||
list_filter = ('is_enabled', 'frequency', 'test_mode_enabled')
|
||||
search_fields = ('site__domain',)
|
||||
readonly_fields = ('test_trigger_at_display',)
|
||||
actions = [
|
||||
'bulk_enable',
|
||||
'bulk_disable',
|
||||
'bulk_update_frequency',
|
||||
'bulk_update_delays',
|
||||
'trigger_test_run',
|
||||
'clear_test_mode',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('site', 'is_enabled', 'frequency', 'scheduled_time')
|
||||
}),
|
||||
('Timing', {
|
||||
'fields': ('last_run_at', 'next_run_at')
|
||||
}),
|
||||
('Stage Enabled/Disabled', {
|
||||
'fields': (
|
||||
'stage_1_enabled',
|
||||
'stage_2_enabled',
|
||||
'stage_3_enabled',
|
||||
'stage_4_enabled',
|
||||
'stage_5_enabled',
|
||||
'stage_6_enabled',
|
||||
'stage_7_enabled',
|
||||
),
|
||||
}),
|
||||
('Batch Sizes', {
|
||||
'fields': (
|
||||
'stage_1_batch_size',
|
||||
'stage_2_batch_size',
|
||||
'stage_3_batch_size',
|
||||
'stage_4_batch_size',
|
||||
'stage_5_batch_size',
|
||||
'stage_6_batch_size',
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Use Testing Model', {
|
||||
'fields': (
|
||||
'stage_1_use_testing',
|
||||
'stage_2_use_testing',
|
||||
'stage_4_use_testing',
|
||||
'stage_5_use_testing',
|
||||
'stage_6_use_testing',
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
'description': 'Use testing AI model instead of live model for these stages.',
|
||||
}),
|
||||
('Budget Allocation (%)', {
|
||||
'fields': (
|
||||
'stage_1_budget_pct',
|
||||
'stage_2_budget_pct',
|
||||
'stage_4_budget_pct',
|
||||
'stage_5_budget_pct',
|
||||
'stage_6_budget_pct',
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
'description': 'Percentage of AI budget allocated to each stage (should total 100%).',
|
||||
}),
|
||||
('Per-Run Limits', {
|
||||
'fields': (
|
||||
'max_keywords_per_run',
|
||||
'max_clusters_per_run',
|
||||
'max_ideas_per_run',
|
||||
'max_tasks_per_run',
|
||||
'max_content_per_run',
|
||||
'max_images_per_run',
|
||||
'max_approvals_per_run',
|
||||
'max_credits_per_run',
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
'description': 'Maximum items to process per automation run (0 = unlimited).',
|
||||
}),
|
||||
('Delays', {
|
||||
'fields': ('within_stage_delay', 'between_stage_delay'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('Test Mode (Admin Only)', {
|
||||
'fields': ('test_mode_enabled', 'test_trigger_at', 'test_trigger_at_display'),
|
||||
'classes': ('collapse',),
|
||||
'description': 'Enable test mode to trigger automation without waiting for schedule. Set trigger time and automation will run at next minute check.'
|
||||
}),
|
||||
)
|
||||
|
||||
def test_mode_status(self, obj):
|
||||
"""Show test mode status in list view"""
|
||||
if not obj.test_mode_enabled:
|
||||
return '-'
|
||||
if obj.test_trigger_at:
|
||||
return f'⏳ Pending: {obj.test_trigger_at.strftime("%H:%M")}'
|
||||
return '✅ Enabled (no trigger set)'
|
||||
test_mode_status.short_description = 'Test Mode'
|
||||
|
||||
def test_trigger_at_display(self, obj):
|
||||
"""Display-only field for test trigger time"""
|
||||
if obj.test_trigger_at:
|
||||
return obj.test_trigger_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
return 'Not set'
|
||||
test_trigger_at_display.short_description = 'Trigger At (Display)'
|
||||
|
||||
def next_scheduled_run(self, obj):
|
||||
"""
|
||||
Calculate the next scheduled run based on:
|
||||
- Celery Beat schedule (every 15 minutes at :00, :15, :30, :45)
|
||||
- Celery Beat schedule (every hour at :05)
|
||||
- Frequency (daily, weekly, monthly)
|
||||
- Scheduled time
|
||||
- Scheduled hour (user selects hour only, stored as HH:00)
|
||||
- 23-hour block after last_run_at
|
||||
|
||||
Celery checks window at :00 for :00-:14, at :15 for :15-:29, etc.
|
||||
So scheduled_time 12:12 will be picked up at the 12:00 check.
|
||||
SIMPLIFIED: Celery runs at :05 of every hour and checks if scheduled_hour == current_hour
|
||||
"""
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
@@ -53,25 +268,20 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
|
||||
now = timezone.now()
|
||||
scheduled_hour = obj.scheduled_time.hour
|
||||
scheduled_minute = obj.scheduled_time.minute
|
||||
|
||||
# Calculate the Celery window start time for this scheduled_time
|
||||
# If scheduled at :12, Celery checks at :00 (window :00-:14)
|
||||
# If scheduled at :35, Celery checks at :30 (window :30-:44)
|
||||
window_start_minute = (scheduled_minute // 15) * 15
|
||||
|
||||
# Calculate next occurrence based on frequency
|
||||
# Celery runs at :05, so we show the next :05 when the hour matches
|
||||
def get_next_celery_pickup():
|
||||
# Start with today at the Celery window start time
|
||||
# Start with today at the scheduled hour :05
|
||||
candidate = now.replace(
|
||||
hour=scheduled_hour,
|
||||
minute=window_start_minute,
|
||||
minute=5,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
|
||||
if obj.frequency == 'daily':
|
||||
# If time has passed today (Celery already checked this window), next is tomorrow
|
||||
# If time has passed today, next is tomorrow
|
||||
if candidate <= now:
|
||||
candidate += timedelta(days=1)
|
||||
elif obj.frequency == 'weekly':
|
||||
@@ -81,7 +291,7 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
# Today is Monday - check if time passed
|
||||
candidate = now.replace(
|
||||
hour=scheduled_hour,
|
||||
minute=window_start_minute,
|
||||
minute=5,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
@@ -92,7 +302,7 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
candidate = now + timedelta(days=days_until_monday)
|
||||
candidate = candidate.replace(
|
||||
hour=scheduled_hour,
|
||||
minute=window_start_minute,
|
||||
minute=5,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
@@ -101,7 +311,7 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
candidate = now.replace(
|
||||
day=1,
|
||||
hour=scheduled_hour,
|
||||
minute=window_start_minute,
|
||||
minute=5,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
@@ -122,10 +332,10 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
if next_celery_pickup < earliest_eligible:
|
||||
# Blocked - need to skip to next cycle
|
||||
if obj.frequency == 'daily':
|
||||
# Move to next day's window
|
||||
# Move to next day
|
||||
next_celery_pickup = earliest_eligible.replace(
|
||||
hour=scheduled_hour,
|
||||
minute=window_start_minute,
|
||||
minute=5,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
@@ -137,7 +347,7 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
if days_until_monday == 0:
|
||||
test_candidate = earliest_eligible.replace(
|
||||
hour=scheduled_hour,
|
||||
minute=window_start_minute,
|
||||
minute=5,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
@@ -146,7 +356,7 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
next_celery_pickup = earliest_eligible + timedelta(days=days_until_monday)
|
||||
next_celery_pickup = next_celery_pickup.replace(
|
||||
hour=scheduled_hour,
|
||||
minute=window_start_minute,
|
||||
minute=5,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
@@ -155,7 +365,7 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
next_celery_pickup = earliest_eligible.replace(
|
||||
day=1,
|
||||
hour=scheduled_hour,
|
||||
minute=window_start_minute,
|
||||
minute=5,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
@@ -170,6 +380,34 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
|
||||
next_scheduled_run.short_description = 'Next Scheduled Run'
|
||||
|
||||
def trigger_test_run(self, request, queryset):
|
||||
"""Set test trigger for immediate execution"""
|
||||
from django.utils import timezone
|
||||
|
||||
count = queryset.update(
|
||||
test_mode_enabled=True,
|
||||
test_trigger_at=timezone.now()
|
||||
)
|
||||
self.message_user(
|
||||
request,
|
||||
f'{count} automation(s) set to trigger at next minute check. Test mode enabled.',
|
||||
messages.SUCCESS
|
||||
)
|
||||
trigger_test_run.short_description = '🧪 Trigger test run (immediate)'
|
||||
|
||||
def clear_test_mode(self, request, queryset):
|
||||
"""Clear test mode and trigger time"""
|
||||
count = queryset.update(
|
||||
test_mode_enabled=False,
|
||||
test_trigger_at=None
|
||||
)
|
||||
self.message_user(
|
||||
request,
|
||||
f'{count} automation(s) test mode cleared.',
|
||||
messages.SUCCESS
|
||||
)
|
||||
clear_test_mode.short_description = '🧹 Clear test mode'
|
||||
|
||||
def bulk_enable(self, request, queryset):
|
||||
"""Enable selected automation configs"""
|
||||
updated = queryset.update(is_enabled=True)
|
||||
@@ -194,9 +432,9 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
return
|
||||
|
||||
FREQUENCY_CHOICES = [
|
||||
('hourly', 'Hourly'),
|
||||
('daily', 'Daily'),
|
||||
('weekly', 'Weekly'),
|
||||
('monthly', 'Monthly'),
|
||||
]
|
||||
|
||||
class FrequencyForm(forms.Form):
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated manually for test mode fields
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('automation', '0009_add_stage_use_testing_and_budget_pct'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automationconfig',
|
||||
name='test_mode_enabled',
|
||||
field=models.BooleanField(default=False, help_text='Enable test mode - allows test triggers'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='automationconfig',
|
||||
name='test_trigger_at',
|
||||
field=models.DateTimeField(blank=True, null=True, help_text='Set datetime to trigger a test run (auto-clears after trigger)'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='automationconfig',
|
||||
index=models.Index(fields=['test_mode_enabled', 'test_trigger_at'], name='automation__test_mo_idx'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
# Generated manually for DefaultAutomationConfig model
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('automation', '0010_add_test_mode_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DefaultAutomationConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('singleton_key', models.CharField(default='X', editable=False, max_length=1, unique=True)),
|
||||
('is_enabled', models.BooleanField(default=False, help_text='Default: Enable scheduled automation for new sites')),
|
||||
('frequency', models.CharField(choices=[('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly')], default='daily', max_length=20)),
|
||||
('base_scheduled_hour', models.IntegerField(default=2, help_text='Starting hour (0-23) for auto-assignment. Each new site gets next hour.')),
|
||||
('next_scheduled_hour', models.IntegerField(default=2, help_text='Next hour to assign (auto-increments, wraps at 24)')),
|
||||
('stage_1_enabled', models.BooleanField(default=True, help_text='Process Stage 1: Keywords → Clusters')),
|
||||
('stage_2_enabled', models.BooleanField(default=True, help_text='Process Stage 2: Clusters → Ideas')),
|
||||
('stage_3_enabled', models.BooleanField(default=True, help_text='Process Stage 3: Ideas → Tasks')),
|
||||
('stage_4_enabled', models.BooleanField(default=True, help_text='Process Stage 4: Tasks → Content')),
|
||||
('stage_5_enabled', models.BooleanField(default=True, help_text='Process Stage 5: Content → Image Prompts')),
|
||||
('stage_6_enabled', models.BooleanField(default=True, help_text='Process Stage 6: Image Prompts → Images')),
|
||||
('stage_7_enabled', models.BooleanField(default=True, help_text='Process Stage 7: Review → Published')),
|
||||
('stage_1_batch_size', models.IntegerField(default=50, help_text='Keywords per batch')),
|
||||
('stage_2_batch_size', models.IntegerField(default=1, help_text='Clusters at a time')),
|
||||
('stage_3_batch_size', models.IntegerField(default=20, help_text='Ideas per batch')),
|
||||
('stage_4_batch_size', models.IntegerField(default=1, help_text='Tasks - sequential')),
|
||||
('stage_5_batch_size', models.IntegerField(default=1, help_text='Content at a time')),
|
||||
('stage_6_batch_size', models.IntegerField(default=1, help_text='Images - sequential')),
|
||||
('stage_1_use_testing', models.BooleanField(default=False, help_text='Use testing model for Stage 1')),
|
||||
('stage_2_use_testing', models.BooleanField(default=False, help_text='Use testing model for Stage 2')),
|
||||
('stage_4_use_testing', models.BooleanField(default=False, help_text='Use testing model for Stage 4')),
|
||||
('stage_5_use_testing', models.BooleanField(default=False, help_text='Use testing model for Stage 5')),
|
||||
('stage_6_use_testing', models.BooleanField(default=False, help_text='Use testing model for Stage 6')),
|
||||
('stage_1_budget_pct', models.IntegerField(default=15, help_text='Budget percentage for Stage 1')),
|
||||
('stage_2_budget_pct', models.IntegerField(default=10, help_text='Budget percentage for Stage 2')),
|
||||
('stage_4_budget_pct', models.IntegerField(default=40, help_text='Budget percentage for Stage 4')),
|
||||
('stage_5_budget_pct', models.IntegerField(default=5, help_text='Budget percentage for Stage 5')),
|
||||
('stage_6_budget_pct', models.IntegerField(default=30, help_text='Budget percentage for Stage 6')),
|
||||
('within_stage_delay', models.IntegerField(default=3, help_text='Delay between batches within a stage (seconds)')),
|
||||
('between_stage_delay', models.IntegerField(default=5, help_text='Delay between stage transitions (seconds)')),
|
||||
('max_keywords_per_run', models.IntegerField(default=0, help_text='Max keywords to process in stage 1 (0=unlimited)')),
|
||||
('max_clusters_per_run', models.IntegerField(default=0, help_text='Max clusters to process in stage 2 (0=unlimited)')),
|
||||
('max_ideas_per_run', models.IntegerField(default=0, help_text='Max ideas to process in stage 3 (0=unlimited)')),
|
||||
('max_tasks_per_run', models.IntegerField(default=0, help_text='Max tasks to process in stage 4 (0=unlimited)')),
|
||||
('max_content_per_run', models.IntegerField(default=0, help_text='Max content pieces for image prompts in stage 5 (0=unlimited)')),
|
||||
('max_images_per_run', models.IntegerField(default=0, help_text='Max images to generate in stage 6 (0=unlimited)')),
|
||||
('max_approvals_per_run', models.IntegerField(default=0, help_text='Max content pieces to auto-approve in stage 7 (0=unlimited)')),
|
||||
('max_credits_per_run', models.IntegerField(default=0, help_text='Max credits to use per run (0=unlimited)')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Default Automation Config',
|
||||
'verbose_name_plural': 'Default Automation Config',
|
||||
'db_table': 'igny8_default_automation_config',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,73 @@
|
||||
# Generated manually for publishing and image default fields
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_default_values(apps, schema_editor):
|
||||
"""Set default values for publish_days and publish_time_slots"""
|
||||
DefaultAutomationConfig = apps.get_model('automation', 'DefaultAutomationConfig')
|
||||
for config in DefaultAutomationConfig.objects.all():
|
||||
if not config.publish_days:
|
||||
config.publish_days = ['mon', 'tue', 'wed', 'thu', 'fri']
|
||||
if not config.publish_time_slots:
|
||||
config.publish_time_slots = ['09:00', '14:00', '18:00']
|
||||
config.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('automation', '0011_add_default_automation_config'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Publishing defaults
|
||||
migrations.AddField(
|
||||
model_name='defaultautomationconfig',
|
||||
name='auto_approval_enabled',
|
||||
field=models.BooleanField(default=False, help_text='Auto-approve content after review'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='defaultautomationconfig',
|
||||
name='auto_publish_enabled',
|
||||
field=models.BooleanField(default=False, help_text='Auto-publish approved content to site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='defaultautomationconfig',
|
||||
name='daily_publish_limit',
|
||||
field=models.IntegerField(default=3, help_text='Max posts per day'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='defaultautomationconfig',
|
||||
name='weekly_publish_limit',
|
||||
field=models.IntegerField(default=15, help_text='Max posts per week'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='defaultautomationconfig',
|
||||
name='monthly_publish_limit',
|
||||
field=models.IntegerField(default=50, help_text='Max posts per month'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='defaultautomationconfig',
|
||||
name='publish_days',
|
||||
field=models.JSONField(default=list, help_text="Days to publish (e.g., ['mon', 'tue', 'wed', 'thu', 'fri'])"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='defaultautomationconfig',
|
||||
name='publish_time_slots',
|
||||
field=models.JSONField(default=list, help_text="Time slots to publish (e.g., ['09:00', '14:00', '18:00'])"),
|
||||
),
|
||||
# Image defaults
|
||||
migrations.AddField(
|
||||
model_name='defaultautomationconfig',
|
||||
name='image_style',
|
||||
field=models.CharField(default='photorealistic', help_text='Default image style', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='defaultautomationconfig',
|
||||
name='max_images_per_article',
|
||||
field=models.IntegerField(default=4, help_text='Images per article (1-8)'),
|
||||
),
|
||||
# Set default JSON values
|
||||
migrations.RunPython(set_default_values, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -7,6 +7,117 @@ from django.utils import timezone
|
||||
from igny8_core.auth.models import Account, Site
|
||||
|
||||
|
||||
class DefaultAutomationConfig(models.Model):
|
||||
"""
|
||||
Singleton model for default automation settings.
|
||||
Used when creating new sites - copies these defaults to the new AutomationConfig.
|
||||
Also tracks the next scheduled hour to auto-increment for each new site.
|
||||
"""
|
||||
|
||||
FREQUENCY_CHOICES = [
|
||||
('daily', 'Daily'),
|
||||
('weekly', 'Weekly'),
|
||||
('monthly', 'Monthly'),
|
||||
]
|
||||
|
||||
# Singleton - only one row allowed
|
||||
singleton_key = models.CharField(max_length=1, default='X', unique=True, editable=False)
|
||||
|
||||
# Default scheduling settings
|
||||
is_enabled = models.BooleanField(default=False, help_text="Default: Enable scheduled automation for new sites")
|
||||
frequency = models.CharField(max_length=20, choices=FREQUENCY_CHOICES, default='daily')
|
||||
base_scheduled_hour = models.IntegerField(default=2, help_text="Starting hour (0-23) for auto-assignment. Each new site gets next hour.")
|
||||
next_scheduled_hour = models.IntegerField(default=2, help_text="Next hour to assign (auto-increments, wraps at 24)")
|
||||
|
||||
# Stage processing toggles
|
||||
stage_1_enabled = models.BooleanField(default=True, help_text="Process Stage 1: Keywords → Clusters")
|
||||
stage_2_enabled = models.BooleanField(default=True, help_text="Process Stage 2: Clusters → Ideas")
|
||||
stage_3_enabled = models.BooleanField(default=True, help_text="Process Stage 3: Ideas → Tasks")
|
||||
stage_4_enabled = models.BooleanField(default=True, help_text="Process Stage 4: Tasks → Content")
|
||||
stage_5_enabled = models.BooleanField(default=True, help_text="Process Stage 5: Content → Image Prompts")
|
||||
stage_6_enabled = models.BooleanField(default=True, help_text="Process Stage 6: Image Prompts → Images")
|
||||
stage_7_enabled = models.BooleanField(default=True, help_text="Process Stage 7: Review → Published")
|
||||
|
||||
# Batch sizes per stage
|
||||
stage_1_batch_size = models.IntegerField(default=50, help_text="Keywords per batch")
|
||||
stage_2_batch_size = models.IntegerField(default=1, help_text="Clusters at a time")
|
||||
stage_3_batch_size = models.IntegerField(default=20, help_text="Ideas per batch")
|
||||
stage_4_batch_size = models.IntegerField(default=1, help_text="Tasks - sequential")
|
||||
stage_5_batch_size = models.IntegerField(default=1, help_text="Content at a time")
|
||||
stage_6_batch_size = models.IntegerField(default=1, help_text="Images - sequential")
|
||||
|
||||
# Use testing model per stage
|
||||
stage_1_use_testing = models.BooleanField(default=False, help_text="Use testing model for Stage 1")
|
||||
stage_2_use_testing = models.BooleanField(default=False, help_text="Use testing model for Stage 2")
|
||||
stage_4_use_testing = models.BooleanField(default=False, help_text="Use testing model for Stage 4")
|
||||
stage_5_use_testing = models.BooleanField(default=False, help_text="Use testing model for Stage 5")
|
||||
stage_6_use_testing = models.BooleanField(default=False, help_text="Use testing model for Stage 6")
|
||||
|
||||
# Budget percentage per stage
|
||||
stage_1_budget_pct = models.IntegerField(default=15, help_text="Budget percentage for Stage 1")
|
||||
stage_2_budget_pct = models.IntegerField(default=10, help_text="Budget percentage for Stage 2")
|
||||
stage_4_budget_pct = models.IntegerField(default=40, help_text="Budget percentage for Stage 4")
|
||||
stage_5_budget_pct = models.IntegerField(default=5, help_text="Budget percentage for Stage 5")
|
||||
stage_6_budget_pct = models.IntegerField(default=30, help_text="Budget percentage for Stage 6")
|
||||
|
||||
# Delay configuration
|
||||
within_stage_delay = models.IntegerField(default=3, help_text="Delay between batches within a stage (seconds)")
|
||||
between_stage_delay = models.IntegerField(default=5, help_text="Delay between stage transitions (seconds)")
|
||||
|
||||
# Per-run item limits
|
||||
max_keywords_per_run = models.IntegerField(default=0, help_text="Max keywords to process in stage 1 (0=unlimited)")
|
||||
max_clusters_per_run = models.IntegerField(default=0, help_text="Max clusters to process in stage 2 (0=unlimited)")
|
||||
max_ideas_per_run = models.IntegerField(default=0, help_text="Max ideas to process in stage 3 (0=unlimited)")
|
||||
max_tasks_per_run = models.IntegerField(default=0, help_text="Max tasks to process in stage 4 (0=unlimited)")
|
||||
max_content_per_run = models.IntegerField(default=0, help_text="Max content pieces for image prompts in stage 5 (0=unlimited)")
|
||||
max_images_per_run = models.IntegerField(default=0, help_text="Max images to generate in stage 6 (0=unlimited)")
|
||||
max_approvals_per_run = models.IntegerField(default=0, help_text="Max content pieces to auto-approve in stage 7 (0=unlimited)")
|
||||
max_credits_per_run = models.IntegerField(default=0, help_text="Max credits to use per run (0=unlimited)")
|
||||
|
||||
# ===== PUBLISHING DEFAULTS =====
|
||||
# Content publishing settings for new sites
|
||||
auto_approval_enabled = models.BooleanField(default=False, help_text="Auto-approve content after review")
|
||||
auto_publish_enabled = models.BooleanField(default=False, help_text="Auto-publish approved content to site")
|
||||
daily_publish_limit = models.IntegerField(default=3, help_text="Max posts per day")
|
||||
weekly_publish_limit = models.IntegerField(default=15, help_text="Max posts per week")
|
||||
monthly_publish_limit = models.IntegerField(default=50, help_text="Max posts per month")
|
||||
publish_days = models.JSONField(default=list, help_text="Days to publish (e.g., ['mon', 'tue', 'wed', 'thu', 'fri'])")
|
||||
publish_time_slots = models.JSONField(default=list, help_text="Time slots to publish (e.g., ['09:00', '14:00', '18:00'])")
|
||||
|
||||
# ===== IMAGE DEFAULTS =====
|
||||
# Image generation settings for new sites
|
||||
image_style = models.CharField(max_length=50, default='photorealistic', help_text="Default image style")
|
||||
max_images_per_article = models.IntegerField(default=4, help_text="Images per article (1-8)")
|
||||
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_default_automation_config'
|
||||
verbose_name = 'Default Automation Config'
|
||||
verbose_name_plural = 'Default Automation Config'
|
||||
|
||||
def __str__(self):
|
||||
return f"Default Automation Config (next hour: {self.next_scheduled_hour}:00)"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Ensure singleton
|
||||
self.singleton_key = 'X'
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get or create the singleton instance"""
|
||||
obj, created = cls.objects.get_or_create(singleton_key='X')
|
||||
return obj
|
||||
|
||||
def get_next_hour_and_increment(self):
|
||||
"""Get the next scheduled hour and increment for the next site"""
|
||||
current_hour = self.next_scheduled_hour
|
||||
self.next_scheduled_hour = (self.next_scheduled_hour + 1) % 24
|
||||
self.save(update_fields=['next_scheduled_hour'])
|
||||
return current_hour
|
||||
|
||||
|
||||
class AutomationConfig(models.Model):
|
||||
"""Per-site automation configuration"""
|
||||
|
||||
@@ -74,6 +185,10 @@ class AutomationConfig(models.Model):
|
||||
last_run_at = models.DateTimeField(null=True, blank=True)
|
||||
next_run_at = models.DateTimeField(null=True, blank=True, help_text="Calculated based on frequency")
|
||||
|
||||
# Test mode fields (for admin testing without waiting for hourly schedule)
|
||||
test_mode_enabled = models.BooleanField(default=False, help_text="Enable test mode - allows test triggers")
|
||||
test_trigger_at = models.DateTimeField(null=True, blank=True, help_text="Set datetime to trigger a test run (auto-clears after trigger)")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -84,10 +199,40 @@ class AutomationConfig(models.Model):
|
||||
indexes = [
|
||||
models.Index(fields=['is_enabled', 'next_run_at']),
|
||||
models.Index(fields=['account', 'site']),
|
||||
models.Index(fields=['test_mode_enabled', 'test_trigger_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Automation Config: {self.site.domain} ({self.frequency})"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
On first save (new config), auto-assign scheduled_time from DefaultAutomationConfig.
|
||||
- Gets next_scheduled_hour from DefaultAutomationConfig
|
||||
- Assigns it to this config
|
||||
- Increments next_scheduled_hour in DefaultAutomationConfig for future configs
|
||||
"""
|
||||
is_new = self.pk is None
|
||||
|
||||
if is_new:
|
||||
# Check if scheduled_time is still the default (not explicitly set by user)
|
||||
default_time_str = '02:00'
|
||||
current_time_str = self.scheduled_time.strftime('%H:%M') if hasattr(self.scheduled_time, 'strftime') else str(self.scheduled_time)
|
||||
|
||||
# Only auto-assign if using default time (user didn't explicitly set it)
|
||||
if current_time_str == default_time_str:
|
||||
try:
|
||||
# Get the tracked next hour from DefaultAutomationConfig and increment it
|
||||
default_config = DefaultAutomationConfig.get_instance()
|
||||
next_hour = default_config.get_next_hour_and_increment()
|
||||
|
||||
# Set the scheduled time to next_hour:00
|
||||
from datetime import time
|
||||
self.scheduled_time = time(hour=next_hour, minute=0)
|
||||
except Exception:
|
||||
pass # Keep default if something fails
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class AutomationRun(models.Model):
|
||||
@@ -96,6 +241,7 @@ class AutomationRun(models.Model):
|
||||
TRIGGER_TYPE_CHOICES = [
|
||||
('manual', 'Manual'),
|
||||
('scheduled', 'Scheduled'),
|
||||
('test', 'Test'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
|
||||
@@ -16,48 +16,38 @@ logger = get_task_logger(__name__)
|
||||
@shared_task(name='automation.check_scheduled_automations')
|
||||
def check_scheduled_automations():
|
||||
"""
|
||||
Check for scheduled automation runs (runs every 15 minutes)
|
||||
Matches automations scheduled within the current 15-minute window.
|
||||
Check for scheduled automation runs (runs every hour at :05)
|
||||
|
||||
SIMPLIFIED LOGIC:
|
||||
- User selects hour only (0-23), stored as HH:00
|
||||
- Celery runs at :05 of every hour
|
||||
- Match: scheduled_hour == current_hour
|
||||
"""
|
||||
logger.info("[AutomationTask] Checking scheduled automations")
|
||||
logger.info("[AutomationTask] Checking scheduled automations (hourly at :05)")
|
||||
|
||||
now = timezone.now()
|
||||
current_time = now.time()
|
||||
current_hour = now.hour
|
||||
|
||||
# Calculate 15-minute window boundaries
|
||||
# Window starts at current quarter hour (0, 15, 30, 45)
|
||||
window_start_minute = (current_time.minute // 15) * 15
|
||||
window_end_minute = window_start_minute + 14
|
||||
logger.info(f"[AutomationTask] Current hour: {current_hour}:05, checking for configs scheduled at hour {current_hour}")
|
||||
|
||||
logger.info(f"[AutomationTask] Current time: {current_time}, checking window {current_time.hour}:{window_start_minute:02d}-{current_time.hour}:{window_end_minute:02d}")
|
||||
|
||||
# Find configs that should run now
|
||||
# Find configs that should run now (matching hour)
|
||||
for config in AutomationConfig.objects.filter(is_enabled=True):
|
||||
# Check if it's time to run
|
||||
should_run = False
|
||||
scheduled_hour = config.scheduled_time.hour
|
||||
scheduled_minute = config.scheduled_time.minute
|
||||
|
||||
# Check if scheduled time falls within current 15-minute window
|
||||
def is_in_window():
|
||||
if current_time.hour != scheduled_hour:
|
||||
return False
|
||||
return window_start_minute <= scheduled_minute <= window_end_minute
|
||||
# Simple hour match
|
||||
should_run = False
|
||||
|
||||
if config.frequency == 'daily':
|
||||
# Run if scheduled_time falls within current 15-minute window
|
||||
if is_in_window():
|
||||
should_run = True
|
||||
# Run every day if hour matches
|
||||
should_run = (scheduled_hour == current_hour)
|
||||
elif config.frequency == 'weekly':
|
||||
# Run on Mondays within scheduled window
|
||||
if now.weekday() == 0 and is_in_window():
|
||||
should_run = True
|
||||
# Run on Mondays if hour matches
|
||||
should_run = (now.weekday() == 0 and scheduled_hour == current_hour)
|
||||
elif config.frequency == 'monthly':
|
||||
# Run on 1st of month within scheduled window
|
||||
if now.day == 1 and is_in_window():
|
||||
should_run = True
|
||||
# Run on 1st of month if hour matches
|
||||
should_run = (now.day == 1 and scheduled_hour == current_hour)
|
||||
|
||||
logger.debug(f"[AutomationTask] Site {config.site_id}: freq={config.frequency}, scheduled={config.scheduled_time}, should_run={should_run}")
|
||||
logger.debug(f"[AutomationTask] Site {config.site_id}: freq={config.frequency}, scheduled_hour={scheduled_hour}, current_hour={current_hour}, should_run={should_run}")
|
||||
|
||||
if should_run:
|
||||
# Check if already ran within the last 23 hours (prevents duplicate runs)
|
||||
@@ -90,6 +80,61 @@ def check_scheduled_automations():
|
||||
logger.error(f"[AutomationTask] Failed to start automation for site {config.site.id}: {e}")
|
||||
|
||||
|
||||
@shared_task(name='automation.check_test_triggers')
|
||||
def check_test_triggers():
|
||||
"""
|
||||
Check for test triggers (runs every minute, but only processes if test_mode_enabled)
|
||||
|
||||
This allows admins to test automation without waiting for hourly schedule.
|
||||
- Set test_mode_enabled=True and test_trigger_at=datetime on AutomationConfig
|
||||
- When test_trigger_at <= now, automation triggers immediately
|
||||
- Bypasses: is_enabled check, 23hr block, frequency rules
|
||||
- Run is marked as trigger_type='test'
|
||||
- test_trigger_at is cleared after trigger
|
||||
"""
|
||||
# Quick check - if no configs have test mode enabled, exit immediately
|
||||
if not AutomationConfig.objects.filter(test_mode_enabled=True).exists():
|
||||
return # No logging to avoid spam
|
||||
|
||||
logger.info("[AutomationTask] Checking test triggers")
|
||||
|
||||
now = timezone.now()
|
||||
|
||||
# Find configs with test mode enabled and trigger time passed
|
||||
test_configs = AutomationConfig.objects.filter(
|
||||
test_mode_enabled=True,
|
||||
test_trigger_at__isnull=False,
|
||||
test_trigger_at__lte=now
|
||||
)
|
||||
|
||||
for config in test_configs:
|
||||
logger.info(f"[AutomationTask] Test trigger found for site {config.site_id}, trigger_at={config.test_trigger_at}")
|
||||
|
||||
# Check if already running (still respect this to avoid conflicts)
|
||||
if AutomationRun.objects.filter(site=config.site, status__in=['running', 'paused']).exists():
|
||||
logger.info(f"[AutomationTask] Skipping test trigger for site {config.site_id} - automation in progress")
|
||||
continue
|
||||
|
||||
try:
|
||||
service = AutomationService(config.account, config.site)
|
||||
run_id = service.start_automation(trigger_type='test')
|
||||
|
||||
# Clear test_trigger_at (don't update last_run_at - test runs don't affect production schedule)
|
||||
config.test_trigger_at = None
|
||||
config.save(update_fields=['test_trigger_at'])
|
||||
|
||||
logger.info(f"[AutomationTask] Started test automation for site {config.site_id}, run_id={run_id}")
|
||||
|
||||
# Start async processing
|
||||
run_automation_task.delay(run_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[AutomationTask] Failed to start test automation for site {config.site_id}: {e}")
|
||||
# Clear trigger to prevent infinite retry
|
||||
config.test_trigger_at = None
|
||||
config.save(update_fields=['test_trigger_at'])
|
||||
|
||||
|
||||
@shared_task(name='automation.run_automation_task', bind=True, max_retries=0)
|
||||
def run_automation_task(self, run_id: str):
|
||||
"""
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.utils import timezone
|
||||
|
||||
from igny8_core.auth.models import Account, Site
|
||||
from igny8_core.business.integration.models import PublishingSettings
|
||||
from igny8_core.business.automation.models import AutomationConfig
|
||||
from igny8_core.business.automation.models import AutomationConfig, DefaultAutomationConfig
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -26,6 +26,7 @@ DEFAULT_PUBLISHING_SETTINGS = {
|
||||
'publish_time_slots': ['09:00', '14:00', '18:00'],
|
||||
}
|
||||
|
||||
# Fallback defaults if no DefaultAutomationConfig exists
|
||||
DEFAULT_AUTOMATION_SETTINGS = {
|
||||
'is_enabled': True,
|
||||
'frequency': 'daily',
|
||||
@@ -128,15 +129,66 @@ class DefaultsService:
|
||||
site: Site,
|
||||
overrides: Optional[Dict[str, Any]] = None
|
||||
) -> AutomationConfig:
|
||||
"""Create automation config with defaults, applying any overrides."""
|
||||
config_data = {**DEFAULT_AUTOMATION_SETTINGS}
|
||||
"""
|
||||
Create automation config using DefaultAutomationConfig settings.
|
||||
Each new site gets an auto-incremented scheduled hour.
|
||||
"""
|
||||
# Get default config (singleton) - creates if not exists
|
||||
default_config = DefaultAutomationConfig.get_instance()
|
||||
|
||||
# Get the next scheduled hour and increment it for future sites
|
||||
scheduled_hour = default_config.get_next_hour_and_increment()
|
||||
scheduled_time = f"{scheduled_hour:02d}:00"
|
||||
|
||||
# Build config from defaults
|
||||
config_data = {
|
||||
'is_enabled': default_config.is_enabled,
|
||||
'frequency': default_config.frequency,
|
||||
# Stage enabled toggles
|
||||
'stage_1_enabled': default_config.stage_1_enabled,
|
||||
'stage_2_enabled': default_config.stage_2_enabled,
|
||||
'stage_3_enabled': default_config.stage_3_enabled,
|
||||
'stage_4_enabled': default_config.stage_4_enabled,
|
||||
'stage_5_enabled': default_config.stage_5_enabled,
|
||||
'stage_6_enabled': default_config.stage_6_enabled,
|
||||
'stage_7_enabled': default_config.stage_7_enabled,
|
||||
# Batch sizes
|
||||
'stage_1_batch_size': default_config.stage_1_batch_size,
|
||||
'stage_2_batch_size': default_config.stage_2_batch_size,
|
||||
'stage_3_batch_size': default_config.stage_3_batch_size,
|
||||
'stage_4_batch_size': default_config.stage_4_batch_size,
|
||||
'stage_5_batch_size': default_config.stage_5_batch_size,
|
||||
'stage_6_batch_size': default_config.stage_6_batch_size,
|
||||
# Use testing model
|
||||
'stage_1_use_testing': default_config.stage_1_use_testing,
|
||||
'stage_2_use_testing': default_config.stage_2_use_testing,
|
||||
'stage_4_use_testing': default_config.stage_4_use_testing,
|
||||
'stage_5_use_testing': default_config.stage_5_use_testing,
|
||||
'stage_6_use_testing': default_config.stage_6_use_testing,
|
||||
# Budget percentages
|
||||
'stage_1_budget_pct': default_config.stage_1_budget_pct,
|
||||
'stage_2_budget_pct': default_config.stage_2_budget_pct,
|
||||
'stage_4_budget_pct': default_config.stage_4_budget_pct,
|
||||
'stage_5_budget_pct': default_config.stage_5_budget_pct,
|
||||
'stage_6_budget_pct': default_config.stage_6_budget_pct,
|
||||
# Delays
|
||||
'within_stage_delay': default_config.within_stage_delay,
|
||||
'between_stage_delay': default_config.between_stage_delay,
|
||||
# Per-run limits
|
||||
'max_keywords_per_run': default_config.max_keywords_per_run,
|
||||
'max_clusters_per_run': default_config.max_clusters_per_run,
|
||||
'max_ideas_per_run': default_config.max_ideas_per_run,
|
||||
'max_tasks_per_run': default_config.max_tasks_per_run,
|
||||
'max_content_per_run': default_config.max_content_per_run,
|
||||
'max_images_per_run': default_config.max_images_per_run,
|
||||
'max_approvals_per_run': default_config.max_approvals_per_run,
|
||||
'max_credits_per_run': default_config.max_credits_per_run,
|
||||
}
|
||||
|
||||
# Apply any overrides
|
||||
if overrides:
|
||||
config_data.update(overrides)
|
||||
|
||||
# Calculate next run time (tomorrow at scheduled time)
|
||||
scheduled_time = config_data.pop('scheduled_time', '02:00')
|
||||
|
||||
automation_config = AutomationConfig.objects.create(
|
||||
account=self.account,
|
||||
site=site,
|
||||
|
||||
@@ -57,7 +57,11 @@ app.conf.beat_schedule = {
|
||||
# Automation Tasks
|
||||
'check-scheduled-automations': {
|
||||
'task': 'automation.check_scheduled_automations',
|
||||
'schedule': crontab(minute='0,15,30,45'), # Every 15 minutes
|
||||
'schedule': crontab(minute=5), # Every hour at :05
|
||||
},
|
||||
'check-test-triggers': {
|
||||
'task': 'automation.check_test_triggers',
|
||||
'schedule': crontab(minute='*'), # Every minute (task self-checks if any test mode enabled)
|
||||
},
|
||||
# Publishing Scheduler Tasks
|
||||
'schedule-approved-content': {
|
||||
|
||||
@@ -10,7 +10,7 @@ from igny8_core.modules.integration.webhooks import (
|
||||
wordpress_status_webhook,
|
||||
wordpress_metadata_webhook,
|
||||
)
|
||||
from igny8_core.api.unified_settings import UnifiedSiteSettingsViewSet
|
||||
from igny8_core.api.unified_settings import UnifiedSiteSettingsViewSet, DefaultSettingsAPIView
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'integrations', IntegrationViewSet, basename='integration')
|
||||
@@ -31,6 +31,9 @@ unified_settings_viewset = UnifiedSiteSettingsViewSet.as_view({
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
|
||||
# Default settings (for reset functionality)
|
||||
path('settings/defaults/', DefaultSettingsAPIView.as_view(), name='settings-defaults'),
|
||||
|
||||
# Site-level publishing settings
|
||||
path('sites/<int:site_id>/publishing-settings/', publishing_settings_viewset, name='publishing-settings'),
|
||||
|
||||
|
||||
@@ -763,6 +763,7 @@ UNFOLD = {
|
||||
"icon": "settings_suggest",
|
||||
"collapsible": True,
|
||||
"items": [
|
||||
{"title": "Default Config", "icon": "settings", "link": lambda request: "/admin/automation/defaultautomationconfig/"},
|
||||
{"title": "Automation Configs", "icon": "tune", "link": lambda request: "/admin/automation/automationconfig/"},
|
||||
{"title": "Automation Runs", "icon": "play_circle", "link": lambda request: "/admin/automation/automationrun/"},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user