feat(parsers): add proverki.gov.ru parser with sync_inspections task
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 1m28s
CI/CD Pipeline / Build Docker Images (push) Has been cancelled
CI/CD Pipeline / Push to Gitea Registry (push) Has been cancelled
CI/CD Pipeline / Run Tests (push) Has been cancelled

- Add InspectionRecord model with is_federal_law_248, data_year, data_month fields
- Add ProverkiClient with Playwright support for JS-rendered portal
- Add streaming XML parser for large files (>50MB)
- Add sync_inspections task with incremental loading logic
  - Starts from 01.01.2025 if DB is empty
  - Loads both FZ-294 and FZ-248 inspections
  - Stops after 2 consecutive empty months
- Add InspectionService methods: get_last_loaded_period, has_data_for_period
- Add Minpromtorg parsers (certificates, manufacturers)
- Add Django Admin for parser models
- Update README with parsers documentation and changelog
This commit is contained in:
2026-01-21 20:16:25 +01:00
parent f121445313
commit 199d871923
45 changed files with 6810 additions and 97 deletions

152
src/apps/core/admin.py Normal file
View File

@@ -0,0 +1,152 @@
"""
Admin configuration for core app.
"""
from apps.core.models import BackgroundJob
from django.contrib import admin
from django.utils.html import format_html
@admin.register(BackgroundJob)
class BackgroundJobAdmin(admin.ModelAdmin):
"""Admin для фоновых задач."""
list_display = [
"task_name_short",
"status_badge",
"progress_bar",
"user_id",
"started_at",
"duration_display",
"created_at",
]
list_filter = ["status", "task_name", "created_at"]
search_fields = ["task_id", "task_name", "error"]
readonly_fields = [
"id",
"task_id",
"task_name",
"status",
"progress",
"progress_message",
"result",
"error",
"traceback",
"started_at",
"completed_at",
"user_id",
"meta",
"created_at",
"updated_at",
]
ordering = ["-created_at"]
list_per_page = 50
date_hierarchy = "created_at"
fieldsets = (
(
"Задача",
{"fields": ("id", "task_id", "task_name", "user_id")},
),
(
"Статус",
{"fields": ("status", "progress", "progress_message")},
),
(
"Результат",
{"fields": ("result",), "classes": ("collapse",)},
),
(
"Ошибка",
{"fields": ("error", "traceback"), "classes": ("collapse",)},
),
(
"Время",
{"fields": ("started_at", "completed_at", "created_at", "updated_at")},
),
(
"Метаданные",
{"fields": ("meta",), "classes": ("collapse",)},
),
)
def task_name_short(self, obj):
"""Сокращённое имя задачи."""
name = obj.task_name or ""
# Берём только последнюю часть пути
parts = name.split(".")
if len(parts) > 2:
return parts[-1]
return name
task_name_short.short_description = "Задача"
task_name_short.admin_order_field = "task_name"
def status_badge(self, obj):
"""Цветной бейдж статуса."""
colors = {
"pending": "#6c757d",
"started": "#007bff",
"success": "#28a745",
"failure": "#dc3545",
"revoked": "#ffc107",
"retry": "#17a2b8",
}
color = colors.get(obj.status, "#6c757d")
return format_html(
'<span style="color: white; background: {}; padding: 3px 10px; '
'border-radius: 3px;">{}</span>',
color,
obj.get_status_display(),
)
status_badge.short_description = "Статус"
status_badge.admin_order_field = "status"
def progress_bar(self, obj):
"""Прогресс-бар."""
progress = obj.progress or 0
color = "#28a745" if progress == 100 else "#007bff"
return format_html(
'<div style="width: 100px; background: #e9ecef; border-radius: 3px;">'
'<div style="width: {}px; background: {}; height: 18px; border-radius: 3px; '
'text-align: center; color: white; font-size: 11px; line-height: 18px;">'
"{}%</div></div>",
progress,
color,
progress,
)
progress_bar.short_description = "Прогресс"
def duration_display(self, obj):
"""Длительность выполнения."""
duration = obj.duration
if duration is None:
return "-"
if duration < 60:
return f"{duration:.1f} сек"
return f"{duration / 60:.1f} мин"
duration_display.short_description = "Длительность"
def has_add_permission(self, request):
"""Запретить создание записей вручную."""
return False
def has_change_permission(self, request, obj=None):
"""Запретить редактирование записей."""
return False
actions = ["revoke_jobs"]
@admin.action(description="Отменить выбранные задачи")
def revoke_jobs(self, request, queryset):
from celery import current_app
count = 0
for job in queryset.filter(status__in=["pending", "started"]):
current_app.control.revoke(job.task_id, terminate=True)
job.revoke()
count += 1
self.message_user(request, f"Отменено {count} задач")