Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""Обратная связь по порталу: отправить может любой сотрудник, читать и отвечать — только администраторы портала."""
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import config, push, schemas as S
|
||||
from ..db import get_db
|
||||
from ..models import Feedback, User
|
||||
from ..security import current_user, is_portal_admin, require_portal_admin
|
||||
from .catalog import commit, get_or_404
|
||||
from .inventory import user_names
|
||||
|
||||
router = APIRouter(tags=["feedback"])
|
||||
PER_HOUR = 20 # не больше сообщений в час от одного сотрудника
|
||||
KIND_RU = {"idea": "Идея", "bug": "Ошибка", "other": "Другое"}
|
||||
|
||||
|
||||
def fb_out(f: Feedback, names: dict) -> S.FeedbackOut:
|
||||
o = S.FeedbackOut.model_validate(f)
|
||||
o.user_name, o.answered_by_name = names.get(f.user_id), names.get(f.answered_by_id)
|
||||
return o
|
||||
|
||||
|
||||
@router.post("/feedback", status_code=201)
|
||||
def send(data: S.FeedbackIn, db: Session = Depends(get_db), u: User = Depends(current_user)):
|
||||
since = config.now() - timedelta(hours=1)
|
||||
if db.scalar(select(func.count()).select_from(Feedback).where(Feedback.user_id == u.id, Feedback.created_at >= since)) >= PER_HOUR:
|
||||
raise HTTPException(429, "Слишком много сообщений, попробуйте позже")
|
||||
f = Feedback(user_id=u.id, kind=data.kind, text=data.text.strip(), page=data.page.strip()[:200], client=data.client)
|
||||
db.add(f)
|
||||
commit(db)
|
||||
admins = [x.id for x in db.scalars(select(User).where(User.is_active.is_(True), User.is_admin.is_(True))) if is_portal_admin(x) and x.id != u.id]
|
||||
push.notify(admins, f"Обратная связь: {KIND_RU.get(f.kind, f.kind)}", f"{u.full_name or u.username}: {f.text[:100]}", "/#/feedback")
|
||||
return {"ok": True, "id": f.id}
|
||||
|
||||
|
||||
@router.get("/feedback", response_model=list[S.FeedbackOut])
|
||||
def list_(status: Optional[str] = Query(None, pattern="^(new|taken|done|rejected|open)$"), limit: int = Query(300, ge=1, le=2000),
|
||||
db: Session = Depends(get_db), _: User = Depends(require_portal_admin)):
|
||||
stmt = select(Feedback).order_by(Feedback.created_at.desc(), Feedback.id.desc()).limit(limit)
|
||||
if status == "open":
|
||||
stmt = stmt.where(Feedback.status.in_(("new", "taken")))
|
||||
elif status:
|
||||
stmt = stmt.where(Feedback.status == status)
|
||||
names = user_names(db)
|
||||
return [fb_out(f, names) for f in db.scalars(stmt)]
|
||||
|
||||
|
||||
@router.patch("/feedback/{id}", response_model=S.FeedbackOut)
|
||||
def update(id: int, data: S.FeedbackPatch, db: Session = Depends(get_db), u: User = Depends(require_portal_admin)):
|
||||
f = get_or_404(db, Feedback, id)
|
||||
if data.status is not None:
|
||||
f.status = data.status
|
||||
if data.answer is not None:
|
||||
f.answer = data.answer.strip()
|
||||
f.answered_by_id, f.answered_at = u.id, config.now()
|
||||
commit(db)
|
||||
return fb_out(f, user_names(db))
|
||||
|
||||
|
||||
@router.delete("/feedback/{id}", status_code=204)
|
||||
def delete(id: int, db: Session = Depends(get_db), _: User = Depends(require_portal_admin)):
|
||||
db.delete(get_or_404(db, Feedback, id))
|
||||
commit(db)
|
||||
Reference in New Issue
Block a user