""" Automation Models Tracks automation runs and configuration """ from django.db import models 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""" FREQUENCY_CHOICES = [ ('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ] account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='automation_configs') site = models.OneToOneField(Site, on_delete=models.CASCADE, related_name='automation_config') is_enabled = models.BooleanField(default=False, help_text="Whether scheduled automation is active") frequency = models.CharField(max_length=20, choices=FREQUENCY_CHOICES, default='daily') scheduled_time = models.TimeField(default='02:00', help_text="Time to run (e.g., 02:00)") # 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 (only for AI stages: 1, 2, 4, 5, 6) 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 (only for AI stages: 1, 2, 4, 5, 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") # Delay configuration (in seconds) 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 (0 = unlimited, processes all available) # These prevent runaway automation and control resource usage 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)") # Credit budget limit per run (0 = use site's full credit balance) max_credits_per_run = models.IntegerField(default=0, help_text="Max credits to use per run (0=unlimited)") 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) class Meta: db_table = 'igny8_automation_configs' verbose_name = 'Automation Config' verbose_name_plural = 'Automation Configs' 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): """Tracks each automation execution""" TRIGGER_TYPE_CHOICES = [ ('manual', 'Manual'), ('scheduled', 'Scheduled'), ('test', 'Test'), ] STATUS_CHOICES = [ ('running', 'Running'), ('paused', 'Paused'), ('cancelled', 'Cancelled'), ('completed', 'Completed'), ('failed', 'Failed'), ] run_id = models.CharField(max_length=100, unique=True, db_index=True, help_text="Format: run_20251203_140523_manual") account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='automation_runs') site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name='automation_runs') trigger_type = models.CharField(max_length=20, choices=TRIGGER_TYPE_CHOICES) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='running', db_index=True) current_stage = models.IntegerField(default=1, help_text="Current stage number (1-7)") # Pause/Resume tracking paused_at = models.DateTimeField(null=True, blank=True, help_text="When automation was paused") resumed_at = models.DateTimeField(null=True, blank=True, help_text="When automation was last resumed") cancelled_at = models.DateTimeField(null=True, blank=True, help_text="When automation was cancelled") started_at = models.DateTimeField(auto_now_add=True, db_index=True) completed_at = models.DateTimeField(null=True, blank=True) total_credits_used = models.IntegerField(default=0) # Initial queue snapshot - captured at run start for accurate progress tracking initial_snapshot = models.JSONField( default=dict, blank=True, help_text="Snapshot of initial queue sizes: {stage_1_initial, stage_2_initial, ..., total_initial_items}" ) # JSON results per stage stage_1_result = models.JSONField(null=True, blank=True, help_text="{keywords_processed, clusters_created, batches}") stage_2_result = models.JSONField(null=True, blank=True, help_text="{clusters_processed, ideas_created}") stage_3_result = models.JSONField(null=True, blank=True, help_text="{ideas_processed, tasks_created}") stage_4_result = models.JSONField(null=True, blank=True, help_text="{tasks_processed, content_created, total_words}") stage_5_result = models.JSONField(null=True, blank=True, help_text="{content_processed, prompts_created}") stage_6_result = models.JSONField(null=True, blank=True, help_text="{images_processed, images_generated}") stage_7_result = models.JSONField(null=True, blank=True, help_text="{ready_for_review}") error_message = models.TextField(null=True, blank=True) class Meta: db_table = 'igny8_automation_runs' verbose_name = 'Automation Run' verbose_name_plural = 'Automation Runs' ordering = ['-started_at'] indexes = [ models.Index(fields=['site', '-started_at']), models.Index(fields=['status', '-started_at']), models.Index(fields=['account', '-started_at']), ] def __str__(self): return f"{self.run_id} - {self.site.domain} ({self.status})"