from django.contrib import admin from django.utils.html import format_html from import_export.admin import ImportExportMixin, ImportExportModelAdmin from simple_history.admin import SimpleHistoryAdmin from .models import CloudKitConfiguration, CloudKitSyncState, CloudKitSyncJob from .resources import CloudKitConfigurationResource, CloudKitSyncStateResource, CloudKitSyncJobResource @admin.register(CloudKitConfiguration) class CloudKitConfigurationAdmin(ImportExportMixin, SimpleHistoryAdmin): resource_class = CloudKitConfigurationResource list_display = [ 'name', 'environment', 'container_id', 'is_active_badge', 'auto_sync_after_scrape', 'batch_size', ] list_filter = ['environment', 'is_active'] search_fields = ['name', 'container_id'] readonly_fields = ['created_at', 'updated_at'] fieldsets = [ (None, { 'fields': ['name', 'environment', 'is_active'] }), ('CloudKit Credentials', { 'fields': ['container_id', 'key_id', 'private_key', 'private_key_path'], 'description': 'Enter your private key content directly OR provide a file path' }), ('Sync Settings', { 'fields': ['batch_size', 'auto_sync_after_scrape'] }), ('Metadata', { 'fields': ['created_at', 'updated_at'], 'classes': ['collapse'] }), ] actions = ['run_sync', 'test_connection'] def is_active_badge(self, obj): if obj.is_active: return format_html( '● ACTIVE' ) return format_html('○ Inactive') is_active_badge.short_description = 'Status' @admin.action(description='Run sync with selected configuration') def run_sync(self, request, queryset): from cloudkit.tasks import run_cloudkit_sync for config in queryset: run_cloudkit_sync.delay(config.id) self.message_user(request, f'Started {queryset.count()} sync jobs.') @admin.action(description='Test CloudKit connection') def test_connection(self, request, queryset): from django.contrib import messages for config in queryset: try: client = config.get_client() if client.test_connection(): self.message_user( request, f'✓ {config.name}: Connection successful!', messages.SUCCESS ) else: self.message_user( request, f'✗ {config.name}: Connection failed', messages.ERROR ) except Exception as e: self.message_user( request, f'✗ {config.name}: {str(e)}', messages.ERROR ) @admin.register(CloudKitSyncState) class CloudKitSyncStateAdmin(ImportExportModelAdmin): resource_class = CloudKitSyncStateResource list_display = [ 'record_id', 'record_type', 'sync_status_badge', 'last_synced', 'retry_count', ] list_filter = ['sync_status', 'record_type'] search_fields = ['record_id', 'cloudkit_record_name'] ordering = ['-updated_at'] readonly_fields = [ 'record_type', 'record_id', 'cloudkit_record_name', 'local_hash', 'remote_change_tag', 'last_synced', 'last_error', 'retry_count', 'created_at', 'updated_at', ] actions = ['mark_pending', 'retry_failed'] def has_add_permission(self, request): return False def sync_status_badge(self, obj): colors = { 'pending': '#f0ad4e', 'synced': '#5cb85c', 'failed': '#d9534f', 'deleted': '#999', } color = colors.get(obj.sync_status, '#999') return format_html( '{}', color, obj.sync_status.upper() ) sync_status_badge.short_description = 'Status' @admin.action(description='Mark selected as pending sync') def mark_pending(self, request, queryset): updated = queryset.update(sync_status='pending') self.message_user(request, f'{updated} records marked as pending.') @admin.action(description='Retry failed syncs') def retry_failed(self, request, queryset): updated = queryset.filter(sync_status='failed').update( sync_status='pending', retry_count=0 ) self.message_user(request, f'{updated} failed records queued for retry.') @admin.register(CloudKitSyncJob) class CloudKitSyncJobAdmin(ImportExportModelAdmin): resource_class = CloudKitSyncJobResource list_display = [ 'id', 'configuration', 'status_badge', 'triggered_by', 'started_at', 'duration_display', 'records_summary', ] list_filter = ['status', 'configuration', 'triggered_by'] date_hierarchy = 'created_at' ordering = ['-created_at'] readonly_fields = [ 'configuration', 'status', 'triggered_by', 'started_at', 'finished_at', 'duration_display', 'records_synced', 'records_created', 'records_updated', 'records_deleted', 'records_failed', 'sport_filter', 'record_type_filter', 'error_message', 'celery_task_id', 'created_at', 'updated_at', ] def has_add_permission(self, request): return False def has_change_permission(self, request, obj=None): return False def status_badge(self, obj): colors = { 'pending': '#999', 'running': '#f0ad4e', 'completed': '#5cb85c', 'failed': '#d9534f', 'cancelled': '#777', } color = colors.get(obj.status, '#999') return format_html( '{}', color, obj.status.upper() ) status_badge.short_description = 'Status' def records_summary(self, obj): if obj.records_synced == 0 and obj.status != 'completed': return '-' return format_html( '' '{} synced ({} new)', obj.records_created, obj.records_updated, obj.records_deleted, obj.records_failed, obj.records_synced, obj.records_created ) records_summary.short_description = 'Records'