old automation cleanup adn status feilds of planner udpate

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-03 05:13:53 +00:00
parent 7df6e190fc
commit c9f082cb12
40 changed files with 1832 additions and 2134 deletions

View File

@@ -1,5 +0,0 @@
"""
Automation Module - API Layer
Business logic is in business/automation/
"""

View File

@@ -1,13 +0,0 @@
"""
Automation App Configuration
"""
from django.apps import AppConfig
class AutomationConfig(AppConfig):
"""Configuration for automation module"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'igny8_core.modules.automation'
label = 'automation'
verbose_name = 'Automation'

View File

@@ -1,60 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-20 23:27
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AutomationRule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Rule name', max_length=255)),
('description', models.TextField(blank=True, help_text='Rule description', null=True)),
('trigger', models.CharField(choices=[('schedule', 'Schedule'), ('event', 'Event'), ('manual', 'Manual')], default='manual', max_length=50)),
('schedule', models.CharField(blank=True, help_text="Cron-like schedule string (e.g., '0 0 * * *' for daily at midnight)", max_length=100, null=True)),
('conditions', models.JSONField(default=list, help_text='List of conditions that must be met for rule to execute')),
('actions', models.JSONField(default=list, help_text='List of actions to execute when rule triggers')),
('is_active', models.BooleanField(default=True, help_text='Whether rule is active')),
('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('paused', 'Paused')], default='active', max_length=50)),
('last_executed_at', models.DateTimeField(blank=True, null=True)),
('execution_count', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)])),
('metadata', models.JSONField(default=dict, help_text='Additional metadata')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Automation Rule',
'verbose_name_plural': 'Automation Rules',
'db_table': 'igny8_automation_rules',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='ScheduledTask',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('scheduled_at', models.DateTimeField(help_text='When the task is scheduled to run')),
('executed_at', models.DateTimeField(blank=True, help_text='When the task was actually executed', null=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed'), ('cancelled', 'Cancelled')], default='pending', max_length=50)),
('result', models.JSONField(default=dict, help_text='Execution result data')),
('error_message', models.TextField(blank=True, help_text='Error message if execution failed', null=True)),
('metadata', models.JSONField(default=dict, help_text='Additional metadata')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Scheduled Task',
'verbose_name_plural': 'Scheduled Tasks',
'db_table': 'igny8_scheduled_tasks',
'ordering': ['-scheduled_at'],
},
),
]

View File

@@ -1,74 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-20 23:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('automation', '0001_initial'),
('igny8_core_auth', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='automationrule',
name='account',
field=models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account'),
),
migrations.AddField(
model_name='automationrule',
name='sector',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector'),
),
migrations.AddField(
model_name='automationrule',
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site'),
),
migrations.AddField(
model_name='scheduledtask',
name='account',
field=models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account'),
),
migrations.AddField(
model_name='scheduledtask',
name='automation_rule',
field=models.ForeignKey(help_text='The automation rule this task belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_tasks', to='automation.automationrule'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['trigger', 'is_active'], name='igny8_autom_trigger_32979f_idx'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['status'], name='igny8_autom_status_827c0d_idx'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['site', 'sector'], name='igny8_autom_site_id_d0a51d_idx'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['trigger', 'is_active', 'status'], name='igny8_autom_trigger_f3f3e2_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['automation_rule', 'status'], name='igny8_sched_automat_da6c85_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['scheduled_at', 'status'], name='igny8_sched_schedul_1e3342_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['account', 'status'], name='igny8_sched_tenant__7244a8_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['status', 'scheduled_at'], name='igny8_sched_status_21f32f_idx'),
),
]

View File

@@ -1,5 +0,0 @@
# Backward compatibility alias - models moved to business/automation/
from igny8_core.business.automation.models import AutomationRule, ScheduledTask
__all__ = ['AutomationRule', 'ScheduledTask']

View File

@@ -1,36 +0,0 @@
"""
Serializers for Automation Models
"""
from rest_framework import serializers
from igny8_core.business.automation.models import AutomationRule, ScheduledTask
class AutomationRuleSerializer(serializers.ModelSerializer):
"""Serializer for AutomationRule model"""
class Meta:
model = AutomationRule
fields = [
'id', 'name', 'description', 'trigger', 'schedule',
'conditions', 'actions', 'is_active', 'status',
'last_executed_at', 'execution_count',
'metadata', 'created_at', 'updated_at',
'account', 'site', 'sector'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'last_executed_at', 'execution_count']
class ScheduledTaskSerializer(serializers.ModelSerializer):
"""Serializer for ScheduledTask model"""
automation_rule_name = serializers.CharField(source='automation_rule.name', read_only=True)
class Meta:
model = ScheduledTask
fields = [
'id', 'automation_rule', 'automation_rule_name',
'scheduled_at', 'executed_at', 'status',
'result', 'error_message', 'metadata',
'created_at', 'updated_at', 'account'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'executed_at']

View File

@@ -1,15 +0,0 @@
"""
URL patterns for automation module.
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import AutomationRuleViewSet, ScheduledTaskViewSet
router = DefaultRouter()
router.register(r'rules', AutomationRuleViewSet, basename='automation-rule')
router.register(r'scheduled-tasks', ScheduledTaskViewSet, basename='scheduled-task')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -1,92 +0,0 @@
"""
ViewSets for Automation Models
Unified API Standard v1.0 compliant
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from drf_spectacular.utils import extend_schema, extend_schema_view
from igny8_core.api.base import SiteSectorModelViewSet, AccountModelViewSet
from igny8_core.api.pagination import CustomPageNumberPagination
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsViewerOrAbove
from igny8_core.business.automation.models import AutomationRule, ScheduledTask
from igny8_core.business.automation.services.automation_service import AutomationService
from .serializers import AutomationRuleSerializer, ScheduledTaskSerializer
@extend_schema_view(
list=extend_schema(tags=['Automation']),
create=extend_schema(tags=['Automation']),
retrieve=extend_schema(tags=['Automation']),
update=extend_schema(tags=['Automation']),
partial_update=extend_schema(tags=['Automation']),
destroy=extend_schema(tags=['Automation']),
)
class AutomationRuleViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing automation rules
Unified API Standard v1.0 compliant
"""
queryset = AutomationRule.objects.all()
serializer_class = AutomationRuleSerializer
permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove]
pagination_class = CustomPageNumberPagination
throttle_scope = 'automation'
throttle_classes = [DebugScopedRateThrottle]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
search_fields = ['name', 'description']
ordering_fields = ['name', 'created_at', 'last_executed_at', 'execution_count']
ordering = ['-created_at']
filterset_fields = ['trigger', 'is_active', 'status']
@action(detail=True, methods=['post'], url_path='execute', url_name='execute')
def execute(self, request, pk=None):
"""Manually execute an automation rule"""
rule = self.get_object()
service = AutomationService()
try:
result = service.execute_rule(rule, context=request.data.get('context', {}))
return success_response(
data=result,
message='Rule executed successfully',
request=request
)
except Exception as e:
return error_response(
error=str(e),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
@extend_schema_view(
list=extend_schema(tags=['Automation']),
create=extend_schema(tags=['Automation']),
retrieve=extend_schema(tags=['Automation']),
update=extend_schema(tags=['Automation']),
partial_update=extend_schema(tags=['Automation']),
destroy=extend_schema(tags=['Automation']),
)
class ScheduledTaskViewSet(AccountModelViewSet):
"""
ViewSet for managing scheduled tasks
Unified API Standard v1.0 compliant
"""
queryset = ScheduledTask.objects.select_related('automation_rule')
serializer_class = ScheduledTaskSerializer
permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove]
pagination_class = CustomPageNumberPagination
throttle_scope = 'automation'
throttle_classes = [DebugScopedRateThrottle]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
ordering_fields = ['scheduled_at', 'executed_at', 'status', 'created_at']
ordering = ['-scheduled_at']
filterset_fields = ['automation_rule', 'status']

View File

@@ -0,0 +1,114 @@
# Generated migration for unified status refactor
from django.db import migrations, models
def migrate_status_data(apps, schema_editor):
"""Transform existing status data to new values"""
Keywords = apps.get_model('planner', 'Keywords')
Clusters = apps.get_model('planner', 'Clusters')
ContentIdeas = apps.get_model('planner', 'ContentIdeas')
# Keywords: pending→new, active→mapped, archived→mapped+disabled=true
Keywords.objects.filter(status='pending').update(status='new')
Keywords.objects.filter(status='active').update(status='mapped')
# Handle archived: set to mapped and mark as disabled
archived_keywords = Keywords.objects.filter(status='archived')
for kw in archived_keywords:
kw.status = 'mapped'
kw.disabled = True
kw.save()
# Clusters: active (with ideas)→mapped, active (no ideas)→new
# Check if cluster has any related ideas using the reverse relationship
for cluster in Clusters.objects.all():
if cluster.ideas.exists():
cluster.status = 'mapped'
else:
cluster.status = 'new'
cluster.save()
# ContentIdeas: scheduled→queued, published→completed, new stays new
ContentIdeas.objects.filter(status='scheduled').update(status='queued')
ContentIdeas.objects.filter(status='published').update(status='completed')
def reverse_status_data(apps, schema_editor):
"""Reverse migration: restore old status values"""
Keywords = apps.get_model('planner', 'Keywords')
Clusters = apps.get_model('planner', 'Clusters')
ContentIdeas = apps.get_model('planner', 'ContentIdeas')
# Keywords: new→pending, mapped→active (or archived if disabled)
Keywords.objects.filter(status='new').update(status='pending')
Keywords.objects.filter(status='mapped', disabled=False).update(status='active')
Keywords.objects.filter(status='mapped', disabled=True).update(status='archived', disabled=False)
# Clusters: all back to 'active'
Clusters.objects.all().update(status='active')
# ContentIdeas: queued→scheduled, completed→published
ContentIdeas.objects.filter(status='queued').update(status='scheduled')
ContentIdeas.objects.filter(status='completed').update(status='published')
class Migration(migrations.Migration):
dependencies = [
('planner', '0005_field_rename_implementation'),
]
operations = [
# Step 1: Add disabled field to all models (with default=False)
migrations.AddField(
model_name='clusters',
name='disabled',
field=models.BooleanField(default=False, help_text='Exclude from processes'),
),
migrations.AddField(
model_name='keywords',
name='disabled',
field=models.BooleanField(default=False, help_text='Exclude from processes'),
),
migrations.AddField(
model_name='contentideas',
name='disabled',
field=models.BooleanField(default=False, help_text='Exclude from processes'),
),
# Step 2: Alter Keywords status field choices
migrations.AlterField(
model_name='keywords',
name='status',
field=models.CharField(
choices=[('new', 'New'), ('mapped', 'Mapped')],
default='new',
max_length=50
),
),
# Step 3: Alter Clusters status field (add choices, change default)
migrations.AlterField(
model_name='clusters',
name='status',
field=models.CharField(
choices=[('new', 'New'), ('mapped', 'Mapped')],
default='new',
max_length=50
),
),
# Step 4: Alter ContentIdeas status field choices
migrations.AlterField(
model_name='contentideas',
name='status',
field=models.CharField(
choices=[('new', 'New'), ('queued', 'Queued'), ('completed', 'Completed')],
default='new',
max_length=50
),
),
# Step 5: Data migration - transform existing records
migrations.RunPython(migrate_status_data, reverse_status_data),
]

View File

@@ -560,7 +560,7 @@ class KeywordViewSet(SiteSectorModelViewSet):
volume=int(row.get('volume', 0) or 0),
difficulty=int(row.get('difficulty', 0) or 0),
intent=row.get('intent', 'informational') or 'informational',
status=row.get('status', 'pending') or 'pending',
status=row.get('status', 'new') or 'new',
site=site,
sector=sector,
account=account
@@ -1080,8 +1080,8 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
created_tasks.append(task.id)
# Update idea status
idea.status = 'scheduled'
# Update idea status to queued
idea.status = 'queued'
idea.save()
except Exception as e:
errors.append({