feature/registers-generate-test-data-command #13
@@ -2,6 +2,13 @@
|
|||||||
Admin configuration for parsers app.
|
Admin configuration for parsers app.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from apps.core.models import BackgroundJob
|
||||||
|
from apps.core.services import BackgroundJobService
|
||||||
from apps.parsers.models import (
|
from apps.parsers.models import (
|
||||||
FinancialReport,
|
FinancialReport,
|
||||||
FinancialReportLine,
|
FinancialReportLine,
|
||||||
@@ -13,7 +20,14 @@ from apps.parsers.models import (
|
|||||||
ProcurementRecord,
|
ProcurementRecord,
|
||||||
Proxy,
|
Proxy,
|
||||||
)
|
)
|
||||||
from django.contrib import admin
|
from apps.parsers.serializers import FNSFileUploadSerializer
|
||||||
|
from apps.parsers.services import FNSReportService
|
||||||
|
from apps.parsers.tasks import process_fns_file
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import admin, messages
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.urls import path, reverse
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
|
||||||
@@ -629,9 +643,11 @@ class FinancialReportLineInline(admin.TabularInline):
|
|||||||
class FinancialReportAdmin(admin.ModelAdmin):
|
class FinancialReportAdmin(admin.ModelAdmin):
|
||||||
"""Admin для финансовых отчетов ФНС."""
|
"""Admin для финансовых отчетов ФНС."""
|
||||||
|
|
||||||
|
change_list_template = "admin/parsers/financialreport/change_list.html"
|
||||||
list_display = [
|
list_display = [
|
||||||
"external_id",
|
"external_id",
|
||||||
"ogrn",
|
"ogrn",
|
||||||
|
"registry_organization",
|
||||||
"file_name",
|
"file_name",
|
||||||
"status_badge",
|
"status_badge",
|
||||||
"source",
|
"source",
|
||||||
@@ -639,11 +655,20 @@ class FinancialReportAdmin(admin.ModelAdmin):
|
|||||||
"load_batch",
|
"load_batch",
|
||||||
"created_at",
|
"created_at",
|
||||||
]
|
]
|
||||||
list_filter = ["status", "source", "load_batch", "created_at"]
|
list_filter = ["status", "source", "load_batch", "registry_organization", "created_at"]
|
||||||
search_fields = ["external_id", "ogrn", "file_name"]
|
search_fields = [
|
||||||
|
"external_id",
|
||||||
|
"ogrn",
|
||||||
|
"file_name",
|
||||||
|
"registry_organization__pn_name",
|
||||||
|
"registry_organization__mn_ogrn",
|
||||||
|
"registry_organization__mn_inn",
|
||||||
|
]
|
||||||
|
list_select_related = ["registry_organization"]
|
||||||
readonly_fields = [
|
readonly_fields = [
|
||||||
"external_id",
|
"external_id",
|
||||||
"ogrn",
|
"ogrn",
|
||||||
|
"registry_organization",
|
||||||
"file_name",
|
"file_name",
|
||||||
"file_hash",
|
"file_hash",
|
||||||
"load_batch",
|
"load_batch",
|
||||||
@@ -661,7 +686,15 @@ class FinancialReportAdmin(admin.ModelAdmin):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(
|
||||||
"Основное",
|
"Основное",
|
||||||
{"fields": ("external_id", "ogrn", "file_name", "file_hash")},
|
{
|
||||||
|
"fields": (
|
||||||
|
"external_id",
|
||||||
|
"ogrn",
|
||||||
|
"registry_organization",
|
||||||
|
"file_name",
|
||||||
|
"file_hash",
|
||||||
|
)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"Статус",
|
"Статус",
|
||||||
@@ -701,6 +734,166 @@ class FinancialReportAdmin(admin.ModelAdmin):
|
|||||||
status_badge.short_description = "Статус"
|
status_badge.short_description = "Статус"
|
||||||
status_badge.admin_order_field = "status"
|
status_badge.admin_order_field = "status"
|
||||||
|
|
||||||
|
def get_urls(self):
|
||||||
|
urls = super().get_urls()
|
||||||
|
custom_urls = [
|
||||||
|
path(
|
||||||
|
"upload-excel/",
|
||||||
|
self.admin_site.admin_view(self.upload_excel_view),
|
||||||
|
name="parsers_financialreport_upload_excel",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
extra_context = extra_context or {}
|
||||||
|
extra_context["upload_excel_url"] = reverse(
|
||||||
|
"admin:parsers_financialreport_upload_excel"
|
||||||
|
)
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
def upload_excel_view(self, request):
|
||||||
|
changelist_url = reverse("admin:parsers_financialreport_changelist")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
files = request.FILES.getlist("files")
|
||||||
|
serializer = FNSFileUploadSerializer(data={"files": files})
|
||||||
|
|
||||||
|
if not serializer.is_valid():
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Ошибка валидации файлов: {serializer.errors}",
|
||||||
|
level=messages.ERROR,
|
||||||
|
)
|
||||||
|
return redirect(changelist_url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
queued, skipped, task_ids = self._enqueue_fns_files(
|
||||||
|
request,
|
||||||
|
serializer.validated_data["files"],
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Ошибка постановки файлов в очередь: {exc}",
|
||||||
|
level=messages.ERROR,
|
||||||
|
)
|
||||||
|
return redirect(changelist_url)
|
||||||
|
|
||||||
|
if queued:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Файлов поставлено в очередь: {queued}. Task IDs: {', '.join(task_ids[:5])}",
|
||||||
|
level=messages.SUCCESS,
|
||||||
|
)
|
||||||
|
if skipped:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Пропущено файлов: {skipped} (дубликаты или уже обрабатываются).",
|
||||||
|
level=messages.WARNING,
|
||||||
|
)
|
||||||
|
if not queued and not skipped:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Файлы не были обработаны.",
|
||||||
|
level=messages.WARNING,
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect(changelist_url)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
**self.admin_site.each_context(request),
|
||||||
|
"opts": self.model._meta,
|
||||||
|
"title": "Загрузка Excel отчетности ФНС",
|
||||||
|
"changelist_url": changelist_url,
|
||||||
|
}
|
||||||
|
return TemplateResponse(
|
||||||
|
request,
|
||||||
|
"admin/parsers/financialreport/upload_excel.html",
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _try_create_fns_lock(file_path: Path) -> bool:
|
||||||
|
lock_path = Path(f"{file_path}.lock")
|
||||||
|
if lock_path.exists():
|
||||||
|
try:
|
||||||
|
age_seconds = time.time() - lock_path.stat().st_mtime
|
||||||
|
ttl_seconds = getattr(settings, "FNS_LOCK_TTL_SECONDS", 3600)
|
||||||
|
if age_seconds > ttl_seconds:
|
||||||
|
lock_path.unlink()
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
lock_path.touch(exist_ok=False)
|
||||||
|
except FileExistsError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _enqueue_fns_files(self, request, files):
|
||||||
|
upload_dir = Path(settings.FNS_WATCH_DIRECTORY)
|
||||||
|
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
task_ids = []
|
||||||
|
queued = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
file_content = file.read()
|
||||||
|
file_hash = hashlib.sha256(file_content).hexdigest()
|
||||||
|
file.seek(0)
|
||||||
|
|
||||||
|
if FNSReportService.exists_by_hash(file_hash):
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_path = upload_dir / file.name
|
||||||
|
if not self._try_create_fns_lock(file_path):
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
lock_path = Path(f"{file_path}.lock")
|
||||||
|
if file_path.exists():
|
||||||
|
lock_path.unlink(missing_ok=True)
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
for chunk in file.chunks():
|
||||||
|
f.write(chunk)
|
||||||
|
except Exception:
|
||||||
|
lock_path.unlink(missing_ok=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
try:
|
||||||
|
BackgroundJobService.create_job(
|
||||||
|
task_id=task_id,
|
||||||
|
task_name="apps.parsers.tasks.process_fns_file",
|
||||||
|
user_id=request.user.id,
|
||||||
|
meta={
|
||||||
|
"source": "fns_reports",
|
||||||
|
"file": file.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
task = process_fns_file.apply_async(
|
||||||
|
args=[str(file_path)],
|
||||||
|
kwargs={"requested_by_id": request.user.id},
|
||||||
|
task_id=task_id,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
lock_path.unlink(missing_ok=True)
|
||||||
|
BackgroundJob.objects.filter(task_id=task_id).delete()
|
||||||
|
raise
|
||||||
|
|
||||||
|
task_ids.append(task.id)
|
||||||
|
queued += 1
|
||||||
|
|
||||||
|
return queued, skipped, task_ids
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
"""Запретить создание записей вручную."""
|
"""Запретить создание записей вручную."""
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ from apps.registers.models import (
|
|||||||
RegisterUpload,
|
RegisterUpload,
|
||||||
RegistryMembershipPeriod,
|
RegistryMembershipPeriod,
|
||||||
)
|
)
|
||||||
from django.contrib import admin
|
from apps.registers.serializers import RegisterFileUploadSerializer
|
||||||
|
from apps.registers.services import RegisterImportError, RegisterImportService
|
||||||
|
from django.contrib import admin, messages
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.urls import path, reverse
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Register)
|
@admin.register(Register)
|
||||||
@@ -53,6 +58,7 @@ class OrganizationAdmin(admin.ModelAdmin):
|
|||||||
class RegisterUploadAdmin(admin.ModelAdmin):
|
class RegisterUploadAdmin(admin.ModelAdmin):
|
||||||
"""Admin для загрузок реестров."""
|
"""Admin для загрузок реестров."""
|
||||||
|
|
||||||
|
change_list_template = "admin/registers/registerupload/change_list.html"
|
||||||
list_display = [
|
list_display = [
|
||||||
"id",
|
"id",
|
||||||
"registry",
|
"registry",
|
||||||
@@ -67,6 +73,82 @@ class RegisterUploadAdmin(admin.ModelAdmin):
|
|||||||
readonly_fields = ["created_at", "updated_at", "file_hash"]
|
readonly_fields = ["created_at", "updated_at", "file_hash"]
|
||||||
ordering = ["-actual_date", "-created_at"]
|
ordering = ["-actual_date", "-created_at"]
|
||||||
|
|
||||||
|
def get_urls(self):
|
||||||
|
urls = super().get_urls()
|
||||||
|
custom_urls = [
|
||||||
|
path(
|
||||||
|
"upload-excel/",
|
||||||
|
self.admin_site.admin_view(self.upload_excel_view),
|
||||||
|
name="registers_registerupload_upload_excel",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
extra_context = extra_context or {}
|
||||||
|
extra_context["upload_excel_url"] = reverse(
|
||||||
|
"admin:registers_registerupload_upload_excel"
|
||||||
|
)
|
||||||
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
def upload_excel_view(self, request):
|
||||||
|
changelist_url = reverse("admin:registers_registerupload_changelist")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
data = request.POST.copy()
|
||||||
|
uploaded_file = request.FILES.get("file")
|
||||||
|
if uploaded_file is not None:
|
||||||
|
data["file"] = uploaded_file
|
||||||
|
|
||||||
|
serializer = RegisterFileUploadSerializer(data=data)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Ошибка валидации загрузки: {serializer.errors}",
|
||||||
|
level=messages.ERROR,
|
||||||
|
)
|
||||||
|
return redirect(changelist_url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = RegisterImportService.sync_registry_memberships(
|
||||||
|
registry=serializer.validated_data["registry"],
|
||||||
|
uploaded_file=serializer.validated_data["file"],
|
||||||
|
actual_date=serializer.validated_data.get("actual_date"),
|
||||||
|
uploaded_by=request.user,
|
||||||
|
)
|
||||||
|
except RegisterImportError as exc:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Ошибка импорта Excel: {exc}",
|
||||||
|
level=messages.ERROR,
|
||||||
|
)
|
||||||
|
return redirect(changelist_url)
|
||||||
|
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
(
|
||||||
|
"Загрузка завершена: "
|
||||||
|
f"{result['registry_name']}, строк: {result['rows_in_file']}, "
|
||||||
|
f"новых организаций: {result['organizations_created']}, "
|
||||||
|
f"обновлено: {result['organizations_updated']}."
|
||||||
|
),
|
||||||
|
level=messages.SUCCESS,
|
||||||
|
)
|
||||||
|
return redirect(changelist_url)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
**self.admin_site.each_context(request),
|
||||||
|
"opts": self.model._meta,
|
||||||
|
"title": "Загрузка списка организаций из Excel",
|
||||||
|
"changelist_url": changelist_url,
|
||||||
|
"registries": Register.objects.only("id", "name").order_by("name"),
|
||||||
|
}
|
||||||
|
return TemplateResponse(
|
||||||
|
request,
|
||||||
|
"admin/registers/registerupload/upload_excel.html",
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(RegistryMembershipPeriod)
|
@admin.register(RegistryMembershipPeriod)
|
||||||
class RegistryMembershipPeriodAdmin(admin.ModelAdmin):
|
class RegistryMembershipPeriodAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{% extends "admin/change_list.html" %}
|
||||||
|
|
||||||
|
{% block object-tools-items %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ upload_excel_url }}" class="addlink">Загрузить Excel ФНС</a>
|
||||||
|
</li>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<a href="{% url 'admin:index' %}">Главная</a>
|
||||||
|
› <a href="{{ changelist_url }}">Финансовые отчеты ФНС</a>
|
||||||
|
› Загрузка Excel
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="content-main">
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset class="module aligned">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="id_files" class="required">Excel файлы:</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="files"
|
||||||
|
id="id_files"
|
||||||
|
accept=".xlsx"
|
||||||
|
multiple
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p class="help">Ожидаемый формат имени: fin_{id}_{ogrn}.xlsx</p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="submit-row">
|
||||||
|
<input type="submit" value="Загрузить" class="default" />
|
||||||
|
<a href="{{ changelist_url }}" class="closelink">Отмена</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{% extends "admin/change_list.html" %}
|
||||||
|
|
||||||
|
{% block object-tools-items %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ upload_excel_url }}" class="addlink">Загрузить Excel организаций</a>
|
||||||
|
</li>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<a href="{% url 'admin:index' %}">Главная</a>
|
||||||
|
› <a href="{{ changelist_url }}">Загрузки реестров</a>
|
||||||
|
› Загрузка Excel
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="content-main">
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<fieldset class="module aligned">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="id_registry" class="required">Реестр:</label>
|
||||||
|
<select name="registry" id="id_registry" required>
|
||||||
|
<option value="">---------</option>
|
||||||
|
{% for registry in registries %}
|
||||||
|
<option value="{{ registry.id }}">{{ registry.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="id_actual_date">Дата актуальности:</label>
|
||||||
|
<input type="date" name="actual_date" id="id_actual_date" />
|
||||||
|
<p class="help">Если не указана, будет использована текущая дата.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="id_file" class="required">Excel файл:</label>
|
||||||
|
<input type="file" name="file" id="id_file" accept=".xlsx" required />
|
||||||
|
<p class="help">
|
||||||
|
Ожидаемые колонки: pn_name, mn_ogrn, mn_inn, mn_okpo. Опционально: in_kpp.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="submit-row">
|
||||||
|
<input type="submit" value="Загрузить" class="default" />
|
||||||
|
<a href="{{ changelist_url }}" class="closelink">Отмена</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -244,3 +244,7 @@ class ParsersAdminTest(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(admin.lines_count(report), 1)
|
self.assertEqual(admin.lines_count(report), 1)
|
||||||
self.assertIn("span", str(admin.status_badge(report)))
|
self.assertIn("span", str(admin.status_badge(report)))
|
||||||
|
self.assertIn("registry_organization", admin.list_display)
|
||||||
|
self.assertIn("registry_organization__pn_name", admin.search_fields)
|
||||||
|
route_names = [route.name for route in admin.get_urls()]
|
||||||
|
self.assertIn("parsers_financialreport_upload_excel", route_names)
|
||||||
|
|||||||
102
tests/apps/registers/test_admin.py
Normal file
102
tests/apps/registers/test_admin.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"""Tests for registers admin configuration."""
|
||||||
|
|
||||||
|
import io
|
||||||
|
|
||||||
|
from apps.registers.admin import RegisterUploadAdmin
|
||||||
|
from apps.registers.models import Organization, RegisterUpload, RegistryMembershipPeriod
|
||||||
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
from openpyxl import Workbook
|
||||||
|
|
||||||
|
from tests.apps.registers.factories import RegisterFactory
|
||||||
|
from tests.apps.user.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
|
def _build_register_excel_upload(filename: str = "registry.xlsx") -> SimpleUploadedFile:
|
||||||
|
workbook = Workbook()
|
||||||
|
worksheet = workbook.active
|
||||||
|
worksheet.append(["pn_name", "mn_ogrn", "mn_inn", "in_kpp", "mn_okpo"])
|
||||||
|
worksheet.append(
|
||||||
|
[
|
||||||
|
'АО "Тестовая организация"',
|
||||||
|
1027600980990,
|
||||||
|
7601000086,
|
||||||
|
760401001,
|
||||||
|
"07506197",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
workbook.save(buffer)
|
||||||
|
workbook.close()
|
||||||
|
|
||||||
|
return SimpleUploadedFile(
|
||||||
|
filename,
|
||||||
|
buffer.getvalue(),
|
||||||
|
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RegistersAdminTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.site = AdminSite()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.user = UserFactory.create_superuser()
|
||||||
|
|
||||||
|
def _post_request(self, data):
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registers/registerupload/upload-excel/",
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
request.user = self.user
|
||||||
|
request.session = {}
|
||||||
|
request._messages = FallbackStorage(request)
|
||||||
|
return request
|
||||||
|
|
||||||
|
def test_register_upload_admin_has_custom_upload_route(self):
|
||||||
|
admin = RegisterUploadAdmin(RegisterUpload, self.site)
|
||||||
|
route_names = [route.name for route in admin.get_urls()]
|
||||||
|
|
||||||
|
self.assertIn("registers_registerupload_upload_excel", route_names)
|
||||||
|
|
||||||
|
def test_register_upload_admin_upload_excel_success(self):
|
||||||
|
admin = RegisterUploadAdmin(RegisterUpload, self.site)
|
||||||
|
registry = RegisterFactory()
|
||||||
|
uploaded_file = _build_register_excel_upload()
|
||||||
|
request = self._post_request(
|
||||||
|
{
|
||||||
|
"registry": str(registry.id),
|
||||||
|
"actual_date": "2026-03-20",
|
||||||
|
"file": uploaded_file,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = admin.upload_excel_view(request)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(RegisterUpload.objects.count(), 1)
|
||||||
|
self.assertEqual(Organization.objects.count(), 1)
|
||||||
|
self.assertEqual(RegistryMembershipPeriod.objects.count(), 1)
|
||||||
|
|
||||||
|
upload = RegisterUpload.objects.first()
|
||||||
|
self.assertEqual(upload.registry, registry)
|
||||||
|
self.assertEqual(upload.actual_date.isoformat(), "2026-03-20")
|
||||||
|
|
||||||
|
def test_register_upload_admin_upload_excel_invalid_extension(self):
|
||||||
|
admin = RegisterUploadAdmin(RegisterUpload, self.site)
|
||||||
|
registry = RegisterFactory()
|
||||||
|
invalid_file = SimpleUploadedFile("registry.txt", b"text/plain")
|
||||||
|
request = self._post_request(
|
||||||
|
{
|
||||||
|
"registry": str(registry.id),
|
||||||
|
"actual_date": "2026-03-20",
|
||||||
|
"file": invalid_file,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = admin.upload_excel_view(request)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(RegisterUpload.objects.count(), 0)
|
||||||
Reference in New Issue
Block a user