From 301fd46b580b739a0f4f10abad66ba372ce6e2d0 Mon Sep 17 00:00:00 2001 From: Ben Stein <115497763+sei-bstein@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:34:10 -0500 Subject: [PATCH] v3.25.1 (#560) * Fix a bug that could cause the challenges report to throw errors when retrieving performance for some challenges. * Fix a permissions issue with game card image changing * Misc cleanup --- .../GetCompetitiveModeCertificateHtml.cs | 34 ++++++--------- .../GetPracticeModeCertificateHtml.cs | 6 ++- .../Services/ChallengeSyncService.cs | 42 +++++++------------ .../Features/ChallengeSpec/ChallengeSpec.cs | 5 +++ .../ChallengeSpec/ChallengeSpecController.cs | 5 ++- .../ChallengeSpec/ChallengeSpecService.cs | 10 +++-- .../Features/Game/GameService.cs | 4 +- .../Features/Player/Services/PlayerService.cs | 4 +- 8 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/Gameboard.Api/Features/Certificates/Requests/GetCompetitiveModeCertificateHtml.cs b/src/Gameboard.Api/Features/Certificates/Requests/GetCompetitiveModeCertificateHtml.cs index 33277837..06509068 100644 --- a/src/Gameboard.Api/Features/Certificates/Requests/GetCompetitiveModeCertificateHtml.cs +++ b/src/Gameboard.Api/Features/Certificates/Requests/GetCompetitiveModeCertificateHtml.cs @@ -12,32 +12,24 @@ namespace Gameboard.Api.Features.Certificates; public record GetCompetitiveModeCertificateHtmlQuery(string GameId, string OwnerUserId, string ActingUserId) : IRequest; -internal class GetCompetitiveModeCertificateHtmlHandler : IRequestHandler +internal class GetCompetitiveModeCertificateHtmlHandler +( + EntityExistsValidator gameExists, + PlayerService playerService, + IStore store, + IValidatorService validatorService +) : IRequestHandler { - private readonly EntityExistsValidator _gameExists; - private readonly PlayerService _playerService; - private readonly IStore _store; - private readonly IValidatorService _validatorService; - - public GetCompetitiveModeCertificateHtmlHandler - ( - EntityExistsValidator gameExists, - PlayerService playerService, - IStore store, - IValidatorService validatorService - ) - { - _gameExists = gameExists; - _playerService = playerService; - _store = store; - _validatorService = validatorService; - } + private readonly EntityExistsValidator _gameExists = gameExists; + private readonly PlayerService _playerService = playerService; + private readonly IStore _store = store; + private readonly IValidatorService _validatorService = validatorService; public async Task Handle(GetCompetitiveModeCertificateHtmlQuery request, CancellationToken cancellationToken) { var isPublished = await _store .WithNoTracking() - .AnyAsync(c => c.GameId == request.GameId && c.OwnerUserId == request.OwnerUserId); + .AnyAsync(c => c.GameId == request.GameId && c.OwnerUserId == request.OwnerUserId, cancellationToken); await _validatorService .AddValidator(_gameExists.UseProperty(r => r.GameId)) @@ -54,7 +46,7 @@ await _validatorService .WithNoTracking() .Where(p => p.UserId == request.OwnerUserId) .Where(p => p.GameId == request.GameId) - .FirstAsync(); + .FirstAsync(cancellationToken); var certificate = await _playerService.MakeCertificate(player.Id); return certificate.Html; diff --git a/src/Gameboard.Api/Features/Certificates/Requests/GetPracticeModeCertificateHtml/GetPracticeModeCertificateHtml.cs b/src/Gameboard.Api/Features/Certificates/Requests/GetPracticeModeCertificateHtml/GetPracticeModeCertificateHtml.cs index d8fa373c..de533068 100644 --- a/src/Gameboard.Api/Features/Certificates/Requests/GetPracticeModeCertificateHtml/GetPracticeModeCertificateHtml.cs +++ b/src/Gameboard.Api/Features/Certificates/Requests/GetPracticeModeCertificateHtml/GetPracticeModeCertificateHtml.cs @@ -49,10 +49,12 @@ await _validatorService .Validate(request, cancellationToken); if (certificate is null) + { throw new ResourceNotFound(request.ChallengeSpecId, $"Couldn't resolve a certificate for owner {request.CertificateOwnerUserId} and challenge spec {request.ChallengeSpecId}"); + } - // load the outer template from this application (this is custom crafted by us to ensure we end up) - // with a consistent HTML-compliant base + // load the outer template from this application (this is custom crafted by us to ensure we end up + // with a consistent HTML-compliant base) var outerTemplatePath = Path.Combine(_coreOptions.TemplatesDirectory, "practice-certificate.template.html"); var outerTemplate = File.ReadAllText(outerTemplatePath); diff --git a/src/Gameboard.Api/Features/Challenge/Services/ChallengeSyncService.cs b/src/Gameboard.Api/Features/Challenge/Services/ChallengeSyncService.cs index e9729b5d..57363868 100644 --- a/src/Gameboard.Api/Features/Challenge/Services/ChallengeSyncService.cs +++ b/src/Gameboard.Api/Features/Challenge/Services/ChallengeSyncService.cs @@ -23,32 +23,22 @@ public interface IChallengeSyncService /// /// Used by the Job service to update challenges which have expired /// -internal class ChallengeSyncService : IChallengeSyncService +internal class ChallengeSyncService +( + ConsoleActorMap consoleActorMap, + IGameEngineService gameEngine, + ILogger logger, + IMapper mapper, + INowService now, + IStore store +) : IChallengeSyncService { - private readonly ConsoleActorMap _consoleActorMap; - private readonly IGameEngineService _gameEngine; - private readonly ILogger _logger; - private readonly IMapper _mapper; - private readonly INowService _now; - private readonly IStore _store; - - public ChallengeSyncService - ( - ConsoleActorMap consoleActorMap, - IGameEngineService gameEngine, - ILogger logger, - IMapper mapper, - INowService now, - IStore store - ) - { - _consoleActorMap = consoleActorMap; - _logger = logger; - _gameEngine = gameEngine; - _mapper = mapper; - _now = now; - _store = store; - } + private readonly ConsoleActorMap _consoleActorMap = consoleActorMap; + private readonly IGameEngineService _gameEngine = gameEngine; + private readonly ILogger _logger = logger; + private readonly IMapper _mapper = mapper; + private readonly INowService _now = now; + private readonly IStore _store = store; public Task Sync(Data.Challenge challenge, GameEngineGameState state, string actingUserId, CancellationToken cancellationToken) => Sync(cancellationToken, new SyncEntry(actingUserId, challenge, state)); @@ -104,7 +94,7 @@ public async Task SyncExpired(CancellationToken cancellationToken) .Where(p => playerIds.Contains(p.Id)) .ToDictionaryAsync(p => p.Id, p => p.SessionEnd, cancellationToken); - _logger.LogInformation($"The ChallengeSyncService is synchronizing {challenges.Count()} challenges..."); + _logger.LogInformation("The ChallengeSyncService is synchronizing {syncCount} challenges...", challenges.Count()); foreach (var challenge in challenges) { try diff --git a/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpec.cs b/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpec.cs index b396c058..3d8ed4c5 100644 --- a/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpec.cs +++ b/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpec.cs @@ -81,6 +81,11 @@ public sealed class ChallengeSpecSummary public required IEnumerable Tags { get; set; } } +public sealed class ChallengeSpecSectionPerformance +{ + public required string Text { get; set; } +} + public sealed class ChallengeSpecQuestionPerformance { public required int QuestionRank { get; set; } diff --git a/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpecController.cs b/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpecController.cs index a3d42efd..5188b310 100644 --- a/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpecController.cs +++ b/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpecController.cs @@ -17,7 +17,8 @@ namespace Gameboard.Api.Controllers; [Authorize] -public class ChallengeSpecController( +public class ChallengeSpecController +( IActingUserService actingUserService, ILogger logger, IDistributedCache cache, @@ -25,7 +26,7 @@ public class ChallengeSpecController( ChallengeSpecValidator validator, ChallengeSpecService challengespecService, IUserRolePermissionsService permissionsService - ) : GameboardLegacyController(actingUserService, logger, cache, validator) +) : GameboardLegacyController(actingUserService, logger, cache, validator) { private readonly IMediator _mediator = mediator; private readonly IUserRolePermissionsService _permissionsService = permissionsService; diff --git a/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpecService.cs b/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpecService.cs index c225140a..632d9981 100644 --- a/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpecService.cs +++ b/src/Gameboard.Api/Features/ChallengeSpec/ChallengeSpecService.cs @@ -117,8 +117,6 @@ public async Task> ListByGame() public async Task> GetQuestionPerformance(string challengeSpecId, CancellationToken cancellationToken) { var results = await GetQuestionPerformance([challengeSpecId], cancellationToken); - if (!results.Any()) - throw new ArgumentException($"Couldn't load performance for specId {challengeSpecId}", nameof(challengeSpecId)); return results[challengeSpecId]; } @@ -151,7 +149,13 @@ public async Task v.Any())) - return null; + { + return challengeSpecIds.ToDictionary + ( + specId => specId, + specId => Array.Empty().OrderBy(c => c.CountCorrect) + ); + } // deserialize states var questionPerformance = new Dictionary>(); diff --git a/src/Gameboard.Api/Features/Game/GameService.cs b/src/Gameboard.Api/Features/Game/GameService.cs index 3f30aec8..e66d4132 100644 --- a/src/Gameboard.Api/Features/Game/GameService.cs +++ b/src/Gameboard.Api/Features/Game/GameService.cs @@ -329,7 +329,9 @@ public async Task DeleteGameCardImage(string gameId) var files = Directory.GetFiles(Options.ImageFolder, fileSearchPattern); foreach (var cardImageFile in files) + { File.Delete(cardImageFile); + } await UpdateImage(gameId, "card", string.Empty); } @@ -342,7 +344,7 @@ public async Task SaveGameCardImage(string gameId, IFormFile file) var fileName = $"{GetGameCardFileNameBase(gameId)}{Path.GetExtension(file.FileName.ToLower())}"; var path = Path.Combine(Options.ImageFolder, fileName); - using var stream = new FileStream(path, FileMode.Create); + using var stream = new FileStream(path, FileMode.OpenOrCreate); await file.CopyToAsync(stream); await UpdateImage(gameId, "card", fileName); diff --git a/src/Gameboard.Api/Features/Player/Services/PlayerService.cs b/src/Gameboard.Api/Features/Player/Services/PlayerService.cs index aab37897..7c535ab4 100644 --- a/src/Gameboard.Api/Features/Player/Services/PlayerService.cs +++ b/src/Gameboard.Api/Features/Player/Services/PlayerService.cs @@ -681,7 +681,7 @@ private PlayerCertificate CertificateFromTemplate(Data.Player player, int player certificateHTML = certificateHTML.Replace("{{player_count}}", playerCount.ToString()); certificateHTML = certificateHTML.Replace("{{team_count}}", teamCount.ToString()); - return new Api.PlayerCertificate + return new PlayerCertificate { Game = _mapper.Map(player.Game), PublishedOn = player.User.PublishedCompetitiveCertificates.FirstOrDefault(c => c.GameId == player.Game.Id)?.PublishedOn, @@ -726,7 +726,7 @@ private async Task RegisterPracticeSession(NewPlayer model, Data.User us { int count = await _store .WithNoTracking() - .CountAsync + .CountAsync ( p => p.Mode == PlayerMode.Practice &&