From 89001f801a7bc1ba14850d37800b7b52792de4e3 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Sun, 19 Jan 2025 18:38:03 -0500 Subject: [PATCH 1/8] WIP timeline updates --- .../GameImportExportModelsMappingTests.cs | 26 +++---- .../Data/Entities/ExternalGameHost.cs | 3 + src/Gameboard.Api/Data/Entities/Game.cs | 13 ---- .../Challenge/Services/ChallengeService.cs | 4 +- .../Features/Support/SupportModels.cs | 6 ++ .../GetTeamEventHorizon.cs | 67 ++++++++++++++++++- .../GetTeamEventHorizonModels.cs | 14 +++- .../Features/Ticket/TicketService.cs | 5 +- 8 files changed, 103 insertions(+), 35 deletions(-) diff --git a/src/Gameboard.Api.Tests.Unit/Tests/Features/Games/GameImportExport/GameImportExportModelsMappingTests.cs b/src/Gameboard.Api.Tests.Unit/Tests/Features/Games/GameImportExport/GameImportExportModelsMappingTests.cs index cbe4ae3c..a4daafab 100644 --- a/src/Gameboard.Api.Tests.Unit/Tests/Features/Games/GameImportExport/GameImportExportModelsMappingTests.cs +++ b/src/Gameboard.Api.Tests.Unit/Tests/Features/Games/GameImportExport/GameImportExportModelsMappingTests.cs @@ -26,18 +26,18 @@ public void GameImportExportExternalHosts_ExportsProperties() .Length.ShouldBe(0); } - // [Fact] - // public void GameImportExportExternalHosts_AttributeStrategy_ExportsProperties() - // { - // var properties = typeof(GameImportExportExternalHost).GetProperties(BindingFlags.Public | BindingFlags.Instance); - // var propertyNames = properties.Select(p => p.Name).ToArray(); - // var gameProperties = typeof(ExternalGameHost) - // .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) - // .Where(p => !p.HasAttribute()) - // .Where(p => !p.HasAttribute()); + [Fact] + public void GameImportExportExternalHosts_AttributeStrategy_ExportsProperties() + { + var properties = typeof(GameImportExportExternalHost).GetProperties(BindingFlags.Public | BindingFlags.Instance); + var propertyNames = properties.Select(p => p.Name).ToArray(); + var gameProperties = typeof(ExternalGameHost) + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Where(p => !p.HasAttribute()) + .Where(p => !p.HasAttribute()); - // gameProperties.Where(p => !propertyNames.Contains(p.Name)) - // .ToArray() - // .Length.ShouldBe(0); - // } + gameProperties.Where(p => !propertyNames.Contains(p.Name)) + .ToArray() + .Length.ShouldBe(0); + } } diff --git a/src/Gameboard.Api/Data/Entities/ExternalGameHost.cs b/src/Gameboard.Api/Data/Entities/ExternalGameHost.cs index 1d6db939..0ecc9353 100644 --- a/src/Gameboard.Api/Data/Entities/ExternalGameHost.cs +++ b/src/Gameboard.Api/Data/Entities/ExternalGameHost.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Gameboard.Api.Features.Games; namespace Gameboard.Api.Data; @@ -10,6 +11,7 @@ public sealed class ExternalGameHost : IEntity public required bool DestroyResourcesOnDeployFailure { get; set; } public int? GamespaceDeployBatchSize { get; set; } public int? HttpTimeoutInSeconds { get; set; } + [DontExport] public string HostApiKey { get; set; } public required string HostUrl { get; set; } public string PingEndpoint { get; set; } @@ -17,5 +19,6 @@ public sealed class ExternalGameHost : IEntity public string TeamExtendedEndpoint { get; set; } // nav properties + [DontExport] public ICollection UsedByGames { get; set; } = new List(); } diff --git a/src/Gameboard.Api/Data/Entities/Game.cs b/src/Gameboard.Api/Data/Entities/Game.cs index 5733182d..2e5cd0ea 100644 --- a/src/Gameboard.Api/Data/Entities/Game.cs +++ b/src/Gameboard.Api/Data/Entities/Game.cs @@ -78,26 +78,13 @@ public class Game : IEntity public ICollection Feedback { get; set; } = []; public ICollection Prerequisites { get; set; } = []; - [NotMapped] public bool RequireSession => SessionMinutes > 0; - [NotMapped] public bool RequireTeam => MinTeamSize > 1; [NotMapped] public bool AllowTeam => MaxTeamSize > 1; - [NotMapped] - public bool IsLive => - GameStart != DateTimeOffset.MinValue && - GameStart.CompareTo(DateTimeOffset.UtcNow) < 0 && - GameEnd.CompareTo(DateTimeOffset.UtcNow) > 0; - - [NotMapped] - public bool HasEnded => - GameEnd.CompareTo(DateTimeOffset.UtcNow) < 0; - [NotMapped] public bool RegistrationActive => RegistrationType != GameRegistrationType.None && RegistrationOpen.CompareTo(DateTimeOffset.UtcNow) < 0 && RegistrationClose.CompareTo(DateTimeOffset.UtcNow) > 0; - [NotMapped] public bool IsCompetitionMode => PlayerMode == PlayerMode.Competition; [NotMapped] public bool IsPracticeMode => PlayerMode == PlayerMode.Practice; } diff --git a/src/Gameboard.Api/Features/Challenge/Services/ChallengeService.cs b/src/Gameboard.Api/Features/Challenge/Services/ChallengeService.cs index 7594c50e..767e3a98 100644 --- a/src/Gameboard.Api/Features/Challenge/Services/ChallengeService.cs +++ b/src/Gameboard.Api/Features/Challenge/Services/ChallengeService.cs @@ -109,7 +109,7 @@ public async Task Create(NewChallenge model, string actorId, string g } // if we're outside the execution window, we need to be sure the acting person is an admin - if (player.Game.IsCompetitionMode) + if (player.Game.PlayerMode == PlayerMode.Competition) { // check gamespace limits for competitive games only var teamActiveChallenges = await _teamService.GetChallengesWithActiveGamespace(player.TeamId, player.GameId, cancellationToken); @@ -156,7 +156,7 @@ public async Task Create(NewChallenge model, string actorId, string g .SingleAsync(s => s.Id == model.SpecId, cancellationToken); var playerCount = 1; - if (player.Game.AllowTeam) + if (player.Game.MaxTeamSize > 1) { playerCount = await _store .WithNoTracking() diff --git a/src/Gameboard.Api/Features/Support/SupportModels.cs b/src/Gameboard.Api/Features/Support/SupportModels.cs index 90cc269f..7151c442 100644 --- a/src/Gameboard.Api/Features/Support/SupportModels.cs +++ b/src/Gameboard.Api/Features/Support/SupportModels.cs @@ -25,3 +25,9 @@ public sealed class UpsertSupportSettingsAutoTagRequest public bool? IsEnabled { get; set; } public required string Tag { get; set; } } + +public static class TicketStatus +{ + public static readonly string Closed = "Closed"; + public static readonly string Open = "Open"; +} diff --git a/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizon.cs b/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizon.cs index 5a2fff50..16d7b4a1 100644 --- a/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizon.cs +++ b/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizon.cs @@ -6,6 +6,7 @@ using Gameboard.Api.Common.Services; using Gameboard.Api.Data; using Gameboard.Api.Features.Scores; +using Gameboard.Api.Features.Support; using Gameboard.Api.Features.Users; using Gameboard.Api.Structure.MediatR; using Gameboard.Api.Structure.MediatR.Validators; @@ -68,7 +69,9 @@ await _validator .ToArrayAsync(cancellationToken); if (challenges.Length == 0) + { return null; + } // make sure we have exactly one game var games = challenges.Select(c => c.Game).ToArray(); @@ -119,6 +122,9 @@ await _validator var submissionEvents = BuildSubmissionEvents(submissions, challengeMaxScores); events.AddRange(submissionEvents); + // ticket events come from ticket activity + events.AddRange(await BuildTicketOpenClosedEvents(request.TeamId, cancellationToken)); + // finally, build the event horizon response return new EventHorizon { @@ -159,7 +165,7 @@ await _validator /// /// /// - private IEnumerable BuildGamespaceEvents(IEnumerable challenges) + private EventHorizonGamespaceOnOffEvent[] BuildGamespaceEvents(IEnumerable challenges) { var retVal = new List(); @@ -222,7 +228,7 @@ private IEnumerable BuildGamespaceEvents(IEnume return retVal.OrderBy(e => e.Timestamp).ToArray(); } - private IEnumerable BuildSubmissionEvents(IEnumerable submissions, IDictionary challengeSolveScores) + private IEventHorizonEvent[] BuildSubmissionEvents(IEnumerable submissions, IDictionary challengeSolveScores) { // we'll return a list of submission/solve events var retVal = new List(); @@ -287,6 +293,61 @@ private IEnumerable BuildSubmissionEvents(IEnumerable BuildTicketOpenClosedEvents(string teamId, CancellationToken cancellationToken) + { + // pull the data first, then worry about organizing it "client" side + var challengeTickets = await _store + .WithNoTracking() + .Where(t => t.ChallengeId != null && t.ChallengeId != string.Empty && t.TeamId == teamId) + .Select(t => new { t.Id, t.Status, t.Created, t.ChallengeId, Activity = t.Activity.Select(a => new { a.Id, a.Timestamp, a.Status }).Distinct().OrderBy(a => a.Status).ToList() }) + .GroupBy(a => a.ChallengeId) + .ToDictionaryAsync(gr => gr.Key, gr => gr.ToList(), cancellationToken); + + var events = new List(); + + // for each challenge, find its tickets and build events for them + foreach (var challengeId in challengeTickets.Keys) + { + // we auto-create an open event for each challenge group, because: + // - The challenge is guaranteed to have at least one ticket if it comes back from the query + // - new tickets don't get an Activity entry just for opening, so we need to simulate that here + var tickets = challengeTickets[challengeId]; + + foreach (var ticket in tickets) + { + var lastOpenStamp = ticket.Created; + var creatingEvent = new EventHorizonTicketOpenCloseEvent + { + Id = challengeId, + ChallengeId = challengeId, + Timestamp = ticket.Created, + Type = EventHorizonEventType.TicketOpenClose, + EventData = new EventHorizonTicketOpenClosedEventData + { + ClosedAt = default + } + }; + events.Add(creatingEvent); + + foreach (var activity in ticket.Activity) + { + if (ticket.Status == TicketStatus.Open && lastOpenStamp == default) + { + lastOpenStamp = activity.Timestamp; + } + else if (activity.Status == TicketStatus.Closed && lastOpenStamp != default) + { + creatingEvent.EventData.ClosedAt = activity.Timestamp; + lastOpenStamp = default; + } + } + } + + } + + return [.. events]; } } diff --git a/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizonModels.cs b/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizonModels.cs index 34e53d6d..7a0b82b8 100644 --- a/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizonModels.cs +++ b/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizonModels.cs @@ -10,13 +10,15 @@ public enum EventHorizonEventType GamespaceOnOff, SolveComplete, SubmissionRejected, - SubmissionScored + SubmissionScored, + TicketOpenClose } [JsonDerivedType(typeof(EventHorizonEvent))] [JsonDerivedType(typeof(EventHorizonGamespaceOnOffEvent))] [JsonDerivedType(typeof(EventHorizonSolveCompleteEvent))] [JsonDerivedType(typeof(EventHorizonSubmissionScoredEvent))] +[JsonDerivedType(typeof(EventHorizonTicketOpenCloseEvent))] public interface IEventHorizonEvent { public string Id { get; set; } @@ -66,6 +68,16 @@ public sealed class EventHorizonSubmissionScoredEvent : EventHorizonEvent public required EventHorizonSubmissionScoredEventData EventData { get; set; } } +public sealed class EventHorizonTicketOpenCloseEvent : EventHorizonEvent, IEventHorizonEvent +{ + public required EventHorizonTicketOpenClosedEventData EventData { get; set; } +} + +public sealed class EventHorizonTicketOpenClosedEventData +{ + public required DateTimeOffset ClosedAt { get; set; } +} + public sealed class EventHorizonGame { public required string Id { get; set; } diff --git a/src/Gameboard.Api/Features/Ticket/TicketService.cs b/src/Gameboard.Api/Features/Ticket/TicketService.cs index 0caa41ea..453b84ed 100644 --- a/src/Gameboard.Api/Features/Ticket/TicketService.cs +++ b/src/Gameboard.Api/Features/Ticket/TicketService.cs @@ -33,8 +33,7 @@ public class TicketService CoreOptions options, IUserRolePermissionsService permissionsService, IStore store, - ISupportHubBus supportHubBus, - ITeamService teamService + ISupportHubBus supportHubBus ) : _Service(logger, mapper, options) { private readonly IActingUserService _actingUserService = actingUserService; @@ -46,9 +45,9 @@ ITeamService teamService private readonly IUserRolePermissionsService _permissionsService = permissionsService; private readonly IStore _store = store; private readonly ISupportHubBus _supportHubBus = supportHubBus; - private readonly ITeamService _teamService = teamService; internal static char TAGS_DELIMITER = ' '; + public static readonly string OpenStatus = "Open"; public string GetFullKey(int key) => $"{(Options.KeyPrefix.IsEmpty() ? "GB" : Options.KeyPrefix)}-{key}"; From 7e1e9cb6971bbcae804f8a669d51de97d34a8278 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Tue, 21 Jan 2025 10:15:46 -0500 Subject: [PATCH 2/8] Minor cleanup --- src/Gameboard.Api/Features/Scores/ScoreDenormalizationService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Gameboard.Api/Features/Scores/ScoreDenormalizationService.cs b/src/Gameboard.Api/Features/Scores/ScoreDenormalizationService.cs index 5bb5235b..9199da79 100644 --- a/src/Gameboard.Api/Features/Scores/ScoreDenormalizationService.cs +++ b/src/Gameboard.Api/Features/Scores/ScoreDenormalizationService.cs @@ -103,7 +103,6 @@ private async Task RerankGame(string gameId) .ToArrayAsync(); var captains = await _teamService.ResolveCaptains(teams.Select(t => t.TeamId).ToArray(), cancellationToken: CancellationToken.None); - var rankedTeams = _scoringService.GetTeamRanks(teams.Select(t => { captains.TryGetValue(t.TeamId, out var captain); From 848f41ba5f75c9a3b26ebb8be2b72f46a94eecb6 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Tue, 21 Jan 2025 12:29:38 -0500 Subject: [PATCH 3/8] Remove a back check in 'get scoreboard' originally intended to cause reranking of old games --- .../GetScoreboardQuery/GetScoreboard.cs | 61 ++++++------------- 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/src/Gameboard.Api/Features/Scores/GetScoreboardQuery/GetScoreboard.cs b/src/Gameboard.Api/Features/Scores/GetScoreboardQuery/GetScoreboard.cs index 3964da56..f91a38f2 100644 --- a/src/Gameboard.Api/Features/Scores/GetScoreboardQuery/GetScoreboard.cs +++ b/src/Gameboard.Api/Features/Scores/GetScoreboardQuery/GetScoreboard.cs @@ -16,38 +16,26 @@ namespace Gameboard.Api.Features.Scores; public record GetScoreboardQuery(string GameId) : IRequest; -internal class GetScoreboardHandler : IRequestHandler +internal class GetScoreboardHandler +( + IActingUserService actingUser, + EntityExistsValidator gameExists, + INowService now, + IScoreDenormalizationService scoringDenormalizationService, + IScoringService scoringService, + IStore store, + ITeamService teamService, + IValidatorService validatorService +) : IRequestHandler { - private readonly IActingUserService _actingUser; - private readonly EntityExistsValidator _gameExists; - private readonly INowService _now; - private readonly IScoreDenormalizationService _scoringDenormalizationService; - private readonly IScoringService _scoringService; - private readonly IStore _store; - private readonly ITeamService _teamService; - private readonly IValidatorService _validatorService; - - public GetScoreboardHandler - ( - IActingUserService actingUser, - EntityExistsValidator gameExists, - INowService now, - IScoreDenormalizationService scoringDenormalizationService, - IScoringService scoringService, - IStore store, - ITeamService teamService, - IValidatorService validatorService - ) - { - _actingUser = actingUser; - _gameExists = gameExists; - _now = now; - _scoringDenormalizationService = scoringDenormalizationService; - _scoringService = scoringService; - _store = store; - _teamService = teamService; - _validatorService = validatorService; - } + private readonly IActingUserService _actingUser = actingUser; + private readonly EntityExistsValidator _gameExists = gameExists; + private readonly INowService _now = now; + private readonly IScoreDenormalizationService _scoringDenormalizationService = scoringDenormalizationService; + private readonly IScoringService _scoringService = scoringService; + private readonly IStore _store = store; + private readonly ITeamService _teamService = teamService; + private readonly IValidatorService _validatorService = validatorService; public async Task Handle(GetScoreboardQuery request, CancellationToken cancellationToken) { @@ -63,17 +51,6 @@ await _validatorService var teams = await LoadDenormalizedTeams(request.GameId, cancellationToken); - // if the teams aren't in the denormalized table, it's probably because this is an older game - // that denormalized data hasn't been generated for yet. Do it here: - if (!teams.Any()) - { - // force the game to rerank - await _scoringDenormalizationService.DenormalizeGame(request.GameId, cancellationToken); - - // then pull teams again - teams = await LoadDenormalizedTeams(request.GameId, cancellationToken); - } - // we grab competitive players only becaause later we filter out teams that have no competitive-mode players // (since the scoreboard isn't really for practice mode) var teamPlayers = await _store From ef2cadbef83fddccc3c226140ba5a5a373dcf806 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Tue, 21 Jan 2025 13:18:27 -0500 Subject: [PATCH 4/8] Add tickets to event horizon --- .../GetTeamEventHorizon/GetTeamEventHorizon.cs | 12 ++++++------ .../GetTeamEventHorizon/GetTeamEventHorizonModels.cs | 1 + src/Gameboard.Api/Features/Ticket/TicketService.cs | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizon.cs b/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizon.cs index 16d7b4a1..637fc597 100644 --- a/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizon.cs +++ b/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizon.cs @@ -8,6 +8,7 @@ using Gameboard.Api.Features.Scores; using Gameboard.Api.Features.Support; using Gameboard.Api.Features.Users; +using Gameboard.Api.Services; using Gameboard.Api.Structure.MediatR; using Gameboard.Api.Structure.MediatR.Validators; using MediatR; @@ -26,6 +27,7 @@ internal class GetTeamEventHorizonHandler IStore store, TeamExistsValidator teamExists, ITeamService teamService, + TicketService ticketService, IValidatorService validator ) : IRequestHandler { @@ -36,6 +38,7 @@ IValidatorService validator private readonly IStore _store = store; private readonly TeamExistsValidator _teamExists = teamExists; private readonly ITeamService _teamService = teamService; + private readonly TicketService _ticketService = ticketService; private readonly IValidatorService _validator = validator; public async Task Handle(GetTeamEventHorizonQuery request, CancellationToken cancellationToken) @@ -302,7 +305,7 @@ private async Task BuildTicketOpenClosedEven var challengeTickets = await _store .WithNoTracking() .Where(t => t.ChallengeId != null && t.ChallengeId != string.Empty && t.TeamId == teamId) - .Select(t => new { t.Id, t.Status, t.Created, t.ChallengeId, Activity = t.Activity.Select(a => new { a.Id, a.Timestamp, a.Status }).Distinct().OrderBy(a => a.Status).ToList() }) + .Select(t => new { t.Id, t.Status, t.Created, t.ChallengeId, t.Key, Activity = t.Activity.Select(a => new { a.Id, a.Timestamp, a.Status }).Distinct().OrderBy(a => a.Status).ToList() }) .GroupBy(a => a.ChallengeId) .ToDictionaryAsync(gr => gr.Key, gr => gr.ToList(), cancellationToken); @@ -311,9 +314,6 @@ private async Task BuildTicketOpenClosedEven // for each challenge, find its tickets and build events for them foreach (var challengeId in challengeTickets.Keys) { - // we auto-create an open event for each challenge group, because: - // - The challenge is guaranteed to have at least one ticket if it comes back from the query - // - new tickets don't get an Activity entry just for opening, so we need to simulate that here var tickets = challengeTickets[challengeId]; foreach (var ticket in tickets) @@ -327,7 +327,8 @@ private async Task BuildTicketOpenClosedEven Type = EventHorizonEventType.TicketOpenClose, EventData = new EventHorizonTicketOpenClosedEventData { - ClosedAt = default + ClosedAt = default, + TicketKey = _ticketService.GetFullKey(ticket.Key), } }; events.Add(creatingEvent); @@ -345,7 +346,6 @@ private async Task BuildTicketOpenClosedEven } } } - } return [.. events]; diff --git a/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizonModels.cs b/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizonModels.cs index 7a0b82b8..7493c5a2 100644 --- a/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizonModels.cs +++ b/src/Gameboard.Api/Features/Teams/Requests/GetTeamEventHorizon/GetTeamEventHorizonModels.cs @@ -76,6 +76,7 @@ public sealed class EventHorizonTicketOpenCloseEvent : EventHorizonEvent, IEvent public sealed class EventHorizonTicketOpenClosedEventData { public required DateTimeOffset ClosedAt { get; set; } + public required string TicketKey { get; set; } } public sealed class EventHorizonGame diff --git a/src/Gameboard.Api/Features/Ticket/TicketService.cs b/src/Gameboard.Api/Features/Ticket/TicketService.cs index 453b84ed..c66f1827 100644 --- a/src/Gameboard.Api/Features/Ticket/TicketService.cs +++ b/src/Gameboard.Api/Features/Ticket/TicketService.cs @@ -11,7 +11,6 @@ using Gameboard.Api.Common.Services; using Gameboard.Api.Data; using Gameboard.Api.Features.Support; -using Gameboard.Api.Features.Teams; using Gameboard.Api.Features.Users; using Gameboard.Api.Hubs; using MediatR; From f6859f115a0ba3e2885e89e3ac17119c1ec4e738 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Tue, 21 Jan 2025 15:35:04 -0500 Subject: [PATCH 5/8] Fix certificate retrieval bugs for players who had nonscoring games --- .../Features/Certificates/CertificatesService.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Gameboard.Api/Features/Certificates/CertificatesService.cs b/src/Gameboard.Api/Features/Certificates/CertificatesService.cs index 4eb1cbdb..b79880e3 100644 --- a/src/Gameboard.Api/Features/Certificates/CertificatesService.cs +++ b/src/Gameboard.Api/Features/Certificates/CertificatesService.cs @@ -78,11 +78,10 @@ public async Task> GetCompetitiveCertifi .WithNoTracking() .Where ( - p => p.UserId == userId && - p.SessionEnd > DateTimeOffset.MinValue && - p.Game.PlayerMode == PlayerMode.Competition && - (p.Game.GameEnd < now || p.Game.GameEnd == DateTimeOffset.MinValue) && - p.Game.CertificateTemplateId != null + p => + p.UserId == userId + && (p.Game.GameEnd < now || p.Game.GameEnd == DateTimeOffset.MinValue) + // && p.Game.CertificateTemplateId != null ) .WhereDateIsNotEmpty(p => p.SessionEnd) .Where(p => p.Challenges.All(c => c.PlayerMode == PlayerMode.Competition)) @@ -159,7 +158,7 @@ public async Task> GetCompetitiveCertifi MaxPossibleScore = t.Game.MaxPossibleScore }, Date = t.Game.GameEnd.IsEmpty() ? captain.SessionEnd : t.Game.GameEnd, - Rank = score.Rank > 0 ? score.Rank : null, + Rank = score?.Rank ?? 0, Score = score is not null ? score.ScoreOverall : 0, Duration = TimeSpan.FromMilliseconds(t.Time), UniquePlayerCount = participationCounts?.UniquePlayerCount, From 1c924d7c5549b3ed406b9b3ec4edcd4eb7905d6d Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Tue, 21 Jan 2025 15:39:16 -0500 Subject: [PATCH 6/8] Restore certificates query --- src/Gameboard.Api/Features/Certificates/CertificatesService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gameboard.Api/Features/Certificates/CertificatesService.cs b/src/Gameboard.Api/Features/Certificates/CertificatesService.cs index b79880e3..995d1c24 100644 --- a/src/Gameboard.Api/Features/Certificates/CertificatesService.cs +++ b/src/Gameboard.Api/Features/Certificates/CertificatesService.cs @@ -81,7 +81,7 @@ public async Task> GetCompetitiveCertifi p => p.UserId == userId && (p.Game.GameEnd < now || p.Game.GameEnd == DateTimeOffset.MinValue) - // && p.Game.CertificateTemplateId != null + && p.Game.CertificateTemplateId != null ) .WhereDateIsNotEmpty(p => p.SessionEnd) .Where(p => p.Challenges.All(c => c.PlayerMode == PlayerMode.Competition)) From 2c141e2f21d9a783a39ad751cf482cac0f53ed75 Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Tue, 21 Jan 2025 15:44:47 -0500 Subject: [PATCH 7/8] Remove legacy ranking code --- .../Features/Game/GameController.cs | 1 - .../Features/Game/GameService.cs | 29 ------------------- .../TeamSessionResetNotification.cs | 16 +++------- 3 files changed, 4 insertions(+), 42 deletions(-) diff --git a/src/Gameboard.Api/Features/Game/GameController.cs b/src/Gameboard.Api/Features/Game/GameController.cs index 68498dec..a9c0696f 100644 --- a/src/Gameboard.Api/Features/Game/GameController.cs +++ b/src/Gameboard.Api/Features/Game/GameController.cs @@ -244,7 +244,6 @@ public async Task Rerank([FromRoute] string id, CancellationToken cancellationTo await Authorize(_permissionsService.Can(PermissionKey.Scores_RegradeAndRerank)); await Validate(new Entity { Id = id }); - await GameService.ReRank(id); await _scoreDenormalization.DenormalizeGame(id, cancellationToken); await _mediator.Publish(new GameCacheInvalidateNotification(id), cancellationToken); } diff --git a/src/Gameboard.Api/Features/Game/GameService.cs b/src/Gameboard.Api/Features/Game/GameService.cs index c361c1b5..aa31ca87 100644 --- a/src/Gameboard.Api/Features/Game/GameService.cs +++ b/src/Gameboard.Api/Features/Game/GameService.cs @@ -23,7 +23,6 @@ public interface IGameService Task IsUserPlaying(string gameId, string userId); Task> List(GameSearchFilter model = null, bool sudo = false); Task ListGrouped(GameSearchFilter model, bool sudo); - Task ReRank(string id); Task Retrieve(string id, bool accessHidden = true); Task RetrieveChallengeSpecs(string id); Task SessionForecast(string id); @@ -213,34 +212,6 @@ public async Task UpdateImage(string id, string type, string filename) await _store.SaveUpdate(entity, default); } - public async Task ReRank(string id) - { - var players = await _store - .WithTracking() - .Where(p => p.GameId == id && p.Mode == PlayerMode.Competition) - .OrderByDescending(p => p.Score) - .ThenBy(p => p.Time) - .ThenByDescending(p => p.CorrectCount) - .ThenByDescending(p => p.PartialCount) - .ToArrayAsync() - ; - - int rank = 0; - string last = ""; - foreach (var player in players) - { - if (player.TeamId != last) - { - rank += 1; - last = player.TeamId; - } - - player.Rank = rank; - } - - await _store.SaveUpdateRange(players); - } - public Task IsUserPlaying(string gameId, string userId) => _store.AnyAsync(p => p.GameId == gameId && p.UserId == userId, CancellationToken.None); diff --git a/src/Gameboard.Api/Features/Teams/Notifications/TeamSessionResetNotification.cs b/src/Gameboard.Api/Features/Teams/Notifications/TeamSessionResetNotification.cs index 3d51145b..d9b76a1d 100644 --- a/src/Gameboard.Api/Features/Teams/Notifications/TeamSessionResetNotification.cs +++ b/src/Gameboard.Api/Features/Teams/Notifications/TeamSessionResetNotification.cs @@ -1,26 +1,18 @@ using System.Threading; using System.Threading.Tasks; -using Gameboard.Api.Services; +using Gameboard.Api.Features.Scores; using MediatR; namespace Gameboard.Api.Features.Teams; public record TeamSessionResetNotification(string GameId, string TeamId) : INotification; -internal class TeamSessionResetHandler : INotificationHandler +internal class TeamSessionResetHandler(IScoreDenormalizationService scoreDenorm) : INotificationHandler { - private readonly GameService _gameService; - - public TeamSessionResetHandler - ( - GameService gameService - ) - { - _gameService = gameService; - } + private readonly IScoreDenormalizationService _scoreDenorm = scoreDenorm; public async Task Handle(TeamSessionResetNotification notification, CancellationToken cancellationToken) { - await _gameService.ReRank(notification.GameId); + await _scoreDenorm.DenormalizeGame(notification.GameId, cancellationToken); } } From f7339dc9e12d35ef8b7b63d3eafb040b79ba456e Mon Sep 17 00:00:00 2001 From: Ben Stein Date: Tue, 21 Jan 2025 16:21:37 -0500 Subject: [PATCH 8/8] Remove legacy property in ticket service --- src/Gameboard.Api/Features/Ticket/TicketService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Gameboard.Api/Features/Ticket/TicketService.cs b/src/Gameboard.Api/Features/Ticket/TicketService.cs index c66f1827..e5313ab9 100644 --- a/src/Gameboard.Api/Features/Ticket/TicketService.cs +++ b/src/Gameboard.Api/Features/Ticket/TicketService.cs @@ -46,7 +46,6 @@ ISupportHubBus supportHubBus private readonly ISupportHubBus _supportHubBus = supportHubBus; internal static char TAGS_DELIMITER = ' '; - public static readonly string OpenStatus = "Open"; public string GetFullKey(int key) => $"{(Options.KeyPrefix.IsEmpty() ? "GB" : Options.KeyPrefix)}-{key}";