Skip to content

Commit

Permalink
v3.10.1-beta0 (#245)
Browse files Browse the repository at this point in the history
* Initial groundwork for reports rewrite.

* Add reports service and fix service registration tech debt.

* More iteration on reports revamp

* Checkpoint for reports revamp

* More reports backend

* Use challenge specs instead of challenges for reporting.

* More reports plumbing

* More report plumbing

* Add support report to db

* Work on support report

* Polishing support report

* Add reports and initial unit tests

* More work on reports

* Refactor - move SimpleEntity into Gameboard.Api.Features.Common.

* Don't bootstrap JobsService in integration test env

* Address an issue where Gameboard was incorrectly evaluating whether a challenge has a deployed gamespace after destroy. Resolves #182.

* Add registration date range to challenges report

* Fix player report bugs

* Server-side hardening for illegal mime type upload. Removed bare repositories.
Updated recommended extensions.

* Add improved file upload validation. Add editor config to suppress private member warnings.

* Update unit tests

* Remove server-side html escaping of user input (handled client side).

* Resolved an issue that prevented assignees from being display in the support ticket list.

* Remove unnecessary dependency injection from the app hub.

* Fix build error in ticket service

* Fix merge issue from main

* Resolve changes from the merge

* Initial version of enrollment report and refactor

* Add minimal unit tests for enrollment report

* Additional refinements to enrollment report

* Remove gamebrain schema generator (no longer needed)

* Add WhenCreated to player table.

* Added export for enrollment report.

* Remove unnecessary pragma warning for new C# tools, unbreak player service tests.

* More work on enrollment report

* Clarify comments on Enrollment report query

* Fix line chart for enrollment, update validation strategies, add new query extension to deal with nullish dates.

* Page to default values when illegal paging is requested. Add Challenges Attempted to enrollment csv export.

* Refactor of practice backend

* Add new user fields for created on, last login date, and login count. Pruned unused code and store implementations that weren't really doing much. Resolves #196.

* Rename reports folder

* Add tests for query extensions

* Started work on practice report

* Code cleanup

* Fix failing integration tests

* Fix a bug with date processing for the enrollment report

* Work on practice mode report

* Add metadata query

* Improvements to practice report

* Update practice report models

* Add player performance tab to practice mode report

* Wrapping up a draft of practice mode

* Fix integration test issues

* Direct dotnet restore command in Github Action to src

* Update GH actions config for correct solutions file config.

* Delete extra sln file

* Fix stackoverflow in captain resolution

* Improvements to support report and solution tooling.

* Improvements to support report filters

* Code cleanup in engine service

* Automatically sort string filters for reports

* fixed an issue with the sponsor filter on the enrollment report

* Guard against orphaned specIds in the practice mode report

* Add games filter to enrollment service.

* Guard against unexpected nulls in practice mode report

* Guard against unexpected sponsor nulls in practice mode report

* Fix criteria processing for practice mode report

* PlayerMode to challenges

- Add "PlayerMode" to challenges. Resolves #218
- Sort support service by created date by default.

* Query practice mode report based on the challenge-level PlayerMode flag rather than the game-level one.

* Code cleanup in game engine service

* Code cleanup

* Fix PlayerService constructor signature to remove unused injections

* Code cleanup

* Align parameter names

* Add stat summary for enrollment report

* Code cleanup

* Fix build

* Guard against null sponsors in enrollment report. Hide practice games on home screen.

* Present percentile for Practice Mode report as a 0-100 value (up from 0-1).

* Add challenge result to practice mode report

* Optimize sponsor count stat for the enrollment report

* Code cleanup

* Remove unused property of game model

* Improvements to practice mode workflow (allow detection of active practice session

* Change model shape for user active practice challenge.

* Enhance information available for an active practice challenge to support new launch path.

* Improvement to practice mode/playcomponent

* More support for auto-deploy of practice challenges/ new 'play' component.

* More work on autolaunch/play component

* Finish autolaunch. Resolves #227

* Fix a bug in practice report where player count per sponsor was not computed correctly.

* Fix test game engine service interface sig

* Fix failing test

* Corrected resolution of json options for integration tests.

* Code cleanup

* Allow API key authentication to resolve grader keys as well as standard user api keys.

- Moved GetUserFromApiKey to service level
- Added tests to verify resolution of user and grader keys
- Resolved an issue where integration tests were inadvertently depending on our internal test Gamebrain instance

* set default solution path

* Additional test for grader/apikey authentication.

* Revert "Additional test for grader/apikey authentication."

This reverts commit b75727d.

* Additional test for grader api key resolution.

* Added revised grader key authentication and modified _Controller to represent an authenticated grader key separately from an authenticated user.

* Remove incorrect tests

* Update game engine mapper to reflect existing mapping in ChallengeMaps between Vm count and IsActive

* Add practice admin stuff

* Improvements to practice admin

* Set default topo timeout to 300 sec (up from 100).

* Finished up practice certificates

* Fixed broken constructor sig intests

* Fixed hours/minutes display for practice certificates and added supporting unit tests.

* Always archive team challenges on reset

* Removed test (the rule is no longer true)

* Playing with various pdf rendering options?

* Various code cleanup

* Do server-side rendering of certificates in PNG format.

* Update tests

* Finish server side generation of html for both practice and competitive.

* Fix test signatures

* Make published certificate collections on User default to empty lists

* Bug fixes for various practice mode session end things.

* Test fixes

* Bug fixes to practice mode.

* Fix test types

* Update dockerfile for chromium install.

* Update Dockerfile - move chromium install to production multistage.

* Accommodate docker execution of chromium with --no-sandbox

* Disable use of dev shared memory for chromium image generation.

* Request full challenge doc from Topomojo during spec sync.

* Fix for challenge doc text in practice mode

* Try wkhtmltopdf as alternate image generation tool.

* Use correct version of wkhtmltopdf binary

* Remove stray pdf

* Decrease quality of images rendered by wkhtmltoimage

* Set wkhtmltoimage quality to 30

* Rename player-facing 'Practice Mode' to 'Practice Area'

* Fix practice area report routes

* Fix count issue on the player prac/competitive report

* Fix a count issue on the 'by challenge' grouping of the practice area report.

* allow authenticated users to retrieve practice mode settings

* Fix grouping issue with per-mode practice area report

* Code cleanup

* Fix typo in Enrollment report desc

* Correct calculation of users per sponsor in Enrollment report.

* Change roles who can access reporting.

* Improve summary info on report cards to better reflect the corresponding reports.

* - corrected an issue that caused practice challenges to appear as though they were created a million years ago in challenge admin.
- The search box on Challenge Admin now automatically trims whitespace.
(Resolves #247)

* Resolved an issue which caused the enrollment report to fail to report challenges assigned to games which are in practice mode (even if the challenge was played competitively.

* Fix failing unit test and add a new one for prac/comp challenge/game fiasco on Enrollment report.

* Resolved an issue that prevented non-registrars from extending/resetting their practice sessions.
  • Loading branch information
sei-bstein authored Sep 7, 2023
1 parent c13f45a commit 0c95d19
Show file tree
Hide file tree
Showing 27 changed files with 198 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ await _testContext.WithDataState(state =>
state.AddUser(u =>
{
u.Id = userId;
u.Role = Api.UserRole.Support;
u.Role = UserRole.Support;
});
state.AddChallenge(c =>
Expand All @@ -41,7 +41,7 @@ await _testContext.WithDataState(state =>
var httpClient = _testContext.CreateHttpClientWithActingUser(u =>
{
u.Id = userId;
u.Role = Api.UserRole.Support;
u.Role = UserRole.Support;
});

// when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ private PlayerServiceTestable(
INowService now,
ITeamService teamService,
IMapper mapper,
IMemoryCache localCache,
GameEngineService gameEngine) : base
IMemoryCache localCache) : base
(
challengeService,
coreOptions,
Expand All @@ -41,8 +40,7 @@ private PlayerServiceTestable(
practiceService,
teamService,
mapper,
localCache,
gameEngine
localCache
)
{
}
Expand Down Expand Up @@ -78,8 +76,7 @@ internal static PlayerService GetTestable(
practiceService ?? A.Fake<IPracticeService>(),
teamService ?? A.Fake<ITeamService>(),
mapper ?? A.Fake<IMapper>(),
localCache ?? A.Fake<IMemoryCache>(),
gameEngine ?? A.Fake<GameEngineService>()
localCache ?? A.Fake<IMemoryCache>()
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Gameboard.Api.Features.Teams;
using Gameboard.Api.Hubs;
using Gameboard.Api.Services;
using Microsoft.Extensions.Caching.Memory;

namespace Gameboard.Api.Tests.Unit;

Expand All @@ -14,7 +15,14 @@ public async Task Standings_WhenGameIdIsEmpty_ReturnsEmptyArray()
// arrange
var playerStore = A.Fake<IPlayerStore>();
var mapper = A.Fake<IMapper>();
var sut = new TeamService(A.Fake<IMapper>(), A.Fake<INowService>(), A.Fake<IInternalHubBus>(), playerStore);
var sut = new TeamService
(
A.Fake<IMapper>(),
A.Fake<IMemoryCache>(),
A.Fake<INowService>(),
A.Fake<IInternalHubBus>(),
playerStore
);

var players = new Data.Player[]
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Gameboard.Api.Data;
using Gameboard.Api.Features.Reports;
using Gameboard.Api.Services;

namespace Gameboard.Api.Tests.Unit;

Expand All @@ -23,6 +22,7 @@ public async Task GetResults_WithOnePlayerAndChallenge_ReportsCompleteSolve(IFix
var challenge = fixture.Create<Data.Challenge>();
challenge.Points = 50;
challenge.Score = 50;
challenge.PlayerMode = PlayerMode.Competition;

var player = fixture.Create<Data.Player>();
player.Challenges = new Data.Challenge[] { challenge };
Expand Down Expand Up @@ -75,6 +75,7 @@ public async Task GetResults_WithOneTeamRecord_ReportsExpectedValues(IFixture fi
var challenge = fixture.Create<Data.Challenge>();
challenge.Points = 50;
challenge.Score = 50;
challenge.PlayerMode = PlayerMode.Competition;

var player1 = fixture.Create<Data.Player>();
player1.Challenges = new Data.Challenge[] { challenge };
Expand All @@ -83,6 +84,7 @@ public async Task GetResults_WithOneTeamRecord_ReportsExpectedValues(IFixture fi
player1.Role = PlayerRole.Manager;

var player2 = fixture.Create<Data.Player>();
player2.Challenges = new Data.Challenge[] { challenge };
player2.Game = player1.Game;
player2.GameId = player1.GameId;
player2.TeamId = player1.TeamId;
Expand All @@ -109,4 +111,48 @@ public async Task GetResults_WithOneTeamRecord_ReportsExpectedValues(IFixture fi
results.Records.First().Team.Sponsors.Count().ShouldBe(2);
results.Records.SelectMany(r => r.Challenges).DistinctBy(c => c.SpecId).Count().ShouldBe(1);
}

[Theory, GameboardAutoData]
public async Task GetResults_WithGameInPracAndChallengeInComp_ReportsOneResult(IFixture fixture)
{
// given
var sponsors = new List<Data.Sponsor>
{
new Data.Sponsor
{
Id = "good-people",
Name = "The Good People",
Logo = "good-people.jpg"
}
}.BuildMock();

var challenge = fixture.Create<Data.Challenge>();
challenge.Points = 50;
challenge.Score = 50;
challenge.PlayerMode = PlayerMode.Competition;

var player = fixture.Create<Data.Player>();
player.Challenges = new Data.Challenge[] { challenge };
player.Game.PlayerMode = PlayerMode.Practice;
player.Sponsor = sponsors.First().Logo;

var players = new List<Data.Player> { player }.BuildMock();

var reportsService = A.Fake<IReportsService>();
A.CallTo(() => reportsService.ParseMultiSelectCriteria(string.Empty))
.WithAnyArguments()
.Returns(Array.Empty<string>());

var store = A.Fake<IStore>();
A.CallTo(() => store.List<Data.Sponsor>(false)).Returns(sponsors);
A.CallTo(() => store.List<Data.Player>(false)).Returns(players);

var sut = new EnrollmentReportService(reportsService, store);

// when
var results = await sut.GetRawResults(new EnrollmentReportParameters(), CancellationToken.None);

// then
results.Records.Count().ShouldBe(1);
}
}
5 changes: 2 additions & 3 deletions src/Gameboard.Api/Data/Store/Store[TEntity].cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,11 @@ public virtual IQueryable<TEntity> List(string term = null)
}

public IQueryable<TEntity> ListWithNoTracking()
=> DbContext.
Set<TEntity>()
=> DbContext
.Set<TEntity>()
.AsNoTracking()
.AsQueryable();


public virtual async Task<TEntity> Create(TEntity entity)
{
if (string.IsNullOrWhiteSpace(entity.Id))
Expand Down
5 changes: 3 additions & 2 deletions src/Gameboard.Api/Features/Challenge/ChallengeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public async Task<bool> UserIsTeamPlayer(string id, string subjectId)

public async Task<ChallengeSummary[]> List(SearchFilter model = null)
{
var q = Store.List(model?.Term ?? null);
var q = Store.List(model?.Term?.Trim() ?? null);

// filter out challenge records with no state used to give starting score to player
q = q.Where(p => p.Name != "_initialscore_" && p.State != null);
Expand Down Expand Up @@ -638,8 +638,9 @@ int variant
challenge.State = _jsonService.Serialize(state);
challenge.StartTime = state.StartTime;
challenge.EndTime = state.EndTime;
challenge.LastSyncTime = _now.Get();

challenge.Events.Add(new Data.ChallengeEvent
challenge.Events.Add(new ChallengeEvent
{
Id = _guids.GetGuid(),
UserId = actorUserId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ public async Task<GameEngineGameState> RegisterGamespace(GameEngineChallengeRegi
{
Players = new RegistrationPlayer[]
{
new RegistrationPlayer
{
new() {
SubjectId = registration.Player.TeamId,
SubjectName = registration.Player.ApprovedName
}
Expand Down
3 changes: 1 addition & 2 deletions src/Gameboard.Api/Features/Player/PlayerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
Expand Down Expand Up @@ -143,7 +142,7 @@ public async Task UpdateSession([FromBody] SessionChangeRequest model, Cancellat

AuthorizeAny(
() => Actor.IsRegistrar,
() => IsSelf(model.TeamId).Result
() => TeamService.IsOnTeam(model.TeamId, Actor.Id).Result
);

await PlayerService.AdjustSessionEnd(model, Actor, cancellationToken);
Expand Down
6 changes: 1 addition & 5 deletions src/Gameboard.Api/Features/Player/PlayerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using AutoMapper;
using Gameboard.Api.Data;
using Gameboard.Api.Data.Abstractions;
using Gameboard.Api.Features.GameEngine;
using Gameboard.Api.Features.Games;
using Gameboard.Api.Features.Player;
using Gameboard.Api.Features.Practice;
Expand Down Expand Up @@ -37,7 +36,6 @@ public class PlayerService
ITeamService TeamService { get; }
IMapper Mapper { get; }
IMemoryCache LocalCache { get; }
IGameEngineService GameEngine { get; }

public PlayerService(
ChallengeService challengeService,
Expand All @@ -52,8 +50,7 @@ public PlayerService(
IPracticeService practiceService,
ITeamService teamService,
IMapper mapper,
IMemoryCache localCache,
IGameEngineService gameEngine
IMemoryCache localCache
)
{
ChallengeService = challengeService;
Expand All @@ -69,7 +66,6 @@ IGameEngineService gameEngine
TeamService = teamService;
Mapper = mapper;
LocalCache = localCache;
GameEngine = gameEngine;
}

public async Task<Player> Enroll(NewPlayer model, User actor, CancellationToken cancellationToken)
Expand Down
2 changes: 0 additions & 2 deletions src/Gameboard.Api/Features/Practice/PracticeController.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Gameboard.Api.Common;
using Gameboard.Api.Common.Services;
using Gameboard.Api.Data;
using Gameboard.Api.Services;
using Gameboard.Api.Structure;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,29 @@

namespace Gameboard.Api.Features.Reports;

public record ChallengesReportExportQuery(GetChallengesReportQueryArgs Parameters) : IRequest<IEnumerable<ChallengesReportCsvRecord>>;
public record ChallengesReportExportQuery(GetChallengesReportQueryArgs Parameters, User ActingUser) : IRequest<IEnumerable<ChallengesReportCsvRecord>>, IReportQuery;

public class ChallengesReportExportQueryHandler : IRequestHandler<ChallengesReportExportQuery, IEnumerable<ChallengesReportCsvRecord>>
internal class ChallengesReportExportQueryHandler : IRequestHandler<ChallengesReportExportQuery, IEnumerable<ChallengesReportCsvRecord>>
{
private readonly IMapper _mapper;
private readonly IReportsService _reportsService;
private readonly ReportsQueryValidator _reportsQueryValidator;

public ChallengesReportExportQueryHandler(IMapper mapper, IReportsService reportsService)
public ChallengesReportExportQueryHandler
(
IMapper mapper,
IReportsService reportsService,
ReportsQueryValidator reportsQueryValidator
)
{
_mapper = mapper;
_reportsService = reportsService;
_reportsQueryValidator = reportsQueryValidator;
}

public async Task<IEnumerable<ChallengesReportCsvRecord>> Handle(ChallengesReportExportQuery request, CancellationToken cancellationToken)
{
await _reportsQueryValidator.Validate(request);
var results = await _reportsService.GetChallengesReportRecords(request.Parameters);
return _mapper.Map<IEnumerable<ChallengesReportCsvRecord>>(results);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,29 @@

namespace Gameboard.Api.Features.Reports;

public record ChallengesReportQuery(GetChallengesReportQueryArgs Args) : IRequest<ReportResults<ChallengesReportRecord>>;
public record ChallengesReportQuery(GetChallengesReportQueryArgs Args, User ActingUser) : IRequest<ReportResults<ChallengesReportRecord>>, IReportQuery;

public class ChallengeReportQueryHandler : IRequestHandler<ChallengesReportQuery, ReportResults<ChallengesReportRecord>>
internal class ChallengeReportQueryHandler : IRequestHandler<ChallengesReportQuery, ReportResults<ChallengesReportRecord>>
{
private readonly INowService _now;
private readonly IReportsService _reportsService;
private readonly ReportsQueryValidator _reportsQueryValidator;

public ChallengeReportQueryHandler(INowService now, IReportsService reportsService)
public ChallengeReportQueryHandler
(
INowService now,
IReportsService reportsService,
ReportsQueryValidator reportsQueryValidator
)
{
_now = now;
_reportsService = reportsService;
_reportsQueryValidator = reportsQueryValidator;
}

public async Task<ReportResults<ChallengesReportRecord>> Handle(ChallengesReportQuery request, CancellationToken cancellationToken)
{
await _reportsQueryValidator.Validate(request);
var results = await _reportsService.GetChallengesReportRecords(request.Args);

return new ReportResults<ChallengesReportRecord>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,35 @@

namespace Gameboard.Api.Features.Reports;

public record EnrollmentReportQuery(EnrollmentReportParameters Parameters, PagingArgs PagingArgs) : IRequest<ReportResults<EnrollmentReportStatSummary, EnrollmentReportRecord>>;
public record EnrollmentReportQuery(EnrollmentReportParameters Parameters, PagingArgs PagingArgs, User ActingUser) : IRequest<ReportResults<EnrollmentReportStatSummary, EnrollmentReportRecord>>, IReportQuery;

internal class EnrollmentReportQueryHandler : IRequestHandler<EnrollmentReportQuery, ReportResults<EnrollmentReportStatSummary, EnrollmentReportRecord>>
{
private readonly IEnrollmentReportService _enrollmentReportService;
private readonly INowService _now;
private readonly IPagingService _pagingService;
private readonly ReportsQueryValidator _reportsQueryValidator;

public EnrollmentReportQueryHandler
(
IEnrollmentReportService enrollmentReportService,
INowService now,
IPagingService pagingService
IPagingService pagingService,
ReportsQueryValidator reportsQueryValidator
)
{
_enrollmentReportService = enrollmentReportService;
_now = now;
_pagingService = pagingService;
_reportsQueryValidator = reportsQueryValidator;
}

public async Task<ReportResults<EnrollmentReportStatSummary, EnrollmentReportRecord>> Handle(EnrollmentReportQuery request, CancellationToken cancellationToken)
{
// validate
await _reportsQueryValidator.Validate(request);

// pull and page results
var rawResults = await _enrollmentReportService.GetRawResults(request.Parameters, cancellationToken);
var paged = _pagingService.Page(rawResults.Records, request.PagingArgs);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@

namespace Gameboard.Api.Features.Reports;

public record EnrollmentReportExportQuery(EnrollmentReportParameters Parameters) : IRequest<IEnumerable<EnrollmentReportCsvRecord>>;
public record EnrollmentReportExportQuery(EnrollmentReportParameters Parameters, User ActingUser) : IRequest<IEnumerable<EnrollmentReportCsvRecord>>, IReportQuery;

internal class EnrollmentReportExportHandler : IRequestHandler<EnrollmentReportExportQuery, IEnumerable<EnrollmentReportCsvRecord>>
{
private readonly IEnrollmentReportService _enrollmentReportService;
private readonly ReportsQueryValidator _reportsQueryValidator;

public EnrollmentReportExportHandler(IEnrollmentReportService enrollmentReportService)
public EnrollmentReportExportHandler(IEnrollmentReportService enrollmentReportService, ReportsQueryValidator reportsQueryValidator)
{
_enrollmentReportService = enrollmentReportService;
_reportsQueryValidator = reportsQueryValidator;
}

public async Task<IEnumerable<EnrollmentReportCsvRecord>> Handle(EnrollmentReportExportQuery request, CancellationToken cancellationToken)
{
// validate
await _reportsQueryValidator.Validate(request);

// ignore paging parameters - for file export, we don't page
var results = await _enrollmentReportService.GetRawResults(request.Parameters, cancellationToken);

Expand Down
Loading

0 comments on commit 0c95d19

Please sign in to comment.