from fastapi import APIRouter, Depends, HTTPException, UploadFile, File from pydantic import BaseModel from typing import Optional import uuid import io from bd import get_session 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 _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, }