343 lines
11 KiB
Python
343 lines
11 KiB
Python
"""
|
|
Admin interface for sync management
|
|
"""
|
|
from django.contrib import admin
|
|
from django_q.models import Task, Schedule
|
|
from django.utils.html import format_html
|
|
from django.urls import reverse
|
|
from django.utils.safestring import mark_safe
|
|
import json
|
|
|
|
|
|
class SyncTaskAdmin(admin.ModelAdmin):
|
|
"""Admin interface for monitoring sync tasks"""
|
|
|
|
list_display = [
|
|
'id', 'task_name', 'task_status', 'started_display',
|
|
'stopped_display', 'result_display', 'actions_display'
|
|
]
|
|
list_filter = ['success', 'stopped', 'group']
|
|
search_fields = ['name', 'func', 'group']
|
|
readonly_fields = [
|
|
'id', 'name', 'func', 'args', 'kwargs', 'started', 'stopped',
|
|
'result', 'success', 'group', 'attempt_count', 'retries',
|
|
'time_taken', 'stopped_early'
|
|
]
|
|
|
|
def task_name(self, obj):
|
|
"""Display task name with group if available"""
|
|
if obj.group:
|
|
return f"{obj.name} ({obj.group})"
|
|
return obj.name
|
|
task_name.short_description = 'Task Name'
|
|
|
|
def task_status(self, obj):
|
|
"""Display task status with color coding"""
|
|
if obj.success:
|
|
color = 'green'
|
|
status = 'SUCCESS'
|
|
elif obj.stopped:
|
|
color = 'red'
|
|
status = 'FAILED'
|
|
else:
|
|
color = 'orange'
|
|
status = 'PENDING'
|
|
|
|
return format_html(
|
|
'<span style="color: {}; font-weight: bold;">{}</span>',
|
|
color, status
|
|
)
|
|
task_status.short_description = 'Status'
|
|
|
|
def started_display(self, obj):
|
|
"""Format started time"""
|
|
if obj.started:
|
|
return obj.started.strftime('%Y-%m-%d %H:%M:%S')
|
|
return '--'
|
|
started_display.short_description = 'Started'
|
|
|
|
def stopped_display(self, obj):
|
|
"""Format stopped time"""
|
|
if obj.stopped:
|
|
return obj.stopped.strftime('%Y-%m-%d %H:%M:%S')
|
|
return '--'
|
|
stopped_display.short_description = 'Stopped'
|
|
|
|
def result_display(self, obj):
|
|
"""Display result summary"""
|
|
if not obj.result:
|
|
return '--'
|
|
|
|
try:
|
|
result = json.loads(obj.result) if isinstance(obj.result, str) else obj.result
|
|
|
|
if isinstance(result, dict):
|
|
if 'summary' in result:
|
|
summary = result['summary']
|
|
return format_html(
|
|
"Sources: {}, Success: {}, Failed: {}",
|
|
summary.get('total_sources', 0),
|
|
summary.get('successful', 0),
|
|
summary.get('failed', 0)
|
|
)
|
|
elif 'error' in result:
|
|
return format_html(
|
|
'<span style="color: red;">Error: {}</span>',
|
|
result['error'][:100]
|
|
)
|
|
|
|
return str(result)[:100] + '...' if len(str(result)) > 100 else str(result)
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
return str(obj.result)[:100] + '...' if len(str(obj.result)) > 100 else str(obj.result)
|
|
|
|
result_display.short_description = 'Result Summary'
|
|
|
|
def actions_display(self, obj):
|
|
"""Display action buttons"""
|
|
actions = []
|
|
|
|
if obj.group:
|
|
# Link to view all tasks in this group
|
|
url = reverse('admin:django_q_task_changelist') + f'?group__exact={obj.group}'
|
|
actions.append(
|
|
f'<a href="{url}" class="button">View Group</a>'
|
|
)
|
|
|
|
return mark_safe(' '.join(actions))
|
|
|
|
actions_display.short_description = 'Actions'
|
|
|
|
def has_add_permission(self, request):
|
|
"""Disable adding tasks through admin"""
|
|
return False
|
|
|
|
def has_change_permission(self, request, obj=None):
|
|
"""Disable editing tasks through admin"""
|
|
return False
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
"""Allow deleting tasks"""
|
|
return True
|
|
|
|
|
|
class SyncScheduleAdmin(admin.ModelAdmin):
|
|
"""Admin interface for managing scheduled sync tasks"""
|
|
|
|
list_display = [
|
|
'name', 'func', 'schedule_type', 'next_run_display',
|
|
'repeats_display', 'enabled_display'
|
|
]
|
|
list_filter = ['repeats', 'schedule_type', 'enabled']
|
|
search_fields = ['name', 'func']
|
|
readonly_fields = ['last_run', 'next_run']
|
|
|
|
fieldsets = (
|
|
('Basic Information', {
|
|
'fields': ('name', 'func', 'enabled')
|
|
}),
|
|
('Schedule Configuration', {
|
|
'fields': (
|
|
'schedule_type', 'repeats', 'cron', 'next_run',
|
|
'minutes', 'hours', 'days', 'weeks'
|
|
)
|
|
}),
|
|
('Task Arguments', {
|
|
'fields': ('args', 'kwargs'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('Runtime Information', {
|
|
'fields': ('last_run', 'next_run'),
|
|
'classes': ('collapse',)
|
|
})
|
|
)
|
|
|
|
def schedule_type_display(self, obj):
|
|
"""Display schedule type with icon"""
|
|
icons = {
|
|
'O': '🕐', # Once
|
|
'I': '🔄', # Interval
|
|
'C': '📅', # Cron
|
|
'D': '📆', # Daily
|
|
'W': '📋', # Weekly
|
|
'M': '📊', # Monthly
|
|
'Y': '📈', # Yearly
|
|
'H': '⏰', # Hourly
|
|
'Q': '📈', # Quarterly
|
|
}
|
|
|
|
icon = icons.get(obj.schedule_type, '❓')
|
|
type_names = {
|
|
'O': 'Once',
|
|
'I': 'Interval',
|
|
'C': 'Cron',
|
|
'D': 'Daily',
|
|
'W': 'Weekly',
|
|
'M': 'Monthly',
|
|
'Y': 'Yearly',
|
|
'H': 'Hourly',
|
|
'Q': 'Quarterly',
|
|
}
|
|
|
|
name = type_names.get(obj.schedule_type, obj.schedule_type)
|
|
return format_html('{} {}', icon, name)
|
|
|
|
schedule_type_display.short_description = 'Schedule Type'
|
|
|
|
def next_run_display(self, obj):
|
|
"""Format next run time"""
|
|
if obj.next_run:
|
|
return obj.next_run.strftime('%Y-%m-%d %H:%M:%S')
|
|
return '--'
|
|
|
|
next_run_display.short_description = 'Next Run'
|
|
|
|
def repeats_display(self, obj):
|
|
"""Display repeat count"""
|
|
if obj.repeats == -1:
|
|
return '∞ (Forever)'
|
|
return str(obj.repeats)
|
|
|
|
repeats_display.short_description = 'Repeats'
|
|
|
|
def enabled_display(self, obj):
|
|
"""Display enabled status with color"""
|
|
if obj.enabled:
|
|
return format_html(
|
|
'<span style="color: green; font-weight: bold;">✓ Enabled</span>'
|
|
)
|
|
else:
|
|
return format_html(
|
|
'<span style="color: red; font-weight: bold;">✗ Disabled</span>'
|
|
)
|
|
|
|
enabled_display.short_description = 'Status'
|
|
|
|
|
|
# Custom admin site for sync management
|
|
class SyncAdminSite(admin.AdminSite):
|
|
"""Custom admin site for sync management"""
|
|
site_header = 'ATS Sync Management'
|
|
site_title = 'Sync Management'
|
|
index_title = 'Sync Task Management'
|
|
|
|
def get_urls(self):
|
|
"""Add custom URLs for sync management"""
|
|
from django.urls import path
|
|
from django.shortcuts import render
|
|
from django.http import JsonResponse
|
|
from recruitment.candidate_sync_service import CandidateSyncService
|
|
|
|
urls = super().get_urls()
|
|
|
|
custom_urls = [
|
|
path('sync-dashboard/', self.admin_view(self.sync_dashboard), name='sync_dashboard'),
|
|
path('api/sync-stats/', self.admin_view(self.sync_stats), name='sync_stats'),
|
|
]
|
|
|
|
return custom_urls + urls
|
|
|
|
def sync_dashboard(self, request):
|
|
"""Custom sync dashboard view"""
|
|
from django_q.models import Task
|
|
from django.db.models import Count, Q
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
# Get sync statistics
|
|
now = timezone.now()
|
|
last_24h = now - timedelta(hours=24)
|
|
last_7d = now - timedelta(days=7)
|
|
|
|
# Task counts
|
|
total_tasks = Task.objects.filter(func__contains='sync_hired_candidates').count()
|
|
successful_tasks = Task.objects.filter(
|
|
func__contains='sync_hired_candidates',
|
|
success=True
|
|
).count()
|
|
failed_tasks = Task.objects.filter(
|
|
func__contains='sync_hired_candidates',
|
|
success=False,
|
|
stopped__isnull=False
|
|
).count()
|
|
pending_tasks = Task.objects.filter(
|
|
func__contains='sync_hired_candidates',
|
|
success=False,
|
|
stopped__isnull=True
|
|
).count()
|
|
|
|
# Recent activity
|
|
recent_tasks = Task.objects.filter(
|
|
func__contains='sync_hired_candidates'
|
|
).order_by('-started')[:10]
|
|
|
|
# Success rate over time
|
|
last_24h_tasks = Task.objects.filter(
|
|
func__contains='sync_hired_candidates',
|
|
started__gte=last_24h
|
|
)
|
|
last_24h_success = last_24h_tasks.filter(success=True).count()
|
|
|
|
last_7d_tasks = Task.objects.filter(
|
|
func__contains='sync_hired_candidates',
|
|
started__gte=last_7d
|
|
)
|
|
last_7d_success = last_7d_tasks.filter(success=True).count()
|
|
|
|
context = {
|
|
**self.each_context(request),
|
|
'title': 'Sync Dashboard',
|
|
'total_tasks': total_tasks,
|
|
'successful_tasks': successful_tasks,
|
|
'failed_tasks': failed_tasks,
|
|
'pending_tasks': pending_tasks,
|
|
'success_rate': (successful_tasks / total_tasks * 100) if total_tasks > 0 else 0,
|
|
'last_24h_success_rate': (last_24h_success / last_24h_tasks.count() * 100) if last_24h_tasks.count() > 0 else 0,
|
|
'last_7d_success_rate': (last_7d_success / last_7d_tasks.count() * 100) if last_7d_tasks.count() > 0 else 0,
|
|
'recent_tasks': recent_tasks,
|
|
}
|
|
|
|
return render(request, 'admin/sync_dashboard.html', context)
|
|
|
|
def sync_stats(self, request):
|
|
"""API endpoint for sync statistics"""
|
|
from django_q.models import Task
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
now = timezone.now()
|
|
last_24h = now - timedelta(hours=24)
|
|
|
|
stats = {
|
|
'total_tasks': Task.objects.filter(func__contains='sync_hired_candidates').count(),
|
|
'successful_24h': Task.objects.filter(
|
|
func__contains='sync_hired_candidates',
|
|
success=True,
|
|
started__gte=last_24h
|
|
).count(),
|
|
'failed_24h': Task.objects.filter(
|
|
func__contains='sync_hired_candidates',
|
|
success=False,
|
|
stopped__gte=last_24h
|
|
).count(),
|
|
'pending_tasks': Task.objects.filter(
|
|
func__contains='sync_hired_candidates',
|
|
success=False,
|
|
stopped__isnull=True
|
|
).count(),
|
|
}
|
|
|
|
return JsonResponse(stats)
|
|
|
|
|
|
# Create custom admin site
|
|
sync_admin_site = SyncAdminSite(name='sync_admin')
|
|
|
|
# Register models with custom admin site
|
|
sync_admin_site.register(Task, SyncTaskAdmin)
|
|
sync_admin_site.register(Schedule, SyncScheduleAdmin)
|
|
|
|
# Also register with default admin site for access
|
|
admin.site.register(Task, SyncTaskAdmin)
|
|
admin.site.register(Schedule, SyncScheduleAdmin)
|