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

181
src/apps/user/admin.py Normal file
View File

@@ -0,0 +1,181 @@
"""
Admin configuration for user app.
"""
from apps.user.models import Profile, User
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
class ProfileInline(admin.StackedInline):
"""Inline для профиля пользователя."""
model = Profile
can_delete = False
verbose_name_plural = "Профиль"
fk_name = "user"
fields = ["first_name", "last_name", "bio", "avatar", "date_of_birth"]
@admin.register(User)
class UserAdmin(BaseUserAdmin):
"""Admin для пользователей."""
inlines = [ProfileInline]
list_display = [
"username",
"email",
"phone",
"is_verified_badge",
"is_active_badge",
"is_staff",
"created_at",
]
list_filter = ["is_staff", "is_superuser", "is_active", "is_verified", "created_at"]
search_fields = ["username", "email", "phone"]
ordering = ["-created_at"]
list_per_page = 50
date_hierarchy = "created_at"
fieldsets = (
(None, {"fields": ("username", "password")}),
(
_("Personal info"),
{"fields": ("email", "phone")},
),
(
_("Permissions"),
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"is_verified",
"groups",
"user_permissions",
),
"classes": ("collapse",),
},
),
(
_("Important dates"),
{"fields": ("last_login", "date_joined", "created_at", "updated_at")},
),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"username",
"email",
"password1",
"password2",
"is_staff",
"is_active",
),
},
),
)
readonly_fields = ["created_at", "updated_at", "last_login", "date_joined"]
def is_verified_badge(self, obj):
"""Бейдж верификации."""
if obj.is_verified:
return format_html(
'<span style="color: white; background: #28a745; padding: 3px 10px; '
'border-radius: 3px;">✓</span>'
)
return format_html(
'<span style="color: white; background: #dc3545; padding: 3px 10px; '
'border-radius: 3px;">✗</span>'
)
is_verified_badge.short_description = "Верифицирован"
is_verified_badge.admin_order_field = "is_verified"
def is_active_badge(self, obj):
"""Бейдж активности."""
if obj.is_active:
return format_html(
'<span style="color: white; background: #28a745; padding: 3px 10px; '
'border-radius: 3px;">Активен</span>'
)
return format_html(
'<span style="color: white; background: #dc3545; padding: 3px 10px; '
'border-radius: 3px;">Неактивен</span>'
)
is_active_badge.short_description = "Статус"
is_active_badge.admin_order_field = "is_active"
actions = ["verify_users", "unverify_users", "activate_users", "deactivate_users"]
@admin.action(description="Верифицировать выбранных пользователей")
def verify_users(self, request, queryset):
updated = queryset.update(is_verified=True)
self.message_user(request, f"Верифицировано {updated} пользователей")
@admin.action(description="Снять верификацию")
def unverify_users(self, request, queryset):
updated = queryset.update(is_verified=False)
self.message_user(request, f"Снята верификация у {updated} пользователей")
@admin.action(description="Активировать пользователей")
def activate_users(self, request, queryset):
updated = queryset.update(is_active=True)
self.message_user(request, f"Активировано {updated} пользователей")
@admin.action(description="Деактивировать пользователей")
def deactivate_users(self, request, queryset):
updated = queryset.update(is_active=False)
self.message_user(request, f"Деактивировано {updated} пользователей")
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
"""Admin для профилей."""
list_display = [
"user",
"full_name",
"date_of_birth",
"has_avatar",
"created_at",
]
list_filter = ["created_at"]
search_fields = ["user__username", "user__email", "first_name", "last_name"]
readonly_fields = ["created_at", "updated_at"]
ordering = ["-created_at"]
list_per_page = 50
raw_id_fields = ["user"]
fieldsets = (
("Пользователь", {"fields": ("user",)}),
(
"Личная информация",
{"fields": ("first_name", "last_name", "bio", "date_of_birth")},
),
("Аватар", {"fields": ("avatar",)}),
("Даты", {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}),
)
def has_avatar(self, obj):
"""Есть ли аватар."""
if obj.avatar:
return format_html(
'<span style="color: white; background: #28a745; padding: 3px 10px; '
'border-radius: 3px;">Да</span>'
)
return format_html(
'<span style="color: white; background: #6c757d; padding: 3px 10px; '
'border-radius: 3px;">Нет</span>'
)
has_avatar.short_description = "Аватар"

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.2.25 on 2026-01-21 17:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('user', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='first_name',
),
migrations.RemoveField(
model_name='user',
name='last_name',
),
migrations.AlterField(
model_name='user',
name='groups',
field=models.ManyToManyField(blank=True, help_text='', related_name='custom_user_set', related_query_name='custom_user', to='auth.Group', verbose_name='groups'),
),
]

View File

@@ -6,6 +6,10 @@ from django.utils.translation import gettext_lazy as _
class User(AbstractUser):
"""Расширенная модель пользователя"""
# Убираем first_name и last_name из модели User (они в Profile)
first_name = None
last_name = None
# Переопределяем группы и разрешения для избежания конфликта
groups = models.ManyToManyField(
"auth.Group",