Files
api-copp/route/organizations_crud.py
2026-04-09 16:43:59 +05:00

209 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}