Skip to content
This repository has been archived by the owner on Apr 8, 2023. It is now read-only.

Ability to drop players from a team #112

Merged
merged 15 commits into from
Jul 6, 2022
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
8 changes: 4 additions & 4 deletions src/Client/Features/Teams/Detail.razor
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,20 @@ else
<MudText Typo="Typo.h6">@_playerTableHeader</MudText>
</ToolBarContent>
<HeaderContent>
@if(_isSignedPlayersShowing && _isUsersTeam)
@if(_isUsersTeam && _onPlayerTableActionClick != null)
{
<MudTh>Sign</MudTh>
<MudTh>Actions</MudTh>
}
<MudTh><MudTableSortLabel SortBy="new Func<PlayerItem,object>(x=> x.Name)">@nameof(PlayerItem.Name)</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<PlayerItem,object>(x=> x.Position)">@nameof(PlayerItem.Position)</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<PlayerItem,object>(x=> x.ContractValue)">Contract Value</MudTableSortLabel></MudTh>
<MudTh><MudTableSortLabel SortBy="new Func<PlayerItem,object>(x=> x.YearContractExpires ?? 0)">Contract Length</MudTableSortLabel></MudTh>
</HeaderContent>
<RowTemplate>
@if(_isSignedPlayersShowing && _isUsersTeam)
@if(_isUsersTeam && _onPlayerTableActionClick != null)
{
<MudTd DataLabel="Actions">
<MudIconButton Icon="@Icons.Filled.AssignmentLate" Color="Color.Primary" Variant="Variant.Outlined" Size="Size.Small" OnClick="(e) => OpenSignPlayerDialog(context.Id)" />
<MudIconButton Icon="@_tableActionIcon" Color="Color.Primary" Variant="Variant.Outlined" Size="Size.Small" OnClick="() => _onPlayerTableActionClick.Invoke(context.Id)" />
</MudTd>
}
<MudTd DataLabel=@nameof(PlayerItem.Name)>@context.Name</MudTd>
Expand Down
50 changes: 40 additions & 10 deletions src/Client/Features/Teams/Detail.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,29 @@ public sealed partial class Detail : IDisposable
{
[Inject] private HttpClient HttpClient { get; set; } = null!;
[Inject] private IDialogService DialogService { get; set; } = null!;
[CascadingParameter] private Task<AuthenticationState> AuthenticationStateTask { get; set; } = null!;
[Parameter] public int TeamId { get; set; }
[Inject] private ISnackbar Snackbar { get; set; } = null!;
[CascadingParameter] public Task<AuthenticationState> AuthenticationStateTask { get; set; } = null!;
[Parameter, EditorRequired] public int TeamId { get; set; }

private TeamDetailResult? _result;
private bool _isUsersTeam = false;
private bool _isSignedPlayersShowing = false;
private string _playerTableHeader = "Rostered Players";
private IEnumerable<PlayerItem> _playersToDisplay = Array.Empty<PlayerItem>();
private string _title = string.Empty;
private readonly CancellationTokenSource _cts = new();
private Func<int, Task>? _onPlayerTableActionClick;
private string? _tableActionIcon;

protected override async Task OnInitializedAsync()
{
var authenticationState = await AuthenticationStateTask;
var user = authenticationState.User!;

var claim = user.FindFirst(nameof(IUser.TeamId));
if (int.Parse(claim!.Value) == TeamId) _isUsersTeam = true;
_isUsersTeam = int.Parse(claim!.Value) == TeamId;

await LoadDataAsync();
ShowRosteredPlayers();
}

private async Task LoadDataAsync()
Expand All @@ -41,7 +44,6 @@ private async Task LoadDataAsync()
{
_result = await HttpClient.GetFromJsonAsync<TeamDetailResult>(TeamDetailRouteFactory.Create(TeamId), _cts.Token);
_title = $"Team Detail - {_result!.Name}";
ShowRosteredPlayers();
}
catch (AccessTokenNotAvailableException exception)
{
Expand All @@ -53,24 +55,50 @@ private void ShowRosteredPlayers()
{
_playersToDisplay = _result!.RosteredPlayers;
_playerTableHeader = "Rostered Players";
_isSignedPlayersShowing = false;
_onPlayerTableActionClick = DropPlayerAsync;
_tableActionIcon = Icons.Outlined.PersonRemove;
}

private void ShowUnrosteredPlayers()
{
_playersToDisplay = _result!.UnrosteredPlayers;
_playerTableHeader = "Unrostered Players";
_isSignedPlayersShowing = false;
_onPlayerTableActionClick = null;
}

private void ShowUnsignedPlayers()
{
_playersToDisplay = _result!.UnsignedPlayers;
_playerTableHeader = "Unsigned Players";
_isSignedPlayersShowing = true;
_onPlayerTableActionClick = OpenSignPlayerDialogAsync;
_tableActionIcon = Icons.Filled.AssignmentLate;
}

private async void OpenSignPlayerDialog(int playerId)
private async Task DropPlayerAsync(int playerId)
{
try
{
var response = await HttpClient.PostAsJsonAsync(DropPlayerRouteFactory.Uri, new DropPlayerRequest { PlayerId = playerId }, _cts.Token);

if (response.IsSuccessStatusCode)
{
Snackbar.Add("Successfully dropped player.", Severity.Success);

await LoadDataAsync();
ShowRosteredPlayers();
}
else
{
Snackbar.Add("Something went wrong...", Severity.Error);
}
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}

private async Task OpenSignPlayerDialogAsync(int playerId)
{
var maxWidth = new DialogOptions() { MaxWidth = MaxWidth.Small, FullWidth = true };
var parameters = new DialogParameters
Expand All @@ -81,16 +109,18 @@ private async void OpenSignPlayerDialog(int playerId)
var dialog = DialogService.Show<SignPlayer>("Sign Player", parameters, maxWidth);
var result = await dialog.Result;


if (!result.Cancelled)
{
await LoadDataAsync();
ShowUnsignedPlayers();
}
}

public void Dispose()
{
_cts.Cancel();
_cts.Dispose();

_onPlayerTableActionClick = null;
}
}
6 changes: 3 additions & 3 deletions src/Client/Features/Teams/SignPlayer.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace DynamoLeagueBlazor.Client.Features.Teams;
public sealed partial class SignPlayer : IDisposable
{
[Inject] private HttpClient HttpClient { get; set; } = null!;
[Inject] private ISnackbar SnackBar { get; set; } = null!;
[Inject] private ISnackbar Snackbar { get; set; } = null!;
[CascadingParameter] public MudDialogInstance MudDialogInstance { get; set; } = null!;
[Parameter, EditorRequired] public int PlayerId { get; set; }
private SignPlayerRequest _form = null!;
Expand Down Expand Up @@ -46,11 +46,11 @@ private async Task OnValidSubmitAsync()

if (response.IsSuccessStatusCode)
{
SnackBar.Add("Successfully signed player.", Severity.Success);
Snackbar.Add("Successfully signed player.", Severity.Success);
}
else
{
SnackBar.Add("Something went wrong...", Severity.Error);
Snackbar.Add("Something went wrong...", Severity.Error);
}
}
catch (AccessTokenNotAvailableException exception)
Expand Down
48 changes: 48 additions & 0 deletions src/Server/Features/Teams/DropPlayer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using DynamoLeagueBlazor.Server.Infrastructure;
using DynamoLeagueBlazor.Shared.Features.Teams;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace DynamoLeagueBlazor.Server.Features.Teams;

[ApiController]
[Route(DropPlayerRouteFactory.Uri)]
public class DropPlayerController : ControllerBase
{
private readonly IMediator _mediator;

public DropPlayerController(IMediator mediator)
{
_mediator = mediator;
}

[HttpPost]
public async Task<int> PostAsync(DropPlayerRequest request, CancellationToken cancellationToken)
=> await _mediator.Send(new DropPlayerCommand(request.PlayerId), cancellationToken);
}

public record DropPlayerCommand(int PlayerId) : IRequest<int> { }

public class DropPlayerCommandHandler : IRequestHandler<DropPlayerCommand, int>
{
private readonly ApplicationDbContext _dbContext;

public DropPlayerCommandHandler(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}

public async Task<int> Handle(DropPlayerCommand command, CancellationToken cancellationToken)
{
var player = await _dbContext.Players
.AsTracking()
.SingleAsync(u => u.Id == command.PlayerId, cancellationToken);

player.DropFromCurrentTeam();

await _dbContext.SaveChangesAsync(cancellationToken);

return player.Id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<int?>("State")
.HasColumnType("int");

b.Property<int?>("State")
.HasColumnType("int");

b.Property<int?>("TeamId")
.HasColumnType("int");

Expand Down
7 changes: 2 additions & 5 deletions src/Server/Models/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,6 @@ public Player(string name, string position, string headShotUrl) : this()
public ICollection<Bid> Bids { get; private set; } = new HashSet<Bid>();
public ICollection<Fine> Fines { get; private set; } = new HashSet<Fine>();

// TODO: For reference when doing unrostering/dropping players. Remove when that state is added.
//public static IQueryable<Player> WhereIsUnrostered(this IQueryable<Player> players)
// => players.Where(p => p.Rostered == false
// && p.YearContractExpires != null
// && p.EndOfFreeAgency == null);
private enum PlayerStateTrigger { NewSeasonStarted, BiddingEnded, OfferMatchedByTeam, MatchExpired, SignedByTeam, DroppedByTeam }
public enum PlayerState { FreeAgent, OfferMatching, Unsigned, Rostered, Unrostered }

Expand Down Expand Up @@ -109,6 +104,8 @@ public Bid AddBid(int amount, int teamIdOfBidder)
return bid;
}

public void DropFromCurrentTeam() => _machine.Fire(PlayerStateTrigger.DroppedByTeam);

private void GrantExtensionToFreeAgency()
{
EndOfFreeAgency = EndOfFreeAgency?.AddDays(1);
Expand Down
12 changes: 12 additions & 0 deletions src/Shared/Features/Teams/DropPlayer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace DynamoLeagueBlazor.Shared.Features.Teams;

public class DropPlayerRequest
{
public int PlayerId { get; set; }
}

public static class DropPlayerRouteFactory
{
public const string Uri = "api/admin/dropplayer";
}

1 change: 0 additions & 1 deletion src/Tests/Features/FreeAgents/EndBiddingTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using DynamoLeagueBlazor.Server.Features.FreeAgents;
using DynamoLeagueBlazor.Server.Models;
using static DynamoLeagueBlazor.Server.Models.Player;

namespace DynamoLeagueBlazor.Tests.Features.FreeAgents;

Expand Down
1 change: 0 additions & 1 deletion src/Tests/Features/OfferMatching/ListTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using DynamoLeagueBlazor.Server.Models;
using DynamoLeagueBlazor.Shared.Features.OfferMatching;
using System.Net.Http.Json;
using static DynamoLeagueBlazor.Server.Models.Player;

namespace DynamoLeagueBlazor.Tests.Features.OfferMatching;

Expand Down
60 changes: 59 additions & 1 deletion src/Tests/Features/Teams/DetailTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using DynamoLeagueBlazor.Shared.Features.Teams;
using DynamoLeagueBlazor.Client.Features.Teams;
using DynamoLeagueBlazor.Shared.Features.Teams;
using DynamoLeagueBlazor.Shared.Utilities;
using MudBlazor;
using System.Net.Http.Json;

namespace DynamoLeagueBlazor.Tests.Features.Teams;
Expand Down Expand Up @@ -83,3 +85,59 @@ public async Task GivenAnyAuthenticatedUser_WhenGivenValidTeamId_ThenReturnsExpe
unsignedPlayer.ContractValue.Should().Be(mockUnsignedPlayer.ContractValue);
}
}

public class DetailClientTests : UITestBase
{
[Fact]
public void WhenPageIsLoading_ThenShowsLoading()
{
AuthorizeAsUser(int.MaxValue);

GetHttpHandler.When(HttpMethod.Get, TeamDetailRouteFactory.Create(int.MaxValue))
.TimesOutAfter(500);

var cut = RenderComponent<Detail>(parameters =>
{
parameters.Add(p => p.TeamId, int.MaxValue);
});

cut.HasComponent<MudSkeleton>().Should().BeTrue();
}

[Fact]
public void WhenThePageInitializes_ThenShowsAListOfRosteredPlayers()
{
var teamId = int.MaxValue;

AuthorizeAsUser(teamId);

GetHttpHandler.When(HttpMethod.Get, TeamDetailRouteFactory.Create(teamId))
.RespondsWithJson(AutoFaker.Generate<TeamDetailResult>());

var cut = RenderComponent<Detail>(parameters =>
{
parameters.Add(p => p.TeamId, teamId);
});

cut.Markup.Should().Contain("Rostered Players");
cut.HasComponent<MudTable<TeamDetailResult.PlayerItem>>().Should().BeTrue();
cut.Markup.Should().Contain("tr");
}

[Fact]
public void GivenAUserHasATeamOfOne_WhenViewingThePageOfTeamTwo_ThenDoesNotShowAnyActionButtons()
{
AuthorizeAsUser(1);

var teamTwo = 2;
GetHttpHandler.When(HttpMethod.Get, TeamDetailRouteFactory.Create(teamTwo))
.RespondsWithJson(AutoFaker.Generate<TeamDetailResult>());

var cut = RenderComponent<Detail>(parameters =>
{
parameters.Add(p => p.TeamId, teamTwo);
});

cut.Markup.Should().NotContain("Actions");
}
}
45 changes: 45 additions & 0 deletions src/Tests/Features/Teams/DropPlayerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using DynamoLeagueBlazor.Server.Models;
using DynamoLeagueBlazor.Shared.Features.Teams;

namespace DynamoLeagueBlazor.Tests.Features.Teams;

public class DropPlayerServerTests : IntegrationTestBase
{
private static DropPlayerRequest CreateFakeValidRequest()
{
var faker = new AutoFaker<DropPlayerRequest>();
return faker.Generate();
}

[Fact]
public async Task GivenUnauthenticatedUser_ThenDoesNotAllowAccess()
{
var application = CreateUnauthenticatedApplication();

var client = application.CreateClient();

var stubRequest = CreateFakeValidRequest();
var response = await client.PostAsJsonAsync(DropPlayerRouteFactory.Uri, stubRequest);

response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}

[Fact]
public async Task GivenAuthenticatedUser_WhenAPlayerIsRostered_ThenUnrostersPlayer()
{
var application = CreateUserAuthenticatedApplication();
var mockPlayer = CreateFakePlayer();
mockPlayer.State = PlayerState.Rostered;
await application.AddAsync(mockPlayer);

var request = CreateFakeValidRequest();
request.PlayerId = mockPlayer.Id;
var client = application.CreateClient();

await client.PostAsJsonAsync(DropPlayerRouteFactory.Uri, request);

var player = await application.FirstOrDefaultAsync<Player>();

player!.State.Should().Be(PlayerState.Unrostered);
}
}
Loading