Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.29.0 #608

Merged
merged 6 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ await _testContext.WithDataState(state =>
g.Players = state.Build<Data.Player>(fixture, p =>
{
p.Id = playerId;
p.Role = PlayerRole.Manager;
p.User = state.Build<Data.User>(fixture, u => u.Id = userId);
p.SessionBegin = DateTimeOffset.UtcNow.AddDays(-1);
p.SessionEnd = DateTimeOffset.UtcNow.AddDays(1);
Expand All @@ -51,7 +52,7 @@ await _testContext.WithDataState(state =>
// act
var challenge = await _testContext
.CreateHttpClientWithActingUser(u => u.Id = userId)
.PostAsync("/api/challenge", model.ToJsonBody())
.PostAsync("/api/challenge/launch", model.ToJsonBody())
.DeserializeResponseAs<Api.Challenge>();

// assert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ public class ChallengeServiceTests
/// <param name="gameId"></param>
/// <param name="playerId"></param>
/// <param name="gamespaceId"></param>
/// <param name="graderUrl"></param>
/// <param name="specId"></param>
/// <param name="specExternalId"></param>
/// <param name="teamId"></param>
Expand Down Expand Up @@ -102,6 +101,7 @@ string userId
A.Fake<IActingUserService>(),
A.Fake<ConsoleActorMap>(),
A.Fake<CoreOptions>(),
A.Fake<IChallengeGraderUrlService>(),
A.Fake<IChallengeStore>(),
A.Fake<IChallengeDocsService>(),
A.Fake<IChallengeSubmissionsService>(),
Expand All @@ -127,7 +127,6 @@ string userId
fakeGame,
fakePlayer,
userId,
graderUrl,
1,
0
);
Expand Down Expand Up @@ -225,6 +224,7 @@ string userId
A.Fake<IActingUserService>(),
A.Fake<ConsoleActorMap>(),
A.Fake<CoreOptions>(),
A.Fake<IChallengeGraderUrlService>(),
A.Fake<IChallengeStore>(),
A.Fake<IChallengeDocsService>(),
A.Fake<IChallengeSubmissionsService>(),
Expand All @@ -250,7 +250,6 @@ string userId
fakeGame,
fakePlayer,
userId,
graderUrl,
1,
0
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using Gameboard.Api.Structure.MediatR.Validators;
using MediatR;
using Microsoft.EntityFrameworkCore;
using ServiceStack.Text;
using TopoMojo.Api.Client;

namespace Gameboard.Api.Features.Admin;

Expand Down Expand Up @@ -60,7 +62,7 @@ await _validator
g.IsPublished,
IsRegistrationActive = g.RegistrationType == GameRegistrationType.Open && g.RegistrationOpen <= nowish && g.RegistrationClose >= nowish,
IsTeamGame = g.MaxTeamSize > 1,
RegisteredTeamCount = g.Players.Select(p => p.TeamId).Distinct().Count()
RegisteredTeamCount = g.Players.Where(p => p.Mode == PlayerMode.Competition).Select(p => p.TeamId).Distinct().Count()
})
.SingleAsync(g => g.Id == request.GameId, cancellationToken);

Expand Down Expand Up @@ -101,12 +103,26 @@ await _validator
var startedTeamsCount = await _store
.WithNoTracking<Data.Player>()
.Where(p => p.GameId == request.GameId)
.Where(p => p.Mode == PlayerMode.Competition)
.SelectedStartedTeamIds()
.CountAsync(cancellationToken);

var playerActivity = await _store
var practiceData = await _store
.WithNoTracking<Data.Player>()
.Where(p => p.GameId == gameData.Id)
.Where(p => p.Mode == PlayerMode.Practice)
.Select(p => new
{
p.Id,
p.TeamId,
p.UserId
})
.ToArrayAsync(cancellationToken);

var competitiveActivity = await _store
.WithNoTracking<Data.Player>()
.Where(p => p.GameId == request.GameId)
.Where(p => p.Mode == PlayerMode.Competition)
.Select(p => new
{
p.GameId,
Expand All @@ -120,20 +136,15 @@ await _validator
.GroupBy(p => p.GameId)
.Select(gr => new GameCenterContextStats
{
AttemptCountPractice = gr.Where(p => p.Mode == PlayerMode.Practice).Count(),
AttemptCountPractice = practiceData.Count(),
PlayerCountActive = gr
.Where(p => p.IsActive)
.Count(),
PlayerCountCompetitive = gr
.Where(p => p.Mode == PlayerMode.Competition)
.Select(p => p.UserId)
.Distinct()
.Count(),
PlayerCountPractice = gr
.Where(p => p.Mode == PlayerMode.Practice)
.Select(p => p.UserId)
.Distinct()
.Count(),
PlayerCountPractice = practiceData.Select(p => p.UserId).Distinct().Count(),
PlayerCountTotal = gr
.Select(p => p.UserId)
.Distinct()
Expand All @@ -149,15 +160,10 @@ await _validator
.Distinct()
.Count(),
TeamCountCompetitive = gr
.Where(p => p.Mode == PlayerMode.Competition)
.Select(p => p.TeamId)
.Distinct()
.Count(),
TeamCountPractice = gr
.Where(p => p.Mode == PlayerMode.Practice)
.Select(p => p.TeamId)
.Distinct()
.Count(),
TeamCountPractice = practiceData.Select(p => p.TeamId).Distinct().Count(),
TeamCountNotStarted = gameData.RegisteredTeamCount - startedTeamsCount,
TeamCountTotal = gameData.RegisteredTeamCount,
TopScore = topScore == null ? null : topScore.ScoreOverall,
Expand All @@ -182,7 +188,7 @@ await _validator
IsPublished = gameData.IsPublished,
IsRegistrationActive = gameData.IsRegistrationActive,
IsTeamGame = gameData.IsTeamGame,
Stats = playerActivity ?? new()
Stats = competitiveActivity ?? new()
{
AttemptCountPractice = gameData.IsPracticeMode ? 0 : null,
TopScore = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public async Task<GameCenterPracticeContext> Handle(GetGameCenterPracticeContext
var users = await _store
.WithNoTracking<Data.Challenge>()
.Where(c => c.GameId == request.GameId)
.Where(c => c.PlayerMode == PlayerMode.Practice)
.Where
(
c =>
Expand Down
32 changes: 26 additions & 6 deletions src/Gameboard.Api/Features/Challenge/ChallengeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ namespace Gameboard.Api.Controllers;
public class ChallengeController
(
IActingUserService actingUserService,
IChallengeGraderUrlService challengeGraderUrlService,
ILogger<ChallengeController> logger,
IDistributedCache cache,
ChallengeValidator validator,
Expand All @@ -37,7 +36,6 @@ public class ChallengeController
ConsoleActorMap actormap
) : GameboardLegacyController(actingUserService, logger, cache, validator)
{
private readonly IChallengeGraderUrlService _challengeGraderUrlService = challengeGraderUrlService;
private readonly IMediator _mediator = mediator;
private readonly IUserRolePermissionsService _permissionsService = permissionsService;

Expand All @@ -47,12 +45,35 @@ ConsoleActorMap actormap
ConsoleActorMap ActorMap { get; } = actormap;

/// <summary>
/// Create new challenge instance
/// Purge a challenge. This deletes the challenge instance, and all progress on it, and can't be undone.
/// A record of the challenge is stored in ArchivedChallenges.
/// </summary>
/// <param name="challengeId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpDelete("api/challenge/{challengeId}")]
public Task DeleteChallenge(string challengeId, CancellationToken cancellationToken)
=> _mediator.Send(new PurgeChallengeCommand(challengeId), cancellationToken);

/// <summary>
/// Create and start an instance of a challenge.
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("api/challenge")]
public Task<StartChallengeResponse> Create([FromBody] StartChallengeCommand request, CancellationToken cancellationToken)
=> _mediator.Send(request, cancellationToken);

/// <summary>
/// Create new challenge instance.
///
/// (This endpoint is deprecated and will be removed in a future release. Instead, use api/challenge and pass teamId + specId)
/// </summary>
/// <remarks>Idempotent method to retrieve or create challenge state</remarks>
/// <param name="model">NewChallenge</param>
/// <returns>Challenge</returns>
[HttpPost("api/challenge")]
[HttpPost("api/challenge/launch")]
public async Task<Challenge> Create([FromBody] NewChallenge model)
{
await AuthorizeAny
Expand All @@ -65,8 +86,7 @@ await AuthorizeAny
if (!await _permissionsService.Can(PermissionKey.Play_ChooseChallengeVariant))
model.Variant = 0;

var graderUrl = _challengeGraderUrlService.BuildGraderUrl();
var result = await ChallengeService.GetOrCreate(model, Actor.Id, graderUrl);
var result = await ChallengeService.GetOrCreate(model, Actor.Id);

await Hub.Clients.Group(result.TeamId).ChallengeEvent(new HubEvent<Challenge>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Threading;
using System.Threading.Tasks;
using Gameboard.Api.Features.Users;
using Gameboard.Api.Services;
using Gameboard.Api.Structure.MediatR;
using MediatR;

namespace Gameboard.Api.Features.Challenges;

public record PurgeChallengeCommand(string ChallengeId) : IRequest;

internal sealed class PurgeChallengeHandler(ChallengeService challenges, IValidatorService validator) : IRequestHandler<PurgeChallengeCommand>
{
private readonly ChallengeService _challenges = challenges;
private readonly IValidatorService _validator = validator;

public async Task Handle(PurgeChallengeCommand request, CancellationToken cancellationToken)
{
await _validator
.Auth(c => c.RequireOneOf(PermissionKey.Teams_EditSession))
.AddEntityExistsValidator<Data.Challenge>(request.ChallengeId)
.Validate(cancellationToken);

await _challenges.ArchiveChallenge(request.ChallengeId, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Gameboard.Api.Common.Services;
using Gameboard.Api.Data;
using Gameboard.Api.Features.Teams;
using Gameboard.Api.Features.Users;
using Gameboard.Api.Hubs;
using Gameboard.Api.Services;
using Gameboard.Api.Structure.MediatR;
using MediatR;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;

namespace Gameboard.Api.Features.Challenges;

public record StartChallengeCommand(string ChallengeSpecId, string TeamId, int variantIndex) : IRequest<StartChallengeResponse>;

internal sealed class StartChallengeHandler
(
IActingUserService actingUser,
ChallengeService challengeService,
IHubContext<AppHub, IAppHubEvent> legacyTeamHub,
IStore store,
ITeamService teamService,
IValidatorService validator
) : IRequestHandler<StartChallengeCommand, StartChallengeResponse>
{
private readonly IActingUserService _actingUser = actingUser;
private readonly ChallengeService _challengeService = challengeService;
private readonly IHubContext<AppHub, IAppHubEvent> _legacyTeamHub = legacyTeamHub;
private readonly IStore _store = store;
private readonly ITeamService _teamService = teamService;
private readonly IValidatorService _validator = validator;

public async Task<StartChallengeResponse> Handle(StartChallengeCommand request, CancellationToken cancellationToken)
{
// resolve the team/challenge/game mess
var captain = await _teamService.ResolveCaptain(request.TeamId, cancellationToken);
var team = await _teamService.GetTeam(request.TeamId);

var spec = await _store
.WithNoTracking<Data.ChallengeSpec>()
.Where(s => s.Id == request.ChallengeSpecId)
.Select(s => new { s.Id, s.GameId, GameName = s.Game.Name })
.SingleOrDefaultAsync(cancellationToken);

await _validator
.Auth
(
c => c
.RequireOneOf(PermissionKey.Teams_EditSession)
.UnlessUserIdIn([.. team.Members.Select(m => m.Id)])
)
.AddValidator(spec is null, new ResourceNotFound<Data.ChallengeSpec>(request.ChallengeSpecId))
.AddValidator(captain.GameId != spec.GameId, new TeamIsntPlayingGame
(
new SimpleEntity
{
Id = captain.TeamId,
Name = captain.ApprovedName
},
new SimpleEntity
{
Id = captain.GameId,
Name = spec.GameName
}
))
.Validate(cancellationToken);

var challenge = await _challengeService.GetOrCreate(new NewChallenge
{
PlayerId = captain.Id,
SpecId = request.ChallengeSpecId,
StartGamespace = true,
Variant = request.variantIndex
});

await _legacyTeamHub.Clients.Group(captain.TeamId).ChallengeEvent(new HubEvent<Challenge>
{
Model = challenge,
Action = EventAction.Updated,
ActingUser = _actingUser.Get().ToSimpleEntity()
});

return new StartChallengeResponse { Challenge = challenge };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Gameboard.Api.Features.Challenges;

public sealed class StartChallengeResponse
{
public required Challenge Challenge { get; set; }
}
Loading