Партии: фото в общем комментарии (до 10), загрузка при завершении
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,42 @@
|
||||
"""Роутер партий: рандом фракции, старт, завершение, детали, правка, удаление."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, File, Request, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.auth.deps import get_current_user
|
||||
from app.core.errors import NoGroupError
|
||||
from app.core.errors import ConflictError, NoGroupError, NotFoundError, ValidationError
|
||||
from app.core.timeutil import iso_utc
|
||||
from app.db.session import get_session
|
||||
from app.models import Match, User
|
||||
from app.models import Match, MatchAttachment, User
|
||||
from app.schemas import api as s
|
||||
from app.services import audit_service, group_service, match_service
|
||||
from app.services import (
|
||||
attachment_service,
|
||||
audit_service,
|
||||
group_service,
|
||||
match_service,
|
||||
user_service,
|
||||
)
|
||||
from app.services.match_service import FinishInput, ParticipantInput, RosterInput
|
||||
|
||||
router = APIRouter(prefix="/matches", tags=["matches"])
|
||||
|
||||
_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024 # 10 МБ
|
||||
|
||||
|
||||
def attachment_read(att: MatchAttachment, base: str) -> s.AttachmentRead:
|
||||
"""AttachmentRead с URL под нужным префиксом (base = '/api/matches/{id}' или
|
||||
'/api/admin/matches/{id}'); cookie игрока/админа доходит до своего пути."""
|
||||
return s.AttachmentRead(
|
||||
id=att.id, # type: ignore[arg-type]
|
||||
kind=att.kind,
|
||||
url=f"{base}/attachments/{att.id}",
|
||||
mime_type=att.mime_type,
|
||||
size_bytes=att.size_bytes,
|
||||
created_at=iso_utc(att.created_at),
|
||||
)
|
||||
|
||||
|
||||
def _ensure_has_any_group(session: Session, user: User) -> None:
|
||||
if not group_service.list_user_groups(session, user.id): # type: ignore[arg-type]
|
||||
@@ -48,7 +70,10 @@ def build_match_read(session: Session, match: Match, *, can_modify: bool = False
|
||||
created_by=match.created_by,
|
||||
can_modify=can_modify,
|
||||
participants=parts,
|
||||
attachments=[],
|
||||
attachments=[
|
||||
attachment_read(a, f"/api/matches/{match.id}")
|
||||
for a in attachment_service.list_for_match(session, match.id) # type: ignore[arg-type]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -189,6 +214,66 @@ def update_match(
|
||||
return build_match_read(session, match, can_modify=match_service.can_modify(session, match, user))
|
||||
|
||||
|
||||
# ─── Медиа партии (фото) ──────────────────────────────────────────────────────
|
||||
|
||||
def _assert_can_attach(session: Session, match: Match, user: User) -> None:
|
||||
match_service.assert_can_modify(session, match, user)
|
||||
if match.status != "in_progress":
|
||||
raise ConflictError("Медиа можно прикреплять только до завершения партии.")
|
||||
|
||||
|
||||
@router.post("/{match_id}/attachments", response_model=s.AttachmentRead)
|
||||
def add_attachment(
|
||||
match_id: int,
|
||||
file: UploadFile = File(...),
|
||||
session: Session = Depends(get_session),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> s.AttachmentRead:
|
||||
match = match_service.get_match(session, match_id)
|
||||
_assert_can_attach(session, match, user)
|
||||
content = file.file.read(_ATTACHMENT_MAX_BYTES + 1)
|
||||
if len(content) > _ATTACHMENT_MAX_BYTES:
|
||||
raise ValidationError("Файл слишком большой (макс. 10 МБ).")
|
||||
ext = user_service.sniff_image_ext(content)
|
||||
if ext is None:
|
||||
raise ValidationError("Поддерживаются только изображения PNG, JPEG или WebP.")
|
||||
att = attachment_service.add_photo(
|
||||
session, match, user, content, ext, user_service.avatar_media_type(ext)
|
||||
)
|
||||
return attachment_read(att, f"/api/matches/{match_id}")
|
||||
|
||||
|
||||
@router.delete("/{match_id}/attachments/{attachment_id}", response_model=s.OkResponse)
|
||||
def delete_attachment(
|
||||
match_id: int,
|
||||
attachment_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> s.OkResponse:
|
||||
match = match_service.get_match(session, match_id)
|
||||
_assert_can_attach(session, match, user)
|
||||
attachment_service.delete(session, match, attachment_id)
|
||||
return s.OkResponse()
|
||||
|
||||
|
||||
@router.get("/{match_id}/attachments/{attachment_id}")
|
||||
def get_attachment(
|
||||
match_id: int,
|
||||
attachment_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> FileResponse:
|
||||
match = match_service.get_match(session, match_id)
|
||||
group_service.assert_member(session, match.group_id, user.id) # type: ignore[arg-type]
|
||||
att = attachment_service.get_for_match(session, match_id, attachment_id)
|
||||
path = attachment_service.file_path(att)
|
||||
if not path.exists():
|
||||
raise NotFoundError("Файл не найден.")
|
||||
return FileResponse(
|
||||
path, media_type=att.mime_type, headers={"Cache-Control": "private, max-age=3600"}
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{match_id}", response_model=s.OkResponse)
|
||||
def delete_match(
|
||||
match_id: int,
|
||||
|
||||
Reference in New Issue
Block a user