Seat the game master as a member so they can post in Public Press - #1311
Merged
Conversation
The game master was a User FK on Game with no Member row, but every chat mechanism keys on Member: ChannelMessage.sender is a non-null FK to it, ChannelMember tracks reads, unread counts subquery on it, and the channel_message notification audience derives from it. A game master could read Public Press but not post, mark read, or be notified. Member becomes the participant row and gains a `kind` discriminant. A game master holds a member row with no nation, no phase state, and no seat against the variant's nation count; MemberQuerySet.players() scopes everything that means "player" — seat counting, game start, nation assignment, phase states, draws, victory, abandonment, staging reclaim, admin reassignment. Game.seat() takes the kind, so game creation seats the game master into Public Press the same way it seats a creator who plays, and a data migration backfills existing games. Permissions split along the same line: the existing member classes are renamed to say Player and scoped to seats, and two participant classes admit any member row. Only message-create and mark-read use them, so the game master gains public press and nothing else — private channels, orders, draws and seats stay player-only. Game.game_master stays the source of truth for who the game master is; the member row carries participation. A unique constraint keeps it to one per game, and the existing (game, user) constraint now stops a game master also holding a seat at the database level. The serialized member roster is unchanged — the prefetches are player-scoped — so there is no visible change in the web app yet. ChannelMemberSerializer gains isGameMaster so a game master's messages can be attributed when the frontend renders them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7zRgVoVUWopeN7TBGkQY4
Contributor
|
Warning WIP limit exceeded. There are now 8 open pull requests — the project target is 5 or fewer. Please close or merge an existing PR before continuing with this one. |
johnpooch
pushed a commit
that referenced
this pull request
Sep 6, 2026
main's #1311 seats the game master as a Member, so the muster queries had to be narrowed to players or the GM would be treated as an unconfirmed seat: they would block the early start and be vacated at the deadline. - Game.unmustered_members() filters members.players(), matching how start_if_full and delete_if_empty_pending already count seats. - IsUnmusteredMember resolves the requester through members.players(). - MemberMusterView permissions follow main's IsGameMember -> IsGamePlayer rename. Adds test_game_master_does_not_block_or_lose_a_seat_in_mustering, which fails without the players() filter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cbt5ynJUeZXJ6MPuAnQ9vR
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
A non-playing game master could read Public Press but not post in it. Every chat mechanism keys on
Member—ChannelMessage.senderis a non-null FK to it (service/channel/models.py:127),ChannelMembertracks reads, the unread-count subquery joins through it (service/channel/models.py:39-56), and thechannel_messagenotification audience derives from it — but the game master was only aUserFK onGame, with no member row (service/game/serializers.py:578-581skipped creating one).This makes
Memberthe participant row rather than strictly a player seat, so the game master gets chat authorship, read tracking, unread counts and notifications with no nullable columns and no second identity shape in the chat layer. Scoped deliberately to Public Press only — private channels, orders, draws and seats stay player-only, leaving the neutrality questions open for a follow-up.Backend only. No visible change in the web app yet — the serialized member roster is player-scoped, so the frontend still finds no
currentMemberfor a game master and still hides the composer. Rendering a game master's messages (avatar, "Game Master" label, colour) is the follow-up;ChannelMemberSerializergainsisGameMasterso that work has something to key on.Model
Membergains akinddiscriminant (player/game_master) andMemberQuerySet.players(). A game master holds a member row with no nation, no phase state, and no seat against the variant's nation count.Game.seat()takes the kind, so game creation seats the game master into Public Press exactly the way it seats a creator who plays.Game.game_masterstays the source of truth for who the game master is — the member row carries participation. That is a deliberate denormalisation, following the existingMember.sandbox/Game.sandboxprecedent; only game creation and the backfill write both. A new unique constraint keeps it to one game master per game, and the existing(game, user)constraint now stops a game master also holding a seat at the database level.Call-site audit
Everything meaning "player" now goes through
players(): game start and the full-seat check (start_if_fullwould otherwise never fire, since the roster would be one over),IsSpaceAvailable, nation assignment and phase-state creation on both start and phase advance, draw proposals, solo-victory detection, the abandonment check (a game master never enters civil disorder, so a game master game could never be abandoned), admin reassignment, staging reclaim, empty-pending deletion,can_leave,can_remove_member, and the three member prefetches that feed the serialized roster.Permissions
The member permission classes are renamed to say
Playerand scoped to seats; two newParticipantclasses admit any member row. OnlyChannelMessageCreateViewandChannelMarkReadViewuse the participant variants.Migrations
0013adds the field and constraint;0014backfills a member row plus a Public PressChannelMemberfor every existing game with a game master.Notes
senderplus a user FK. That leaves read tracking and unread counts still broken for the game master, and forks the sender shape for every future game master chat feature. Its "Approach" section also cites anIsGameMemberOrGameMasterpermission that does not exist.GameMasterSerializer, so_is_maskednow returnsFalsefor them — otherwise the header would show a real name and their messages "Anonymous".Checklist
/review-pragainst this PR in Claude Code and addressed (or responded to) its findingsBackend suite 2357 passed, frontend 666 passed,
npx tsc -b --noEmitclean. Codegen rerun with the Firebase guard satisfied, so the schema diff is the one added field with no/devices/drift.🤖 Generated with Claude Code
https://claude.ai/code/session_01C7zRgVoVUWopeN7TBGkQY4
Generated by Claude Code