feat(web): add private club showcases with membership flow (v3.7.0)
PR Checks / test-and-build (pull_request) Successful in 7m28s
PR Checks / test-and-build (pull_request) Successful in 7m28s
Implements Issue #110: game masters can now publish sessions exclusively to a club's private showcase, gated behind a member application and approval flow. Adds a 4-state publication_mode (None/Catalog/ClubOnly/Both) replacing the binary is_public, plus a club_memberships table with Pending/Active/Rejected/Left lifecycle and partial unique index ensuring a single Active row per (group, player). Highlights - V030 migration: club_memberships, publication_mode, drop is_public, recreate partial indexes, portfolio_games gains publication_mode. - PublicationMode enum + extensions in GmRelay.Shared. - ISessionStore gains 12 membership/showcase methods; AuthorizedMembershipService owns the membership flow with GM-only approve/reject authorization. - PublicClub / PublicMasterProfile / PublicSession: member- aware queries (ClubOnly visible only to Active members). - New pages: MyClubMemberships (/profile/memberships) and ClubApplications (/group/{id}/applications). - GroupDetails and EditSession switch from a bool toggle to a 4-state publication_mode selector. - NavMenu adds Moji kluby, PublicLayout adds Kluby. Tests: 4 new test files (PublicationMode, ClubMemberships, AuthorizedMembershipService, ClubShowcaseSource) + updates to PublicClubPages, AuthorizedSessionService/Portfolio service FakeSessionStore, CampaignTemplatesNavigation. 493 tests pass. Bump version 3.6.0 -> 3.7.0 across Directory.Build.props, compose.yaml, deploy.yml, NavMenu.razor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -839,9 +839,11 @@ public sealed class AuthorizedSessionServiceTests
|
||||
public Guid? LastPublicSessionId { get; private set; }
|
||||
public Guid? LastPublicSessionGroupId { get; private set; }
|
||||
public bool? LastSessionPublicValue { get; private set; }
|
||||
public PublicationMode? LastSessionPublicationMode { get; private set; }
|
||||
public Guid? LastPublicBatchId { get; private set; }
|
||||
public Guid? LastPublicBatchGroupId { get; private set; }
|
||||
public bool? LastBatchPublicValue { get; private set; }
|
||||
public PublicationMode? LastBatchPublicationMode { get; private set; }
|
||||
public bool RemovePlayerCalled { get; private set; }
|
||||
public Guid? LastRemovedPlayerSessionId { get; private set; }
|
||||
public Guid? LastRemovedPlayerGroupId { get; private set; }
|
||||
@@ -888,45 +890,81 @@ public sealed class AuthorizedSessionServiceTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SetSessionPublicAsync(Guid sessionId, Guid groupId, bool isPublic)
|
||||
public Task SetSessionPublicationModeAsync(Guid sessionId, Guid groupId, PublicationMode mode)
|
||||
{
|
||||
SetSessionPublicCalled = true;
|
||||
LastPublicSessionId = sessionId;
|
||||
LastPublicSessionGroupId = groupId;
|
||||
LastSessionPublicValue = isPublic;
|
||||
LastSessionPublicationMode = mode;
|
||||
|
||||
if (sessionsById.TryGetValue(sessionId, out var session))
|
||||
{
|
||||
sessionsById[sessionId] = session with { IsPublic = isPublic };
|
||||
sessionsById[sessionId] = session with { PublicationMode = mode.ToDatabaseValue() };
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SetBatchPublicAsync(Guid batchId, Guid groupId, bool isPublic)
|
||||
public Task SetBatchPublicationModeAsync(Guid batchId, Guid groupId, PublicationMode mode)
|
||||
{
|
||||
SetBatchPublicCalled = true;
|
||||
LastPublicBatchId = batchId;
|
||||
LastPublicBatchGroupId = groupId;
|
||||
LastBatchPublicValue = isPublic;
|
||||
LastBatchPublicationMode = mode;
|
||||
|
||||
foreach (var session in sessionsById.Values.Where(session => session.BatchId == batchId && session.GroupId == groupId).ToList())
|
||||
{
|
||||
sessionsById[session.Id] = session with { IsPublic = isPublic };
|
||||
sessionsById[session.Id] = session with { PublicationMode = mode.ToDatabaseValue() };
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<WebPublicClub?> GetPublicClubBySlugAsync(string slug) =>
|
||||
public Task<WebPublicClub?> GetPublicClubBySlugAsync(string slug, Guid? viewerPlayerId = null) =>
|
||||
Task.FromResult<WebPublicClub?>(null);
|
||||
|
||||
public Task<WebPublicSession?> GetPublicSessionAsync(Guid sessionId) =>
|
||||
public Task<WebPublicSession?> GetPublicSessionAsync(Guid sessionId, Guid? viewerPlayerId = null) =>
|
||||
Task.FromResult<WebPublicSession?>(null);
|
||||
|
||||
public Task<bool> IsGroupManagerAsync(Guid groupId, long telegramId) =>
|
||||
Task.FromResult(IsManager(groupId, telegramId));
|
||||
|
||||
public Task<bool> IsActiveClubMemberAsync(Guid groupId, Guid playerId) =>
|
||||
Task.FromResult(false);
|
||||
|
||||
public Task<Guid?> GetPlayerIdByPlatformIdentityAsync(string platform, string externalUserId) =>
|
||||
Task.FromResult<Guid?>(null);
|
||||
|
||||
public Task<IReadOnlyList<WebClubShowcaseSession>> GetClubShowcaseSessionsAsync(Guid groupId, Guid? viewerPlayerId, int page, int pageSize) =>
|
||||
Task.FromResult<IReadOnlyList<WebClubShowcaseSession>>([]);
|
||||
|
||||
public Task<int> GetPendingApplicationsCountAsync(Guid groupId) =>
|
||||
Task.FromResult(0);
|
||||
|
||||
public Task<List<WebPendingApplication>> GetPendingApplicationsAsync(Guid groupId) =>
|
||||
Task.FromResult(new List<WebPendingApplication>());
|
||||
|
||||
public Task<List<WebMembership>> GetMembershipsForPlayerAsync(Guid playerId) =>
|
||||
Task.FromResult(new List<WebMembership>());
|
||||
|
||||
public Task<Guid> ApplyForMembershipAsync(Guid groupId, Guid playerId, string? message) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task ApproveMembershipAsync(Guid membershipId, Guid approverPlayerId) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task RejectMembershipAsync(Guid membershipId, Guid approverPlayerId) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task LeaveClubMembershipAsync(Guid membershipId, Guid playerId) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<Guid?> GetGroupIdForMembershipAsync(Guid membershipId) =>
|
||||
Task.FromResult<Guid?>(null);
|
||||
|
||||
public Task<PublicMasterProfile?> GetPublicMasterProfileBySlugAsync(string slug, Guid? viewerPlayerId = null) =>
|
||||
Task.FromResult<PublicMasterProfile?>(null);
|
||||
|
||||
public Task<bool> IsGroupOwnerAsync(Guid groupId, long telegramId) =>
|
||||
Task.FromResult(IsOwner(groupId, telegramId));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user