209 lines
7.3 KiB
Python
209 lines
7.3 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||
from pydantic import BaseModel
|
||
from typing import Optional
|
||
import uuid
|
||
import io
|
||
|
||
from bd import make_engine
|
||
|
||
from bd.tables.organization import Organization
|
||
from route.auth_utils import require_admin_key
|
||
|
||
router = APIRouter(tags=["organizations"], prefix="/organizations")
|
||
|
||
|
||
class OrganizationCreate(BaseModel):
|
||
name_organization: str
|
||
|
||
class OrganizationUpdate(BaseModel):
|
||
name_organization: Optional[str] = None
|
||
|
||
|
||
|
||
def get_session():
|
||
from sqlalchemy.orm import sessionmaker
|
||
return sessionmaker(bind=make_engine(), future=True)
|
||
|
||
|
||
def _org_dict(org: Organization) -> dict:
|
||
return {
|
||
"id": str(org.id),
|
||
"name_organization": org.name_organization,
|
||
}
|
||
|
||
|
||
@router.post("/", status_code=201, dependencies=[Depends(require_admin_key)])
|
||
def create_organization(payload: OrganizationCreate):
|
||
Session = get_session()
|
||
with Session() as session:
|
||
existing = session.query(Organization).filter(
|
||
Organization.name_organization == payload.name_organization
|
||
).first()
|
||
if existing:
|
||
raise HTTPException(status_code=400, detail="Organization name already exists")
|
||
org = Organization(name_organization=payload.name_organization)
|
||
session.add(org)
|
||
session.commit()
|
||
session.refresh(org)
|
||
return _org_dict(org)
|
||
|
||
|
||
@router.get("/")
|
||
def list_organizations():
|
||
Session = get_session()
|
||
with Session() as session:
|
||
rows = session.query(Organization).all()
|
||
return [_org_dict(o) for o in rows]
|
||
|
||
|
||
@router.get("/{org_id}")
|
||
def get_organization(org_id: str):
|
||
Session = get_session()
|
||
try:
|
||
oid = uuid.UUID(org_id)
|
||
except Exception:
|
||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||
with Session() as session:
|
||
org = session.get(Organization, oid)
|
||
if not org:
|
||
raise HTTPException(status_code=404, detail="Organization not found")
|
||
return _org_dict(org)
|
||
|
||
|
||
@router.put("/{org_id}", dependencies=[Depends(require_admin_key)])
|
||
def update_organization(org_id: str, payload: OrganizationUpdate):
|
||
Session = get_session()
|
||
try:
|
||
oid = uuid.UUID(org_id)
|
||
except Exception:
|
||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||
with Session() as session:
|
||
org = session.get(Organization, oid)
|
||
if not org:
|
||
raise HTTPException(status_code=404, detail="Organization not found")
|
||
if payload.name_organization is not None:
|
||
org.name_organization = payload.name_organization
|
||
session.add(org)
|
||
session.commit()
|
||
session.refresh(org)
|
||
return _org_dict(org)
|
||
|
||
|
||
@router.delete("/{org_id}", status_code=204, dependencies=[Depends(require_admin_key)])
|
||
def delete_organization(org_id: str):
|
||
Session = get_session()
|
||
try:
|
||
oid = uuid.UUID(org_id)
|
||
except Exception:
|
||
raise HTTPException(status_code=400, detail="Invalid UUID")
|
||
with Session() as session:
|
||
org = session.get(Organization, oid)
|
||
if not org:
|
||
raise HTTPException(status_code=404, detail="Organization not found")
|
||
session.delete(org)
|
||
session.commit()
|
||
return {}
|
||
|
||
|
||
def _extract_names_from_docx(content: bytes) -> list[str]:
|
||
"""Извлекает уникальные непустые строки из docx.
|
||
|
||
Для таблиц берётся только ПЕРВЫЙ столбец каждой строки.
|
||
Параграфы вне таблиц используются как запасной источник (если таблиц нет).
|
||
"""
|
||
from docx import Document as DocxDocument
|
||
doc = DocxDocument(io.BytesIO(content))
|
||
names: list[str] = []
|
||
seen: set[str] = set()
|
||
|
||
def _add(text: str):
|
||
t = text.strip()
|
||
if t and t not in seen:
|
||
seen.add(t)
|
||
names.append(t)
|
||
|
||
# Если есть таблицы — берём только первый столбец
|
||
if doc.tables:
|
||
for table in doc.tables:
|
||
for row in table.rows:
|
||
if row.cells:
|
||
_add(row.cells[0].text)
|
||
else:
|
||
# Нет таблиц — берём параграфы
|
||
for para in doc.paragraphs:
|
||
_add(para.text)
|
||
|
||
return names
|
||
|
||
|
||
@router.post("/import/docx/preview", dependencies=[Depends(require_admin_key)])
|
||
async def preview_import_from_docx(file: UploadFile = File(...)):
|
||
"""Предпросмотр: возвращает список строк, которые будут импортированы, БЕЗ записи в БД.
|
||
|
||
Используйте перед /import/docx чтобы убедиться, что извлекаются нужные данные.
|
||
"""
|
||
if not file.filename or not file.filename.lower().endswith(".docx"):
|
||
raise HTTPException(status_code=400, detail="Ожидается файл .docx")
|
||
try:
|
||
from docx import Document as DocxDocument # noqa: F401
|
||
except ImportError:
|
||
raise HTTPException(status_code=500, detail="python-docx не установлен на сервере")
|
||
|
||
content = await file.read()
|
||
try:
|
||
names = _extract_names_from_docx(content)
|
||
except Exception:
|
||
raise HTTPException(status_code=400, detail="Не удалось открыть файл")
|
||
|
||
return {"total": len(names), "names": names}
|
||
|
||
|
||
@router.post("/import/docx", dependencies=[Depends(require_admin_key)])
|
||
async def import_organizations_from_docx(file: UploadFile = File(...)):
|
||
"""Массовый импорт организаций из .docx файла.
|
||
|
||
Каждая непустая строка (параграф или ячейка таблицы) становится отдельной организацией.
|
||
Уже существующие названия пропускаются без ошибки.
|
||
Перед использованием проверьте результат через /import/docx/preview.
|
||
"""
|
||
if not file.filename or not file.filename.lower().endswith(".docx"):
|
||
raise HTTPException(status_code=400, detail="Ожидается файл .docx")
|
||
|
||
try:
|
||
from docx import Document as DocxDocument # noqa: F401
|
||
except ImportError:
|
||
raise HTTPException(status_code=500, detail="python-docx не установлен на сервере")
|
||
|
||
content = await file.read()
|
||
try:
|
||
names = _extract_names_from_docx(content)
|
||
except Exception:
|
||
raise HTTPException(status_code=400, detail="Не удалось открыть файл. Убедитесь, что это корректный .docx")
|
||
|
||
if not names:
|
||
raise HTTPException(status_code=400, detail="В файле не найдено ни одной строки")
|
||
|
||
Session = get_session()
|
||
added: list[str] = []
|
||
skipped: list[str] = []
|
||
|
||
with Session() as session:
|
||
for name in names:
|
||
existing = session.query(Organization).filter(
|
||
Organization.name_organization == name
|
||
).first()
|
||
if existing:
|
||
skipped.append(name)
|
||
continue
|
||
org = Organization(name_organization=name)
|
||
session.add(org)
|
||
added.append(name)
|
||
session.commit()
|
||
|
||
return {
|
||
"added": len(added),
|
||
"skipped": len(skipped),
|
||
"added_names": added,
|
||
"skipped_names": skipped,
|
||
}
|