248 lines
7.7 KiB
Python
248 lines
7.7 KiB
Python
"""Write API составов проектов."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import traceback
|
||
from typing import Annotated, Any
|
||
|
||
import pymysql
|
||
from fastapi import APIRouter, Depends, HTTPException
|
||
from pydantic import BaseModel, Field
|
||
|
||
from app.labor import _column_lookup, _prefixed_col, _resolve_table
|
||
from app.labor_identity import ensure_emp_exists, parse_acting_emp_id
|
||
from app.labor_permissions import can_write_project_member
|
||
from app.main import _db_name, _quote_ident, _table_columns, get_conn, require_api_key
|
||
from app.member_roles import is_valid_member_role
|
||
from app.merakomis_schema import TEAM_MEMBER_TABLE
|
||
from app.project_members_helpers import (
|
||
emp_is_active_for_team,
|
||
fetch_member_item,
|
||
fetch_project_row,
|
||
fetch_team_member_row,
|
||
project_section_ids,
|
||
)
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
class ProjectMemberWrite(BaseModel):
|
||
project_id: int = Field(..., ge=1)
|
||
emp_id: int = Field(..., ge=1)
|
||
section_id: int = Field(..., ge=1)
|
||
role: int = Field(..., ge=1)
|
||
active: bool = True
|
||
text: str = ""
|
||
|
||
|
||
def _http_500(e: Exception) -> HTTPException:
|
||
dbg = os.environ.get("DEBUG", "")
|
||
msg = str(e)
|
||
if dbg == "1":
|
||
msg = f"{msg}\n{traceback.format_exc()}"
|
||
return HTTPException(status_code=500, detail=msg)
|
||
|
||
|
||
def _member_columns(cur, db: str) -> dict[str, str]:
|
||
table = _resolve_table(cur, db, TEAM_MEMBER_TABLE)
|
||
cols = _table_columns(cur, db, table)
|
||
lut = _column_lookup(cols)
|
||
mapping: dict[str, str] = {"table": table}
|
||
for f in ("id", "emp", "team", "section", "role", "active", "text", "portal"):
|
||
c = _prefixed_col(lut, TEAM_MEMBER_TABLE, f)
|
||
if c:
|
||
mapping[f] = c
|
||
return mapping
|
||
|
||
|
||
def validate_member_write(
|
||
cur,
|
||
db: str,
|
||
body: ProjectMemberWrite,
|
||
) -> tuple[dict[str, Any], list[int]]:
|
||
"""Валидация тела PUT. Возвращает project row и allowed section ids."""
|
||
if not is_valid_member_role(body.role):
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"code": "invalid_role", "message": "Укажите роль в проекте"},
|
||
)
|
||
|
||
project = fetch_project_row(cur, db, body.project_id)
|
||
if not project:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"code": "project_not_found", "message": "Проект не найден"},
|
||
)
|
||
|
||
try:
|
||
ensure_emp_exists(cur, db, body.emp_id, kind="target")
|
||
except HTTPException as e:
|
||
if e.status_code == 404:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail={"code": "emp_not_found", "message": f"Сотрудник {body.emp_id} не найден"},
|
||
) from e
|
||
raise
|
||
|
||
if not emp_is_active_for_team(cur, db, body.emp_id):
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={
|
||
"code": "invalid_employee",
|
||
"message": "Нельзя добавить архивного или удалённого сотрудника",
|
||
},
|
||
)
|
||
|
||
allowed = project_section_ids(cur, db, body.project_id)
|
||
if not allowed:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={
|
||
"code": "no_project_sections",
|
||
"message": "У проекта не настроены разделы",
|
||
},
|
||
)
|
||
if body.section_id not in allowed:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={
|
||
"code": "invalid_section",
|
||
"message": "Раздел не входит в состав проекта",
|
||
},
|
||
)
|
||
|
||
team_id = int(project.get("team") or 0)
|
||
if not team_id:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail={"code": "no_team", "message": "У проекта не задана команда"},
|
||
)
|
||
|
||
return project, allowed
|
||
|
||
|
||
def upsert_team_member(
|
||
cur,
|
||
db: str,
|
||
*,
|
||
team_id: int,
|
||
emp_id: int,
|
||
section_id: int,
|
||
role: int,
|
||
active: bool,
|
||
text: str,
|
||
) -> int:
|
||
"""INSERT или UPDATE; возвращает member id."""
|
||
cols = _member_columns(cur, db)
|
||
table = cols["table"]
|
||
tq = _quote_ident(table)
|
||
|
||
existing = fetch_team_member_row(cur, db, team_id, emp_id)
|
||
active_val = 1 if active else 0
|
||
|
||
if existing:
|
||
member_id = int(existing["id"])
|
||
sets = []
|
||
params: list[Any] = []
|
||
for field, value in (
|
||
("section", section_id),
|
||
("role", role),
|
||
("active", active_val),
|
||
("text", text),
|
||
):
|
||
if field in cols:
|
||
sets.append(f"{_quote_ident(cols[field])} = %s")
|
||
params.append(value)
|
||
if sets:
|
||
params.append(member_id)
|
||
cur.execute(
|
||
f"UPDATE {tq} SET {', '.join(sets)} WHERE {_quote_ident(cols['id'])} = %s",
|
||
params,
|
||
)
|
||
return member_id
|
||
|
||
insert_cols: list[str] = []
|
||
insert_vals: list[Any] = []
|
||
for field, value in (
|
||
("emp", emp_id),
|
||
("team", team_id),
|
||
("section", section_id),
|
||
("role", role),
|
||
("active", active_val),
|
||
("text", text),
|
||
("portal", 0),
|
||
):
|
||
if field in cols:
|
||
insert_cols.append(_quote_ident(cols[field]))
|
||
insert_vals.append(value)
|
||
|
||
placeholders = ", ".join(["%s"] * len(insert_vals))
|
||
cur.execute(
|
||
f"INSERT INTO {tq} ({', '.join(insert_cols)}) VALUES ({placeholders})",
|
||
insert_vals,
|
||
)
|
||
return int(cur.lastrowid)
|
||
|
||
|
||
@router.put("/api/project-members")
|
||
def put_project_member(
|
||
body: ProjectMemberWrite,
|
||
_auth: Annotated[None, Depends(require_api_key)],
|
||
acting_emp_id: Annotated[int, Depends(parse_acting_emp_id)],
|
||
) -> dict[str, Any]:
|
||
try:
|
||
with get_conn() as c:
|
||
with c.cursor() as cur:
|
||
db = _db_name(cur)
|
||
ensure_emp_exists(cur, db, acting_emp_id, kind="acting")
|
||
|
||
project, _allowed = validate_member_write(cur, db, body)
|
||
team_id = int(project["team"])
|
||
|
||
existing = fetch_team_member_row(cur, db, team_id, body.emp_id)
|
||
member_id = int(existing["id"]) if existing else None
|
||
|
||
if not can_write_project_member(
|
||
cur,
|
||
db,
|
||
acting_emp_id,
|
||
body.project_id,
|
||
member_id=member_id,
|
||
target_emp_id=body.emp_id,
|
||
):
|
||
raise HTTPException(
|
||
status_code=403,
|
||
detail={
|
||
"code": "forbidden",
|
||
"message": "Нет прав на изменение состава проекта",
|
||
},
|
||
)
|
||
|
||
upsert_team_member(
|
||
cur,
|
||
db,
|
||
team_id=team_id,
|
||
emp_id=body.emp_id,
|
||
section_id=body.section_id,
|
||
role=body.role,
|
||
active=body.active,
|
||
text=body.text or "",
|
||
)
|
||
c.commit()
|
||
|
||
item = fetch_member_item(cur, db, body.project_id, body.emp_id)
|
||
if not item:
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail={"code": "db_error", "message": "Запись не найдена после сохранения"},
|
||
)
|
||
|
||
return {"ok": True, **item}
|
||
except HTTPException:
|
||
raise
|
||
except pymysql.Error as e:
|
||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||
except Exception as e:
|
||
raise _http_500(e) from e
|