autmation final reaftocrs and setitgns dafautls
This commit is contained in:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user