Skip to content

Commit

Permalink
Enable null for team venue and home match day/time
Browse files Browse the repository at this point in the history
Team.SelectVenue, TeamApplication.SelectVenue
* Change 'Not found' response to JsonResponseRedirect
* Change display message from TeamDataFailure to VenueSelectFailure

TeamApplication
* Call SignInManager.RefreshSignInAsync() after a user has registered a new team, so that it's immediately visible in Team.MyTeam
* Enable null for team venue and home match day/time
  * Add ApplicationSessionModel.TeamVenueSetKind to track "not set, no venue, existing venue, new venue"
  * Add TeamVenueValidator to validate selected venue types

TeamEditor component
* Add 'readonly' attribute to HTML element instead of 'disabled' for 'null' value

VenueSelector component
* Add 'Criteria.NotSpecified' if this is allowed in tenant specific 'TournamentContext'
* Include 'Criteria.NotSpecified' to selectable venue options

Team.MyTeam, Team.Single views
* Update display for venue, match day/time
  • Loading branch information
axunonb committed Aug 21, 2023
1 parent aab6132 commit b8059ae
Show file tree
Hide file tree
Showing 35 changed files with 749 additions and 125 deletions.
6 changes: 3 additions & 3 deletions League/Components/VenueEditorComponentModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace League.Components;
public class VenueEditorComponentModel
{
[HiddenInput]
public long Id { get; set; }
public long? Id { get; set; }

[HiddenInput]
public bool IsNew { get; set; }
Expand Down Expand Up @@ -81,15 +81,15 @@ public void MapFormFieldsToEntity(VenueEntity venueEntity)
{
// The entity is populated from the database,
// so we can track all eventual changes to fields
venueEntity.Id = IsNew ? default : Id;
venueEntity.Id = IsNew ? default : Id ?? default;
venueEntity.IsNew = IsNew;
venueEntity.Name = Name;
venueEntity.Extension = Extension;
venueEntity.PostalCode = PostalCode;
venueEntity.City = City;
venueEntity.Street = Street;
venueEntity.Direction = Direction;
if (venueEntity.Longitude.HasValue && venueEntity.Latitude.HasValue)
if (venueEntity is { Longitude: null, Latitude: null })
{
venueEntity.Longitude = Longitude;
venueEntity.Latitude = Latitude;
Expand Down
4 changes: 1 addition & 3 deletions League/Components/VenueSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ namespace League.Components;

public class VenueSelector : ViewComponent
{
private readonly ITenantContext _tenantContext;
private readonly TournamentManager.MultiTenancy.AppDb _appDb;
private readonly AppDb _appDb;
private readonly ILogger<VenueSelector> _logger;

public VenueSelector(ITenantContext tenantContext, ILogger<VenueSelector> logger)
{
_tenantContext = tenantContext;
_appDb = tenantContext.DbContext.AppDb;
_logger = logger;
}
Expand Down
14 changes: 8 additions & 6 deletions League/Controllers/Team.cs
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,10 @@ public async Task<IActionResult> SelectVenue([FromForm][Bind("TeamId, VenueId")]
_appDb.TeamRepository.GetTeamEntityAsync(new PredicateExpression(TeamFields.Id == model.TeamId),
cancellationToken);

if (teamEntity == null || !model.VenueId.HasValue ||
!await _appDb.VenueRepository.IsValidVenueIdAsync(model.VenueId, cancellationToken))
if (teamEntity == null)
{
return NotFound();
TempData.Put<MyTeamMessageModel.MyTeamMessage>(nameof(MyTeamMessageModel.MyTeamMessage), new MyTeamMessageModel.MyTeamMessage { AlertType = SiteAlertTagHelper.AlertType.Danger, MessageId = MyTeamMessageModel.MessageId.TeamDataFailure});
return JsonResponseRedirect(TenantLink.Action(nameof(MyTeam), nameof(Team), new { model.TeamId }));
}

if (!(await _authorizationService.AuthorizeAsync(User, new TeamEntity(teamEntity.Id),
Expand All @@ -319,19 +319,21 @@ public async Task<IActionResult> SelectVenue([FromForm][Bind("TeamId, VenueId")]
}

model.TournamentId = _tenantContext.TournamentContext.TeamTournamentId;
teamEntity.VenueId = model.VenueId;
var teamVenueValidator = new TeamVenueValidator(teamEntity, _tenantContext);

if (!ModelState.IsValid)
if (!await model.ValidateAsync(teamVenueValidator, ModelState, cancellationToken))
{
return PartialView(Views.ViewNames.Team._SelectVenueModalPartial, model);
}

TempData.Put<MyTeamMessageModel.MyTeamMessage>(nameof(MyTeamMessageModel.MyTeamMessage), new MyTeamMessageModel.MyTeamMessage { AlertType = SiteAlertTagHelper.AlertType.Danger, MessageId = MyTeamMessageModel.MessageId.TeamDataFailure});
TempData.Put<MyTeamMessageModel.MyTeamMessage>(nameof(MyTeamMessageModel.MyTeamMessage), new MyTeamMessageModel.MyTeamMessage { AlertType = SiteAlertTagHelper.AlertType.Danger, MessageId = MyTeamMessageModel.MessageId.VenueSelectFailure });
try
{
teamEntity.VenueId = model.VenueId;
if(await _appDb.GenericRepository.SaveEntityAsync(teamEntity, false, false, cancellationToken))
{
TempData.Put<MyTeamMessageModel.MyTeamMessage>(nameof(MyTeamMessageModel.MyTeamMessage), new MyTeamMessageModel.MyTeamMessage { AlertType = SiteAlertTagHelper.AlertType.Success, MessageId = MyTeamMessageModel.MessageId.VenueCreateSuccess});
TempData.Put<MyTeamMessageModel.MyTeamMessage>(nameof(MyTeamMessageModel.MyTeamMessage), new MyTeamMessageModel.MyTeamMessage { AlertType = SiteAlertTagHelper.AlertType.Success, MessageId = MyTeamMessageModel.MessageId.VenueSelectSuccess});
}
else
{
Expand Down
64 changes: 41 additions & 23 deletions League/Controllers/TeamApplication.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Axuno.Tools.GeoSpatial;
using System.Security.Claims;
using Axuno.Tools.GeoSpatial;
using League.BackgroundTasks;
using League.Components;
using League.ConfigurationPoco;
Expand All @@ -18,6 +19,7 @@
using TournamentManager.DAL.TypedViewClasses;
using TournamentManager.ModelValidators;
using TournamentManager.MultiTenancy;
using TeamVenueSetKind = League.Models.TeamApplicationViewModels.ApplicationSessionModel.TeamVenueSetKind;

namespace League.Controllers;

Expand All @@ -35,14 +37,16 @@ public class TeamApplication : AbstractController
private readonly GoogleConfiguration _googleConfig;
private readonly Axuno.BackgroundTask.IBackgroundQueue _queue;
private readonly SendEmailTask _sendEmailTask;
private readonly SignInManager<Identity.ApplicationUser> _signInManager;
private const string TeamApplicationSessionName = "TeamApplicationSession";

public TeamApplication(ITenantContext tenantContext,
Axuno.Tools.DateAndTime.TimeZoneConverter timeZoneConverter,
IStringLocalizer<TeamApplication> localizer, IAuthorizationService authorizationService,
RegionInfo regionInfo,
IConfiguration configuration, Axuno.BackgroundTask.IBackgroundQueue queue,
SendEmailTask sendEmailTask, ILogger<TeamApplication> logger)
SendEmailTask sendEmailTask, SignInManager<Identity.ApplicationUser> signInManger,
ILogger<TeamApplication> logger)
{
_tenantContext = tenantContext;
_timeZoneConverter = timeZoneConverter;
Expand All @@ -53,6 +57,7 @@ public TeamApplication(ITenantContext tenantContext,
_sendEmailTask = sendEmailTask;
_appDb = tenantContext.DbContext.AppDb;
_authorizationService = authorizationService;
_signInManager = signInManger;
_logger = logger;
}

Expand Down Expand Up @@ -133,7 +138,7 @@ public async Task<IActionResult> SelectTeam([FromForm] ApplicationSelectTeamMode
System.Diagnostics.Debug.Assert(selectTeamModel.SelectedTeamId != null, "selectTeamModel.SelectedTeamId != null");

var sessionModel = await GetModelFromSession(cancellationToken);
if (sessionModel.TeamInRoundIsSet && sessionModel.TeamIsSet &&
if (sessionModel is { TeamInRoundIsSet: true, TeamIsSet: true } &&
sessionModel.Team!.Id != selectTeamModel.SelectedTeamId)
{
sessionModel.TeamInRoundIsSet = false;
Expand Down Expand Up @@ -204,7 +209,7 @@ public async Task<IActionResult> EditTeam(CancellationToken cancellationToken)
Team = GetTeamEditorComponentModel(teamEntity)
};

if(sessionModel.TeamInRoundIsSet) teamEditModel.Round.SelectedRoundId = sessionModel.TeamInRound!.RoundId;
if (sessionModel.TeamInRoundIsSet) teamEditModel.Round.SelectedRoundId = sessionModel.TeamInRound!.RoundId;
if (sessionModel.TeamIsSet) teamEditModel.Team = sessionModel.Team;

return View(Views.ViewNames.TeamApplication.EditTeam, teamEditModel);
Expand All @@ -219,7 +224,7 @@ public async Task<IActionResult> EditTeam([FromForm] TeamEditModel teamEditModel
ViewData["TournamentName"] = sessionModel.TournamentName;

TeamEntity? teamEntity = null;
if (teamEditModel.Team != null && !teamEditModel.Team.IsNew)
if (teamEditModel.Team is { IsNew: false })
{
teamEntity = await _appDb.TeamRepository.GetTeamEntityAsync(new PredicateExpression(TeamFields.Id == teamEditModel.Team.Id), cancellationToken);
if (teamEntity == null)
Expand Down Expand Up @@ -284,6 +289,7 @@ public async Task<IActionResult> SelectVenue(CancellationToken cancellationToken
ViewData["TournamentName"] = sessionModel.TournamentName;

sessionModel.Venue!.IsNew = true;
sessionModel.VenueIsSet = TeamVenueSetKind.NotSet;
SaveModelToSession(sessionModel);

var teamEntity = new TeamEntity();
Expand All @@ -299,7 +305,7 @@ public async Task<IActionResult> SelectVenue(CancellationToken cancellationToken
{
TournamentId = sessionModel.PreviousTournamentId ?? _tenantContext.TournamentContext.ApplicationTournamentId,
TeamId = teamEntity.Id,
VenueId = (sessionModel.VenueIsSet && !sessionModel.Venue.IsNew)
VenueId = (sessionModel.VenueIsSet == TeamVenueSetKind.ExistingVenue && !sessionModel.Venue.IsNew)
? sessionModel.Venue.Id
: teamEntity.VenueId
}
Expand All @@ -325,40 +331,45 @@ public async Task<IActionResult> SelectVenue([FromForm][Bind("TeamId, VenueId")]
if (teamEntity == null) return Redirect(TenantLink.Action(nameof(SelectTeam))!);
}

if (selectVenueModel.VenueId.HasValue && !await _appDb.VenueRepository.IsValidVenueIdAsync(selectVenueModel.VenueId, cancellationToken))
{
return Redirect(TenantLink.Action(nameof(SelectVenue))!);
}

selectVenueModel.TournamentId = sessionModel.PreviousTournamentId ?? _tenantContext.TournamentContext.ApplicationTournamentId;
var teamValidator = new TeamVenueValidator(new TeamEntity { VenueId = selectVenueModel.VenueId }, _tenantContext);

if (!ModelState.IsValid)
if (!await selectVenueModel.ValidateAsync(teamValidator, ModelState, cancellationToken))
{
return View(Views.ViewNames.TeamApplication.SelectVenue, selectVenueModel);
}

sessionModel.Venue!.IsNew = !selectVenueModel.VenueId.HasValue;
sessionModel.Venue.Id = selectVenueModel.VenueId ?? 0;
sessionModel.VenueIsSet =
selectVenueModel.VenueId == null ? TeamVenueSetKind.NoVenue : TeamVenueSetKind.ExistingVenue;
sessionModel.Venue!.IsNew = false;
sessionModel.Venue.Id = selectVenueModel.VenueId;

SaveModelToSession(sessionModel);

return Redirect(TenantLink.Action(nameof(EditVenue))!);
return Redirect(sessionModel.VenueIsSet == TeamVenueSetKind.NoVenue
? TenantLink.Action(nameof(Confirm))!
: TenantLink.Action(nameof(EditVenue))!);
}

[HttpGet("edit-venue")]
public async Task<IActionResult> EditVenue(bool? isNew, CancellationToken cancellationToken)
{
var sessionModel = await GetModelFromSession(cancellationToken);
if (!sessionModel.IsFromSession) return Redirect(TenantLink.Action(nameof(SelectTeam))!);

if (sessionModel.VenueIsSet == TeamVenueSetKind.NoVenue)
return Redirect(TenantLink.Action(nameof(Confirm))!);

ViewData["TournamentName"] = sessionModel.TournamentName;

if (isNew.HasValue && isNew.Value)
{
sessionModel.Venue = GetVenueEditorComponentModel(new VenueEntity());
sessionModel.VenueIsSet = false;
sessionModel.VenueIsSet = TeamVenueSetKind.NewVenue;
}

var venueEntity = new VenueEntity();
sessionModel.Venue?.MapFormFieldsToEntity(venueEntity);
var venueTeams = new List<VenueTeamRow>();
if (!sessionModel.Venue!.IsNew)
{
Expand All @@ -371,14 +382,14 @@ public async Task<IActionResult> EditVenue(bool? isNew, CancellationToken cancel
return Redirect(TenantLink.Action(nameof(SelectVenue))!);
}

sessionModel.VenueIsSet = TeamVenueSetKind.ExistingVenue;
venueTeams = await _appDb.VenueRepository.GetVenueTeamRowsAsync(new PredicateExpression(VenueTeamFields.VenueId == venueEntity.Id), cancellationToken);
}

var venueEditModel = GetVenueEditModel(venueEntity, sessionModel.Team!.IsNew ? null : new TeamEntity{Id = sessionModel.Team.Id, Name = sessionModel.Team.Name},
venueTeams.Select(vt => vt.TeamName).Distinct().OrderBy(n => n).ToList());

venueEditModel.Venue.MapEntityToFormFields(venueEditModel.VenueEntity!);
if (sessionModel.VenueIsSet) venueEditModel.Venue = sessionModel.Venue!;

return View(Views.ViewNames.TeamApplication.EditVenue, venueEditModel);
}
Expand All @@ -395,7 +406,7 @@ public async Task<IActionResult> EditVenue([FromForm] VenueEditModel venueEditMo
if (!venueEditModel.Venue.IsNew)
{
venueEntity = (await _appDb.VenueRepository.GetVenuesAsync(
new PredicateExpression(VenueFields.Id == venueEditModel.Venue?.Id),
new PredicateExpression(VenueFields.Id == venueEditModel.Venue.Id),
cancellationToken)).FirstOrDefault();

if (venueEntity == null)
Expand Down Expand Up @@ -434,7 +445,7 @@ public async Task<IActionResult> EditVenue([FromForm] VenueEditModel venueEditMo
}

sessionModel.Venue!.MapEntityToFormFields(venueEditModel.VenueEntity!);
sessionModel.VenueIsSet = true;
sessionModel.VenueIsSet = venueEditModel.Venue.IsNew ? TeamVenueSetKind.NewVenue : TeamVenueSetKind.ExistingVenue;
SaveModelToSession(sessionModel);

return Redirect(TenantLink.Action(nameof(Confirm))!);
Expand Down Expand Up @@ -496,9 +507,13 @@ public async Task<IActionResult> Confirm(bool done, CancellationToken cancellati
sessionModel.Team!.MapFormFieldsToEntity(teamInRoundEntity.Team);
// An EXISTING venue MUST be set by its ID, otherwise no venue will be stored for the Team
if(!sessionModel.Venue!.IsNew) teamInRoundEntity.Team.VenueId = sessionModel.Venue.Id;
// Add a new venue, or take over changes to an existing venue
teamInRoundEntity.Team.Venue = new VenueEntity();
sessionModel.Venue.MapFormFieldsToEntity(teamInRoundEntity.Team.Venue);

if (sessionModel.VenueIsSet is TeamVenueSetKind.NewVenue or TeamVenueSetKind.ExistingVenue)
{
// Add a new venue, or take over changes to an existing venue
teamInRoundEntity.Team.Venue = new VenueEntity();
sessionModel.Venue.MapFormFieldsToEntity(teamInRoundEntity.Team.Venue);
}

// Adds the current user as team manager, unless she already is team manager
await AddManagerToTeamEntity(teamInRoundEntity.Team, cancellationToken);
Expand Down Expand Up @@ -700,7 +715,7 @@ private async Task<ApplicationSessionModel> GetNewSessionModel(CancellationToken

return new ApplicationSessionModel
{
TeamInRound = new League.Models.TeamViewModels.TeamInRoundModel {IsNew = true},
TeamInRound = new TeamInRoundModel {IsNew = true},
Team = new TeamEditorComponentModel {HtmlFieldPrefix = nameof(ApplicationSessionModel.Team), IsNew = true},
Venue = new VenueEditorComponentModel {HtmlFieldPrefix = nameof(ApplicationSessionModel.Venue), IsNew = true},
TournamentName = teamApplicationTournament.Name,
Expand Down Expand Up @@ -750,5 +765,8 @@ private async Task AddManagerToTeamEntity(TeamEntity teamEntity, CancellationTok
mot.UserId = GetCurrentUserId() ?? throw new InvalidOperationException("Current user ID must not be null");
}
}

// make sure that any changes are immediately reflected in the user's application cookie
await _signInManager.RefreshSignInAsync(await _signInManager.UserManager.FindByIdAsync(User.FindFirstValue(ClaimTypes.NameIdentifier)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ namespace League.Models.TeamApplicationViewModels;

public class ApplicationSessionModel
{
public enum TeamVenueSetKind
{
NotSet = 0,
NoVenue,
ExistingVenue,
NewVenue
}

/// <summary>
/// The state of the model will be set to <see langword="true"/> whenever it is restored from a session.
/// </summary>
Expand All @@ -15,7 +23,7 @@ public class ApplicationSessionModel
public bool TeamIsSet { get; set; } = false;
public TeamEditorComponentModel? Team { get; set; }

public bool VenueIsSet { get; set; } = false;
public TeamVenueSetKind VenueIsSet { get; set; } = TeamVenueSetKind.NotSet;
public VenueEditorComponentModel? Venue { get; set; }

/// <summary>
Expand Down
12 changes: 12 additions & 0 deletions League/Models/TeamViewModels/TeamVenueSelectModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using TournamentManager.ModelValidators;

namespace League.Models.TeamViewModels;

Expand All @@ -12,4 +13,15 @@ public class TeamVenueSelectModel
public long? VenueId { get; set; }
[HiddenInput]
public string ReturnUrl { get; set; } = string.Empty;

public async Task<bool> ValidateAsync(TeamVenueValidator teamValidator, ModelStateDictionary modelState, CancellationToken cancellationToken)
{
var fact = await teamValidator.CheckAsync(TeamVenueValidator.FactId.VenueIsSetIfRequired, cancellationToken);
if (fact is { IsChecked: true, Success: false }) modelState.AddModelError(fact.FieldNames.First(), fact.Message);

fact = await teamValidator.CheckAsync(TeamVenueValidator.FactId.VenueIsValid, cancellationToken);
if (fact is { IsChecked: true, Success: false }) modelState.AddModelError(fact.FieldNames.First(), fact.Message);

return modelState.IsValid;
}
}
13 changes: 8 additions & 5 deletions League/Views/Shared/Components/TeamEditor/Default.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,18 @@ ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = Model.HtmlFieldPrefix;
<script type="text/javascript" site-on-modal-shown="true" site-on-content-loaded="true">
const onMatchDayOfWeekChangeFunction = function() {
const matchTimeEle = document.getElementById('@Html.IdFor(m => m.MatchTime)');
const matchTimeSpanEle = document.querySelector('span[data-td-target="@nameof(Model.MatchTime)-c"]');
if (this.value === '') {
matchTimeEle.setAttribute('disabled', 'disabled');
matchTimeEle.style.cursor = 'not-allowed';
matchTimeEle.value = '';
document.querySelector('span[data-td-target="@nameof(Model.MatchTime)-c"]').style.cursor = 'not-allowed';
matchTimeEle.setAttribute('readonly', '');
matchTimeEle.style.cursor = 'not-allowed';
matchTimeEle.style.backgroundColor = 'var(--bs-gray-100)';
matchTimeSpanEle.style.display = 'none';
} else {
matchTimeEle.removeAttribute('disabled');
matchTimeEle.removeAttribute('readonly');
matchTimeEle.style.cursor = 'auto';
document.querySelector('span[data-td-target="@nameof(Model.MatchTime)-c"]').style.cursor = 'pointer';
matchTimeEle.style.backgroundColor = '';
matchTimeSpanEle.style.display = 'block';
}
};
const matchDayOfWeek = document.getElementById('@Html.IdFor(m => m.MatchDayOfWeek)');
Expand Down
2 changes: 1 addition & 1 deletion League/Views/Shared/Components/TeamEditor/Default.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Not specified" xml:space="preserve">
<value>Nicht bestimmt</value>
<value>Nicht festgelegt</value>
</data>
<data name="Please select a weekday" xml:space="preserve">
<value>Bitte einen Wochentag auswählen</value>
Expand Down
2 changes: 1 addition & 1 deletion League/Views/Shared/Components/VenueEditor/Default.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Not specified" xml:space="preserve">
<value>Nicht bestimmt</value>
<value>Nicht festgelegt</value>
</data>
<data name="Please select a weekday" xml:space="preserve">
<value>Bitte einen Wochentag auswählen</value>
Expand Down
Loading

0 comments on commit b8059ae

Please sign in to comment.