SSE: не рассылать ratings на правки без влияния на рейтинг; событие group при смене настроек группы
- notify.match_changed получил флаг ratings: фото (игрок и админ) его не шлют, правка
партии — только если изменились входы рейтинга (match_service.rating_inputs до/после).
- PATCH /api/groups/{id} публикует group — переименование и правило 9 раундов видны
остальным участникам сразу.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -243,6 +243,7 @@ def update_match(
|
|||||||
admin: User = Depends(get_current_admin),
|
admin: User = Depends(get_current_admin),
|
||||||
) -> s.MatchRead:
|
) -> s.MatchRead:
|
||||||
match = match_service.get_match(session, match_id)
|
match = match_service.get_match(session, match_id)
|
||||||
|
rating_before = match_service.rating_inputs(session, match)
|
||||||
match = match_service.update_match(
|
match = match_service.update_match(
|
||||||
session,
|
session,
|
||||||
match,
|
match,
|
||||||
@@ -265,7 +266,9 @@ def update_match(
|
|||||||
ip=client_ip(request),
|
ip=client_ip(request),
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
notify.match_changed(session, match)
|
notify.match_changed(
|
||||||
|
session, match, ratings=match_service.rating_inputs(session, match) != rating_before
|
||||||
|
)
|
||||||
return build_match_read(session, match, can_modify=True)
|
return build_match_read(session, match, can_modify=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -356,7 +359,7 @@ def admin_add_attachment(
|
|||||||
att = attachment_service.add_photo(
|
att = attachment_service.add_photo(
|
||||||
session, match, admin, content, ext, user_service.avatar_media_type(ext)
|
session, match, admin, content, ext, user_service.avatar_media_type(ext)
|
||||||
)
|
)
|
||||||
notify.match_changed(session, match)
|
notify.match_changed(session, match, ratings=False)
|
||||||
return attachment_read(att, f"/api/admin/matches/{match_id}")
|
return attachment_read(att, f"/api/admin/matches/{match_id}")
|
||||||
|
|
||||||
|
|
||||||
@@ -369,7 +372,7 @@ def admin_delete_attachment(
|
|||||||
) -> s.OkResponse:
|
) -> s.OkResponse:
|
||||||
match = match_service.get_match(session, match_id)
|
match = match_service.get_match(session, match_id)
|
||||||
attachment_service.delete(session, match, attachment_id)
|
attachment_service.delete(session, match, attachment_id)
|
||||||
notify.match_changed(session, match)
|
notify.match_changed(session, match, ratings=False)
|
||||||
return s.OkResponse()
|
return s.OkResponse()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ def update_group(
|
|||||||
group_service.rename_group(session, group, body.name)
|
group_service.rename_group(session, group, body.name)
|
||||||
if body.nine_rounds_rule is not None:
|
if body.nine_rounds_rule is not None:
|
||||||
group_service.set_nine_rounds_rule(session, group, body.nine_rounds_rule)
|
group_service.set_nine_rounds_rule(session, group, body.nine_rounds_rule)
|
||||||
|
notify.group_changed(session, group_id) # остальным участникам — живое обновление настроек
|
||||||
return _detail(session, group_id, user.id) # type: ignore[arg-type]
|
return _detail(session, group_id, user.id) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ def update_match(
|
|||||||
) -> s.MatchRead:
|
) -> s.MatchRead:
|
||||||
match = match_service.get_match(session, match_id)
|
match = match_service.get_match(session, match_id)
|
||||||
match_service.assert_can_modify(session, match, user)
|
match_service.assert_can_modify(session, match, user)
|
||||||
|
rating_before = match_service.rating_inputs(session, match)
|
||||||
match = match_service.update_match(
|
match = match_service.update_match(
|
||||||
session,
|
session,
|
||||||
match,
|
match,
|
||||||
@@ -257,7 +258,9 @@ def update_match(
|
|||||||
user_agent=request.headers.get("user-agent"),
|
user_agent=request.headers.get("user-agent"),
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
notify.match_changed(session, match)
|
notify.match_changed(
|
||||||
|
session, match, ratings=match_service.rating_inputs(session, match) != rating_before
|
||||||
|
)
|
||||||
return build_match_read(session, match, can_modify=match_service.can_modify(session, match, user))
|
return build_match_read(session, match, can_modify=match_service.can_modify(session, match, user))
|
||||||
|
|
||||||
|
|
||||||
@@ -304,7 +307,7 @@ def add_attachment(
|
|||||||
att = attachment_service.add_photo(
|
att = attachment_service.add_photo(
|
||||||
session, match, user, content, ext, user_service.avatar_media_type(ext)
|
session, match, user, content, ext, user_service.avatar_media_type(ext)
|
||||||
)
|
)
|
||||||
notify.match_changed(session, match)
|
notify.match_changed(session, match, ratings=False)
|
||||||
return attachment_read(att, f"/api/matches/{match_id}")
|
return attachment_read(att, f"/api/matches/{match_id}")
|
||||||
|
|
||||||
|
|
||||||
@@ -318,7 +321,7 @@ def delete_attachment(
|
|||||||
match = match_service.get_match(session, match_id)
|
match = match_service.get_match(session, match_id)
|
||||||
_assert_can_attach(session, match, user)
|
_assert_can_attach(session, match, user)
|
||||||
attachment_service.delete(session, match, attachment_id)
|
attachment_service.delete(session, match, attachment_id)
|
||||||
notify.match_changed(session, match)
|
notify.match_changed(session, match, ratings=False)
|
||||||
return s.OkResponse()
|
return s.OkResponse()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -479,6 +479,27 @@ def assert_can_modify(session: Session, match: Match, user: User) -> None:
|
|||||||
raise ForbiddenError("Недостаточно прав для изменения партии.")
|
raise ForbiddenError("Недостаточно прав для изменения партии.")
|
||||||
|
|
||||||
|
|
||||||
|
def rating_inputs(session: Session, match: Match) -> tuple:
|
||||||
|
"""Снимок всего, что из партии читает рейтинг (stats_service.load_history): порядок
|
||||||
|
в истории, итоги и места. Сравнение снимков до и после правки показывает, сдвинула ли
|
||||||
|
она рейтинг, — комментарий или фото его не трогают."""
|
||||||
|
seats = session.exec(
|
||||||
|
select(MatchParticipant).where(MatchParticipant.match_id == match.id)
|
||||||
|
).all()
|
||||||
|
return (
|
||||||
|
match.status,
|
||||||
|
match.played_at,
|
||||||
|
match.finished_at,
|
||||||
|
match.win_reason,
|
||||||
|
match.end_round,
|
||||||
|
match.nine_rounds_rule,
|
||||||
|
sorted(
|
||||||
|
(p.user_id, p.faction_id, p.place, p.eliminated, p.objectives, p.worlds)
|
||||||
|
for p in seats
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_match(
|
def update_match(
|
||||||
session: Session,
|
session: Session,
|
||||||
match: Match,
|
match: Match,
|
||||||
|
|||||||
@@ -32,12 +32,15 @@ def _ratings_changed(session: Session, notified: list[int]) -> None:
|
|||||||
hub.publish(ids, {"type": "ratings"})
|
hub.publish(ids, {"type": "ratings"})
|
||||||
|
|
||||||
|
|
||||||
def match_changed(session: Session, match: Match) -> None:
|
def match_changed(session: Session, match: Match, *, ratings: bool = True) -> None:
|
||||||
"""Партия изменилась — уведомить всех участников её группы, а если она завершена —
|
"""Партия изменилась — уведомить всех участников её группы, а если она завершена —
|
||||||
и остальных игроков (_ratings_changed)."""
|
и остальных игроков (_ratings_changed).
|
||||||
|
|
||||||
|
ratings=False — правка, которая рейтинг не двигает (фото, комментарий): тогда
|
||||||
|
остальным игрокам незачем перезапрашивать все рейтинговые витрины."""
|
||||||
members = _group_member_ids(session, match.group_id)
|
members = _group_member_ids(session, match.group_id)
|
||||||
hub.publish(members, {"type": "match", "match_id": match.id, "group_id": match.group_id})
|
hub.publish(members, {"type": "match", "match_id": match.id, "group_id": match.group_id})
|
||||||
if match.status == "finished":
|
if ratings and match.status == "finished":
|
||||||
_ratings_changed(session, members)
|
_ratings_changed(session, members)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -81,3 +81,60 @@ def test_deleting_finished_match_reaches_players_outside_group(
|
|||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
assert _recipients(published, "match") == {me["id"], p2}
|
assert _recipients(published, "match") == {me["id"], p2}
|
||||||
assert outsider in _recipients(published, "ratings")
|
assert outsider in _recipients(published, "ratings")
|
||||||
|
|
||||||
|
|
||||||
|
def _finished_match(client: TestClient, me: dict, p2: int, gid: int, fids: list[int]) -> int:
|
||||||
|
mid = start_match(
|
||||||
|
client, gid,
|
||||||
|
[{"user_id": me["id"], "faction_id": fids[0]}, {"user_id": p2, "faction_id": fids[1]}],
|
||||||
|
).json()["id"]
|
||||||
|
finish_match(client, mid, [{"user_id": me["id"], "place": 1}, {"user_id": p2, "place": 2}])
|
||||||
|
return mid
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_match(client: TestClient, mid: int, me: dict, p2: int, fids: list[int], **changes):
|
||||||
|
"""PATCH как у формы правки: состав передаётся целиком, даже если меняется комментарий."""
|
||||||
|
places = changes.pop("places", (1, 2))
|
||||||
|
body = {
|
||||||
|
"expected_version": client.get(f"/api/matches/{mid}").json()["version"],
|
||||||
|
"participants": [
|
||||||
|
{"user_id": me["id"], "faction_id": fids[0], "place": places[0]},
|
||||||
|
{"user_id": p2, "faction_id": fids[1], "place": places[1]},
|
||||||
|
],
|
||||||
|
**changes,
|
||||||
|
}
|
||||||
|
r = client.patch(f"/api/matches/{mid}", json=body, headers=csrf_headers(client))
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_edit_does_not_broadcast_ratings(client: TestClient, engine, monkeypatch):
|
||||||
|
"""Правка без влияния на рейтинг — событие только группе: остальным игрокам незачем
|
||||||
|
перезапрашивать все рейтинговые витрины."""
|
||||||
|
me, gid, p2, outsider, fids = _two_groups(client, engine)
|
||||||
|
mid = _finished_match(client, me, p2, gid, fids)
|
||||||
|
|
||||||
|
published = _capture_events(monkeypatch)
|
||||||
|
_patch_match(client, mid, me, p2, fids, overall_comment="Хорошая партия")
|
||||||
|
assert _recipients(published, "match") == {me["id"], p2}
|
||||||
|
assert _recipients(published, "ratings") == set()
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_edit_broadcasts_ratings(client: TestClient, engine, monkeypatch):
|
||||||
|
me, gid, p2, outsider, fids = _two_groups(client, engine)
|
||||||
|
mid = _finished_match(client, me, p2, gid, fids)
|
||||||
|
|
||||||
|
published = _capture_events(monkeypatch)
|
||||||
|
_patch_match(client, mid, me, p2, fids, places=(2, 1))
|
||||||
|
assert outsider in _recipients(published, "ratings")
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_settings_change_reaches_members(client: TestClient, engine, monkeypatch):
|
||||||
|
"""Переключение домашнего правила видно остальным участникам без перезагрузки."""
|
||||||
|
me, gid, p2, outsider, fids = _two_groups(client, engine)
|
||||||
|
|
||||||
|
published = _capture_events(monkeypatch)
|
||||||
|
r = client.patch(
|
||||||
|
f"/api/groups/{gid}", json={"nine_rounds_rule": True}, headers=csrf_headers(client)
|
||||||
|
)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert _recipients(published, "group") == {me["id"], p2}
|
||||||
|
|||||||
Reference in New Issue
Block a user