| 55 | |
| 56 | |
| 57 | class AsyncMigrationStatusAdmin(admin.ModelAdmin): |
| 58 | def __init__(self, *args, **kwargs): |
| 59 | super(AsyncMigrationStatusAdmin, self).__init__(*args, **kwargs) |
| 60 | |
| 61 | self.list_display = ('id', 'name', 'project', 'status', 'created_at', 'updated_at', 'meta') |
| 62 | self.list_filter = ('name', 'status') |
| 63 | self.search_fields = ('name', 'project__id') |
| 64 | self.ordering = ('-id',) |
| 65 | self.actions = ['run_scheduled_migrations'] |
| 66 | |
| 67 | def _mark_migration_error(self, migration, error_message): |
| 68 | """Helper to mark migration as ERROR with error message in meta.""" |
| 69 | meta = migration.meta or {} |
| 70 | meta['error'] = error_message |
| 71 | migration.meta = meta |
| 72 | migration.status = migration.STATUS_ERROR |
| 73 | migration.save() |
| 74 | |
| 75 | def run_scheduled_migrations(self, request, queryset): |
| 76 | """Run selected scheduled migrations manually. |
| 77 | |
| 78 | Expects AsyncMigrationStatus.name in one of forms: |
| 79 | - "<app>:<migration_module>" |
| 80 | - "<app>.migrations.<migration_module>" |
| 81 | - Full dotted path "label_studio.<app>.migrations.<migration_module>" |
| 82 | |
| 83 | Does not scan all apps. If app is not provided, marks error. |
| 84 | """ |
| 85 | import importlib |
| 86 | |
| 87 | from core.migration_helpers import execute_sql_job |
| 88 | from core.redis import start_job_async_or_sync |
| 89 | |
| 90 | executed_count = 0 |
| 91 | skipped_count = 0 |
| 92 | error_count = 0 |
| 93 | |
| 94 | def resolve_import_path(name: str) -> tuple[str | None, str | None]: |
| 95 | s = (name or '').strip() |
| 96 | if not s: |
| 97 | return None, 'Empty migration import path' |
| 98 | if '.migrations.' not in s: |
| 99 | return None, 'Migration import path must include ".migrations." (use full dotted path)' |
| 100 | return s, None |
| 101 | |
| 102 | for migration in queryset: |
| 103 | if migration.status != migration.STATUS_SCHEDULED: |
| 104 | skipped_count += 1 |
| 105 | continue |
| 106 | |
| 107 | import_path, err = resolve_import_path(migration.name) |
| 108 | if err: |
| 109 | self._mark_migration_error(migration, err) |
| 110 | error_count += 1 |
| 111 | continue |
| 112 | try: |
| 113 | module = importlib.import_module(import_path) |
| 114 | except Exception as e: |