302 lines
12 KiB
Python
302 lines
12 KiB
Python
"""
|
|
Admin registration for Automation models
|
|
"""
|
|
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 import_export.admin import ExportMixin
|
|
from import_export import resources
|
|
|
|
|
|
class AutomationConfigResource(resources.ModelResource):
|
|
"""Resource class for exporting Automation Configs"""
|
|
class Meta:
|
|
model = AutomationConfig
|
|
fields = ('id', 'site__domain', 'is_enabled', 'frequency', 'scheduled_time',
|
|
'last_run_at', 'next_run_at', 'created_at')
|
|
export_order = fields
|
|
|
|
|
|
@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')
|
|
search_fields = ('site__domain',)
|
|
actions = [
|
|
'bulk_enable',
|
|
'bulk_disable',
|
|
'bulk_update_frequency',
|
|
'bulk_update_delays',
|
|
]
|
|
|
|
def next_scheduled_run(self, obj):
|
|
"""
|
|
Calculate the next scheduled run based on:
|
|
- Celery Beat schedule (every 15 minutes at :00, :15, :30, :45)
|
|
- Frequency (daily, weekly, monthly)
|
|
- Scheduled time
|
|
- 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.
|
|
"""
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
if not obj.is_enabled:
|
|
return 'Disabled'
|
|
|
|
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
|
|
def get_next_celery_pickup():
|
|
# Start with today at the Celery window start time
|
|
candidate = now.replace(
|
|
hour=scheduled_hour,
|
|
minute=window_start_minute,
|
|
second=0,
|
|
microsecond=0
|
|
)
|
|
|
|
if obj.frequency == 'daily':
|
|
# If time has passed today (Celery already checked this window), next is tomorrow
|
|
if candidate <= now:
|
|
candidate += timedelta(days=1)
|
|
elif obj.frequency == 'weekly':
|
|
# Run on Mondays
|
|
days_until_monday = (7 - now.weekday()) % 7
|
|
if days_until_monday == 0:
|
|
# Today is Monday - check if time passed
|
|
candidate = now.replace(
|
|
hour=scheduled_hour,
|
|
minute=window_start_minute,
|
|
second=0,
|
|
microsecond=0
|
|
)
|
|
if candidate <= now:
|
|
days_until_monday = 7
|
|
candidate += timedelta(days=7)
|
|
else:
|
|
candidate = now + timedelta(days=days_until_monday)
|
|
candidate = candidate.replace(
|
|
hour=scheduled_hour,
|
|
minute=window_start_minute,
|
|
second=0,
|
|
microsecond=0
|
|
)
|
|
elif obj.frequency == 'monthly':
|
|
# Run on 1st of month
|
|
candidate = now.replace(
|
|
day=1,
|
|
hour=scheduled_hour,
|
|
minute=window_start_minute,
|
|
second=0,
|
|
microsecond=0
|
|
)
|
|
if candidate <= now:
|
|
# Next month
|
|
if now.month == 12:
|
|
candidate = candidate.replace(year=now.year + 1, month=1)
|
|
else:
|
|
candidate = candidate.replace(month=now.month + 1)
|
|
|
|
return candidate
|
|
|
|
next_celery_pickup = get_next_celery_pickup()
|
|
|
|
# Check 23-hour block
|
|
if obj.last_run_at:
|
|
earliest_eligible = obj.last_run_at + timedelta(hours=23)
|
|
if next_celery_pickup < earliest_eligible:
|
|
# Blocked - need to skip to next cycle
|
|
if obj.frequency == 'daily':
|
|
# Move to next day's window
|
|
next_celery_pickup = earliest_eligible.replace(
|
|
hour=scheduled_hour,
|
|
minute=window_start_minute,
|
|
second=0,
|
|
microsecond=0
|
|
)
|
|
if next_celery_pickup < earliest_eligible:
|
|
next_celery_pickup += timedelta(days=1)
|
|
elif obj.frequency == 'weekly':
|
|
# Find next Monday after earliest_eligible
|
|
days_until_monday = (7 - earliest_eligible.weekday()) % 7
|
|
if days_until_monday == 0:
|
|
test_candidate = earliest_eligible.replace(
|
|
hour=scheduled_hour,
|
|
minute=window_start_minute,
|
|
second=0,
|
|
microsecond=0
|
|
)
|
|
if test_candidate <= earliest_eligible:
|
|
days_until_monday = 7
|
|
next_celery_pickup = earliest_eligible + timedelta(days=days_until_monday)
|
|
next_celery_pickup = next_celery_pickup.replace(
|
|
hour=scheduled_hour,
|
|
minute=window_start_minute,
|
|
second=0,
|
|
microsecond=0
|
|
)
|
|
elif obj.frequency == 'monthly':
|
|
# Find next 1st of month after earliest_eligible
|
|
next_celery_pickup = earliest_eligible.replace(
|
|
day=1,
|
|
hour=scheduled_hour,
|
|
minute=window_start_minute,
|
|
second=0,
|
|
microsecond=0
|
|
)
|
|
if next_celery_pickup < earliest_eligible:
|
|
if earliest_eligible.month == 12:
|
|
next_celery_pickup = next_celery_pickup.replace(year=earliest_eligible.year + 1, month=1)
|
|
else:
|
|
next_celery_pickup = next_celery_pickup.replace(month=earliest_eligible.month + 1)
|
|
|
|
# Format nicely
|
|
return next_celery_pickup.strftime('%b %d, %Y, %-I:%M %p')
|
|
|
|
next_scheduled_run.short_description = 'Next Scheduled Run'
|
|
|
|
def bulk_enable(self, request, queryset):
|
|
"""Enable selected automation configs"""
|
|
updated = queryset.update(is_enabled=True)
|
|
self.message_user(request, f'{updated} automation config(s) enabled.', messages.SUCCESS)
|
|
bulk_enable.short_description = 'Enable selected automations'
|
|
|
|
def bulk_disable(self, request, queryset):
|
|
"""Disable selected automation configs"""
|
|
updated = queryset.update(is_enabled=False)
|
|
self.message_user(request, f'{updated} automation config(s) disabled.', messages.SUCCESS)
|
|
bulk_disable.short_description = 'Disable selected automations'
|
|
|
|
def bulk_update_frequency(self, request, queryset):
|
|
"""Update frequency for selected automation configs"""
|
|
from django import forms
|
|
|
|
if 'apply' in request.POST:
|
|
frequency = request.POST.get('frequency')
|
|
if frequency:
|
|
updated = queryset.update(frequency=frequency)
|
|
self.message_user(request, f'{updated} automation config(s) updated to frequency: {frequency}', messages.SUCCESS)
|
|
return
|
|
|
|
FREQUENCY_CHOICES = [
|
|
('hourly', 'Hourly'),
|
|
('daily', 'Daily'),
|
|
('weekly', 'Weekly'),
|
|
]
|
|
|
|
class FrequencyForm(forms.Form):
|
|
frequency = forms.ChoiceField(
|
|
choices=FREQUENCY_CHOICES,
|
|
label="Select Frequency",
|
|
help_text=f"Update frequency for {queryset.count()} automation config(s)"
|
|
)
|
|
|
|
from django.shortcuts import render
|
|
return render(request, 'admin/bulk_action_form.html', {
|
|
'title': 'Update Automation Frequency',
|
|
'queryset': queryset,
|
|
'form': FrequencyForm(),
|
|
'action': 'bulk_update_frequency',
|
|
})
|
|
bulk_update_frequency.short_description = 'Update frequency'
|
|
|
|
def bulk_update_delays(self, request, queryset):
|
|
"""Update delay settings for selected automation configs"""
|
|
from django import forms
|
|
|
|
if 'apply' in request.POST:
|
|
within_delay = int(request.POST.get('within_stage_delay', 0))
|
|
between_delay = int(request.POST.get('between_stage_delay', 0))
|
|
|
|
updated = queryset.update(
|
|
within_stage_delay=within_delay,
|
|
between_stage_delay=between_delay
|
|
)
|
|
self.message_user(request, f'{updated} automation config(s) delay settings updated.', messages.SUCCESS)
|
|
return
|
|
|
|
class DelayForm(forms.Form):
|
|
within_stage_delay = forms.IntegerField(
|
|
min_value=0,
|
|
initial=10,
|
|
label="Within Stage Delay (minutes)",
|
|
help_text="Delay between operations within the same stage"
|
|
)
|
|
between_stage_delay = forms.IntegerField(
|
|
min_value=0,
|
|
initial=60,
|
|
label="Between Stage Delay (minutes)",
|
|
help_text="Delay between different stages"
|
|
)
|
|
|
|
from django.shortcuts import render
|
|
return render(request, 'admin/bulk_action_form.html', {
|
|
'title': 'Update Automation Delays',
|
|
'queryset': queryset,
|
|
'form': DelayForm(),
|
|
'action': 'bulk_update_delays',
|
|
})
|
|
bulk_update_delays.short_description = 'Update delay settings'
|
|
|
|
|
|
class AutomationRunResource(resources.ModelResource):
|
|
"""Resource class for exporting Automation Runs"""
|
|
class Meta:
|
|
model = AutomationRun
|
|
fields = ('id', 'run_id', 'site__domain', 'status', 'current_stage',
|
|
'started_at', 'completed_at', 'created_at')
|
|
export_order = fields
|
|
|
|
|
|
@admin.register(AutomationRun)
|
|
class AutomationRunAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|
resource_class = AutomationRunResource
|
|
list_display = ('run_id', 'site', 'status', 'current_stage', 'started_at', 'completed_at')
|
|
list_filter = ('status', 'current_stage')
|
|
search_fields = ('run_id', 'site__domain')
|
|
actions = [
|
|
'bulk_retry_failed',
|
|
'bulk_cancel_running',
|
|
'bulk_delete_old_runs',
|
|
]
|
|
|
|
def bulk_retry_failed(self, request, queryset):
|
|
"""Retry failed automation runs"""
|
|
failed_runs = queryset.filter(status='failed')
|
|
count = failed_runs.update(status='pending', current_stage='keyword_research')
|
|
self.message_user(request, f'{count} failed run(s) marked for retry.', messages.SUCCESS)
|
|
bulk_retry_failed.short_description = 'Retry failed runs'
|
|
|
|
def bulk_cancel_running(self, request, queryset):
|
|
"""Cancel running automation runs"""
|
|
running = queryset.filter(status__in=['pending', 'running'])
|
|
count = running.update(status='failed')
|
|
self.message_user(request, f'{count} running automation(s) cancelled.', messages.SUCCESS)
|
|
bulk_cancel_running.short_description = 'Cancel running automations'
|
|
|
|
def bulk_delete_old_runs(self, request, queryset):
|
|
"""Delete automation runs older than 30 days"""
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
cutoff_date = timezone.now() - timedelta(days=30)
|
|
old_runs = queryset.filter(created_at__lt=cutoff_date)
|
|
count = old_runs.count()
|
|
old_runs.delete()
|
|
self.message_user(request, f'{count} old automation run(s) deleted (older than 30 days).', messages.SUCCESS)
|
|
bulk_delete_old_runs.short_description = 'Delete old runs (>30 days)' |