diff --git a/League/Components/VenueEditorComponentModel.cs b/League/Components/VenueEditorComponentModel.cs index da63ef48..a48f880e 100644 --- a/League/Components/VenueEditorComponentModel.cs +++ b/League/Components/VenueEditorComponentModel.cs @@ -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; } @@ -81,7 +81,7 @@ 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; @@ -89,7 +89,7 @@ public void MapFormFieldsToEntity(VenueEntity venueEntity) 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; diff --git a/League/Components/VenueSelector.cs b/League/Components/VenueSelector.cs index f1cb375e..116d0aa6 100644 --- a/League/Components/VenueSelector.cs +++ b/League/Components/VenueSelector.cs @@ -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 _logger; public VenueSelector(ITenantContext tenantContext, ILogger logger) { - _tenantContext = tenantContext; _appDb = tenantContext.DbContext.AppDb; _logger = logger; } diff --git a/League/Controllers/Team.cs b/League/Controllers/Team.cs index 91ec1f8d..8237c505 100644 --- a/League/Controllers/Team.cs +++ b/League/Controllers/Team.cs @@ -305,10 +305,10 @@ public async Task 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(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), @@ -319,19 +319,21 @@ public async Task 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(nameof(MyTeamMessageModel.MyTeamMessage), new MyTeamMessageModel.MyTeamMessage { AlertType = SiteAlertTagHelper.AlertType.Danger, MessageId = MyTeamMessageModel.MessageId.TeamDataFailure}); + TempData.Put(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(nameof(MyTeamMessageModel.MyTeamMessage), new MyTeamMessageModel.MyTeamMessage { AlertType = SiteAlertTagHelper.AlertType.Success, MessageId = MyTeamMessageModel.MessageId.VenueCreateSuccess}); + TempData.Put(nameof(MyTeamMessageModel.MyTeamMessage), new MyTeamMessageModel.MyTeamMessage { AlertType = SiteAlertTagHelper.AlertType.Success, MessageId = MyTeamMessageModel.MessageId.VenueSelectSuccess}); } else { diff --git a/League/Controllers/TeamApplication.cs b/League/Controllers/TeamApplication.cs index 09b71203..8f65c837 100644 --- a/League/Controllers/TeamApplication.cs +++ b/League/Controllers/TeamApplication.cs @@ -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; @@ -18,6 +19,7 @@ using TournamentManager.DAL.TypedViewClasses; using TournamentManager.ModelValidators; using TournamentManager.MultiTenancy; +using TeamVenueSetKind = League.Models.TeamApplicationViewModels.ApplicationSessionModel.TeamVenueSetKind; namespace League.Controllers; @@ -35,6 +37,7 @@ public class TeamApplication : AbstractController private readonly GoogleConfiguration _googleConfig; private readonly Axuno.BackgroundTask.IBackgroundQueue _queue; private readonly SendEmailTask _sendEmailTask; + private readonly SignInManager _signInManager; private const string TeamApplicationSessionName = "TeamApplicationSession"; public TeamApplication(ITenantContext tenantContext, @@ -42,7 +45,8 @@ public TeamApplication(ITenantContext tenantContext, IStringLocalizer localizer, IAuthorizationService authorizationService, RegionInfo regionInfo, IConfiguration configuration, Axuno.BackgroundTask.IBackgroundQueue queue, - SendEmailTask sendEmailTask, ILogger logger) + SendEmailTask sendEmailTask, SignInManager signInManger, + ILogger logger) { _tenantContext = tenantContext; _timeZoneConverter = timeZoneConverter; @@ -53,6 +57,7 @@ public TeamApplication(ITenantContext tenantContext, _sendEmailTask = sendEmailTask; _appDb = tenantContext.DbContext.AppDb; _authorizationService = authorizationService; + _signInManager = signInManger; _logger = logger; } @@ -133,7 +138,7 @@ public async Task 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; @@ -204,7 +209,7 @@ public async Task 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); @@ -219,7 +224,7 @@ public async Task 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) @@ -284,6 +289,7 @@ public async Task SelectVenue(CancellationToken cancellationToken ViewData["TournamentName"] = sessionModel.TournamentName; sessionModel.Venue!.IsNew = true; + sessionModel.VenueIsSet = TeamVenueSetKind.NotSet; SaveModelToSession(sessionModel); var teamEntity = new TeamEntity(); @@ -299,7 +305,7 @@ public async Task 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 } @@ -325,24 +331,24 @@ public async Task 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")] @@ -350,15 +356,20 @@ public async Task EditVenue(bool? isNew, CancellationToken cancel { 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(); if (!sessionModel.Venue!.IsNew) { @@ -371,6 +382,7 @@ public async Task 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); } @@ -378,7 +390,6 @@ public async Task EditVenue(bool? isNew, CancellationToken cancel 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); } @@ -395,7 +406,7 @@ public async Task 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) @@ -434,7 +445,7 @@ public async Task 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))!); @@ -496,9 +507,13 @@ public async Task 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); @@ -700,7 +715,7 @@ private async Task 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, @@ -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))); } } diff --git a/League/Models/TeamApplicationViewModels/ApplicationSessionModel.cs b/League/Models/TeamApplicationViewModels/ApplicationSessionModel.cs index bf5e01aa..d005b84a 100644 --- a/League/Models/TeamApplicationViewModels/ApplicationSessionModel.cs +++ b/League/Models/TeamApplicationViewModels/ApplicationSessionModel.cs @@ -4,6 +4,14 @@ namespace League.Models.TeamApplicationViewModels; public class ApplicationSessionModel { + public enum TeamVenueSetKind + { + NotSet = 0, + NoVenue, + ExistingVenue, + NewVenue + } + /// /// The state of the model will be set to whenever it is restored from a session. /// @@ -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; } /// diff --git a/League/Models/TeamViewModels/TeamVenueSelectModel.cs b/League/Models/TeamViewModels/TeamVenueSelectModel.cs index b7f85a6b..b0340e5c 100644 --- a/League/Models/TeamViewModels/TeamVenueSelectModel.cs +++ b/League/Models/TeamViewModels/TeamVenueSelectModel.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; +using TournamentManager.ModelValidators; namespace League.Models.TeamViewModels; @@ -12,4 +13,15 @@ public class TeamVenueSelectModel public long? VenueId { get; set; } [HiddenInput] public string ReturnUrl { get; set; } = string.Empty; + + public async Task 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; + } } diff --git a/League/Views/Shared/Components/TeamEditor/Default.cshtml b/League/Views/Shared/Components/TeamEditor/Default.cshtml index d1335cff..5c8827cf 100644 --- a/League/Views/Shared/Components/TeamEditor/Default.cshtml +++ b/League/Views/Shared/Components/TeamEditor/Default.cshtml @@ -67,15 +67,18 @@ ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = Model.HtmlFieldPrefix;