diff --git a/Snittlistan.Test/ApiControllers/Infrastructure/InMemoryContext.cs b/Snittlistan.Test/ApiControllers/Infrastructure/InMemoryContext.cs index 8d787a1b..cf5354a7 100644 --- a/Snittlistan.Test/ApiControllers/Infrastructure/InMemoryContext.cs +++ b/Snittlistan.Test/ApiControllers/Infrastructure/InMemoryContext.cs @@ -1,4 +1,6 @@ -using System.Collections; +#nullable enable + +using System.Collections; using System.Collections.ObjectModel; using System.Data.Entity; using System.Data.Entity.Infrastructure; @@ -6,9 +8,8 @@ using System.Reflection; using Snittlistan.Web.Infrastructure.Database; -#nullable enable - namespace Snittlistan.Test.ApiControllers.Infrastructure; + public sealed class InMemoryDbSet : IDbSet, IDbAsyncEnumerable where T : class { private readonly IdGenerator _generator; @@ -106,19 +107,34 @@ public InMemoryContext() IdGenerator generator = new(); PublishedTasks = new InMemoryDbSet(generator); Tenants = new InMemoryDbSet(generator); - Teams = new InMemoryDbSet(generator); - Hallar = new InMemoryDbSet(generator); + Team = new InMemoryDbSet(generator); + HallRef = new InMemoryDbSet(generator); + Hall = new InMemoryDbSet(generator); RosterMails = new InMemoryDbSet(generator); ChangeLogs = new InMemoryDbSet(generator); KeyValueProperties = new InMemoryDbSet(generator); SentEmails = new InMemoryDbSet(generator); + Match = new InMemoryDbSet(generator); + TeamRef = new InMemoryDbSet(generator); + OilProfile = new InMemoryDbSet(generator); + VMatchHeadInfo = new InMemoryDbSet(generator); } public IDbSet PublishedTasks { get; } - public IDbSet Teams { get; } + public IDbSet Team { get; } + + public IDbSet HallRef { get; } + + public IDbSet Hall { get; } + + public IDbSet Match { get; } - public IDbSet Hallar { get; } + public IDbSet TeamRef { get; } + + public IDbSet OilProfile { get; } + + public IDbSet VMatchHeadInfo { get; } public IDbSet Tenants { get; } @@ -130,7 +146,7 @@ public InMemoryContext() public IDbSet SentEmails { get; } - public DbChangeTracker ChangeTracker => throw new NotImplementedException(); + public DbChangeTracker ChangeTracker { get; } = null!; public int SaveChanges() { @@ -141,4 +157,8 @@ public Task SaveChangesAsync() { return Task.FromResult(0); } + + public void Dispose() + { + } } diff --git a/Snittlistan.Test/Domain/MatchResult_MatchCommentary.cs b/Snittlistan.Test/Domain/MatchResult_MatchCommentary.cs index 90608b11..802cd45b 100644 --- a/Snittlistan.Test/Domain/MatchResult_MatchCommentary.cs +++ b/Snittlistan.Test/Domain/MatchResult_MatchCommentary.cs @@ -1,5 +1,6 @@ #nullable enable +using Castle.Core.Logging; using EventStoreLite; using Moq; using NUnit.Framework; @@ -168,13 +169,16 @@ await Transact(async session => session, eventStoreSession, Databases, + new(() => Databases), Container.Resolve(), Container.Resolve(), CurrentTenant, Container.Resolve(), - Mock.Of(MockBehavior.Strict)); + Mock.Of(MockBehavior.Strict), + NullLogger.Instance); CommandExecutor commandExecutor = new( compositionRoot, + Databases, compositionRoot.CorrelationId, null, string.Empty); @@ -184,6 +188,7 @@ await Transact(async session => }; HandlerContext context = new( compositionRoot, + Databases, command, new("hostname", "favicon", "appleTouchIcon", "appleTouchIconSize", "webAppTitle", -1, "teamFullName"), Guid.NewGuid(), diff --git a/Snittlistan.Web/Areas/V1/Controllers/SearchController.cs b/Snittlistan.Web/Areas/V1/Controllers/SearchController.cs index d75c560a..e6cbadf2 100644 --- a/Snittlistan.Web/Areas/V1/Controllers/SearchController.cs +++ b/Snittlistan.Web/Areas/V1/Controllers/SearchController.cs @@ -16,7 +16,7 @@ public JsonResult TeamsQuickSearch(string term) } var query = - from team in CompositionRoot.Databases.Bits.Teams + from team in CompositionRoot.Databases.Bits.Team where team.TeamAlias.StartsWith(term) orderby team.TeamAlias select new @@ -35,7 +35,7 @@ public JsonResult LocationsQuickSearch(string term) } var query = - from hall in CompositionRoot.Databases.Bits.Hallar + from hall in CompositionRoot.Databases.Bits.Hall where hall.HallName.StartsWith(term) orderby hall.HallName select new @@ -43,6 +43,6 @@ orderby hall.HallName label = hall.HallName }; - return Json(term, JsonRequestBehavior.AllowGet); + return Json(query, JsonRequestBehavior.AllowGet); } } diff --git a/Snittlistan.Web/Areas/V2/Controllers/AdminTasksController.cs b/Snittlistan.Web/Areas/V2/Controllers/AdminTasksController.cs index 5c67a7a4..38c6c8f5 100644 --- a/Snittlistan.Web/Areas/V2/Controllers/AdminTasksController.cs +++ b/Snittlistan.Web/Areas/V2/Controllers/AdminTasksController.cs @@ -12,6 +12,7 @@ using Snittlistan.Web.Infrastructure.Indexes; using Snittlistan.Web.Models; using Snittlistan.Web.Services; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Web; using System.Web.Mvc; @@ -271,17 +272,19 @@ await CompositionRoot.Databases.Snittlistan.KeyValueProperties.SingleOrDefaultAs public async Task Features(TenantFeaturesViewModel vm) { UpdateFeaturesCommandHandler.Command command = new( - vm.RosterMailEnabled); + vm.RosterMailEnabled, + vm.RosterMailDelayMinutes); await ExecuteCommand(command); return RedirectToAction("Index"); } - public class TenantFeaturesViewModel + public class TenantFeaturesViewModel : IValidatableObject { public TenantFeaturesViewModel(TenantFeatures tenantFeatures) { RosterMailEnabled = tenantFeatures.RosterMailEnabled; + RosterMailDelayMinutes = tenantFeatures.RosterMailDelayMinutes; } public TenantFeaturesViewModel() @@ -289,5 +292,20 @@ public TenantFeaturesViewModel() } public bool RosterMailEnabled { get; set; } + + public int RosterMailDelayMinutes { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (RosterMailDelayMinutes < 1) + { + yield return new("Must be at least 1", new[] { nameof(RosterMailDelayMinutes) }); + } + + if (RosterMailDelayMinutes > 60) + { + yield return new("Must be at most 60", new[] { nameof(RosterMailDelayMinutes) }); + } + } } } diff --git a/Snittlistan.Web/Areas/V2/Controllers/Api/TaskController.cs b/Snittlistan.Web/Areas/V2/Controllers/Api/TaskController.cs index 12882c6a..9ecc3f41 100644 --- a/Snittlistan.Web/Areas/V2/Controllers/Api/TaskController.cs +++ b/Snittlistan.Web/Areas/V2/Controllers/Api/TaskController.cs @@ -56,32 +56,40 @@ public async Task Post(TaskRequest request) $"throttled due to unhandled exception (allowance = {cacheItem.Allowance:N2})"); } - // check for published task - PublishedTask? publishedTask = - await CompositionRoot.Databases.Snittlistan.PublishedTasks.SingleOrDefaultAsync( - x => x.MessageId == request.MessageId); - if (publishedTask is null) - { - return BadRequest($"No published task found with message id {request.MessageId}"); - } - - if (publishedTask.HandledDate.HasValue) - { - return Ok($"task with message id {publishedTask.MessageId} already handled"); - } - + IDisposable scope = NLog.NestedDiagnosticsLogicalContext.Push(taskObject.BusinessKey); try { - using IDisposable scope = NLog.NestedDiagnosticsLogicalContext.Push(taskObject.BusinessKey); - Logger.Info("Begin"); - bool handled = await HandleMessage( - taskObject, - request.CorrelationId ?? default, - request.MessageId ?? default); - if (handled) + Logger.Info("begin"); + IHttpActionResult result = await Transact(async databases => { - publishedTask.MarkHandled(DateTime.Now); - } + // check for published task + PublishedTask? publishedTask = + await databases.Snittlistan.PublishedTasks.SingleOrDefaultAsync( + x => x.MessageId == request.MessageId); + if (publishedTask is null) + { + return BadRequest($"no published task found with message id {request.MessageId}"); + } + + if (publishedTask.HandledDate.HasValue) + { + return Ok($"task with message id {publishedTask.MessageId} already handled"); + } + + bool handled = await HandleMessage( + taskObject, + request.CorrelationId ?? default, + request.MessageId ?? default, + databases); + if (handled) + { + publishedTask.MarkHandled(DateTime.Now); + } + + return Ok(); + }); + + return result; } catch (Exception ex) { @@ -94,29 +102,30 @@ await CompositionRoot.Databases.Snittlistan.PublishedTasks.SingleOrDefaultAsync( } finally { - Logger.Info("End"); + Logger.Info("end"); + scope.Dispose(); } - - return Ok(); } private async Task HandleMessage( TaskBase taskObject, Guid correlationId, - Guid causationId) + Guid causationId, + Databases databases) { Type handlerType = typeof(ITaskHandler<>).MakeGenericType(taskObject.GetType()); MethodInfo handleMethod = handlerType.GetMethod(nameof(ITaskHandler.Handle)); TaskPublisher taskPublisher = new( CompositionRoot.CurrentTenant, - CompositionRoot.Databases, + databases, CompositionRoot.MsmqFactory, correlationId, causationId); IHandlerContext handlerContext = (IHandlerContext)Activator.CreateInstance( typeof(HandlerContext<>).MakeGenericType(taskObject.GetType()), CompositionRoot, + databases, taskObject, CompositionRoot.CurrentTenant, correlationId, @@ -147,6 +156,21 @@ private async Task HandleMessage( return true; } + + private async Task Transact(Func> func) + { + using Databases databases = CompositionRoot.DatabasesFactory.Create(); + TResult result = await func.Invoke(databases); + int changesSaved = databases.Snittlistan.SaveChanges(); + if (changesSaved > 0) + { + Logger.InfoFormat( + "saved {changesSaved} to database", + changesSaved); + } + + return result; + } } public class TaskRequest diff --git a/Snittlistan.Web/Areas/V2/Controllers/RosterController.cs b/Snittlistan.Web/Areas/V2/Controllers/RosterController.cs index 9cc361e7..eb1b68c7 100644 --- a/Snittlistan.Web/Areas/V2/Controllers/RosterController.cs +++ b/Snittlistan.Web/Areas/V2/Controllers/RosterController.cs @@ -377,7 +377,7 @@ public ActionResult Print( } [Authorize(Roles = WebsiteRoles.Uk.UkTasks)] - public async Task EditPlayers(string rosterId) + public ActionResult EditPlayers(string rosterId) { Roster roster = CompositionRoot.DocumentSession .Include(r => r.Players) @@ -392,7 +392,6 @@ public async Task EditPlayers(string rosterId) .Where(p => p.PlayerStatus == Player.Status.Active) .ToList(); - TenantFeatures? features = await CompositionRoot.GetFeatures(); EditRosterPlayersViewModel vm = new() { RosterViewModel = CompositionRoot.DocumentSession.LoadRosterViewModel(roster), @@ -407,7 +406,7 @@ public async Task EditPlayers(string rosterId, RosterPlayersViewMo { if (ModelState.IsValid == false) { - return await EditPlayers(rosterId); + return EditPlayers(rosterId); } Roster roster = CompositionRoot.DocumentSession.Load(rosterId); @@ -488,8 +487,8 @@ public async Task EditPlayers(string rosterId, RosterPlayersViewMo break; } - TenantFeatures? features = await CompositionRoot.GetFeatures(); - if ((features?.RosterMailEnabled ?? false) == false) + TenantFeatures features = await CompositionRoot.GetFeatures(); + if (features.RosterMailEnabled == false) { Logger.Info("RosterMailEnabled evaluated to false"); break; diff --git a/Snittlistan.Web/Areas/V2/Views/AdminTasks/Features.cshtml b/Snittlistan.Web/Areas/V2/Views/AdminTasks/Features.cshtml index 7555c252..e70483bb 100644 --- a/Snittlistan.Web/Areas/V2/Views/AdminTasks/Features.cshtml +++ b/Snittlistan.Web/Areas/V2/Views/AdminTasks/Features.cshtml @@ -20,6 +20,15 @@ +
+ @Html.LabelFor(x => x.RosterMailDelayMinutes, new { @class = "control-label" }) +
+ @Html.TextBoxFor(x => x.RosterMailDelayMinutes) + + Antal minuter att vänta innan laguttagningarna skickas ut per mail, efter en uppdatering. + +
+
diff --git a/Snittlistan.Web/Areas/V2/Views/AdminTasks/Features.generated.cs b/Snittlistan.Web/Areas/V2/Views/AdminTasks/Features.generated.cs index 9592174f..9ab864e7 100644 --- a/Snittlistan.Web/Areas/V2/Views/AdminTasks/Features.generated.cs +++ b/Snittlistan.Web/Areas/V2/Views/AdminTasks/Features.generated.cs @@ -87,6 +87,32 @@ public override void Execute() +WriteLiteral("
\r\n "); + + + + #line 24 "..\..\Areas\V2\Views\AdminTasks\Features.cshtml" + Write(Html.LabelFor(x => x.RosterMailDelayMinutes, new { @class = "control-label" })); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n "); + + + + #line 26 "..\..\Areas\V2\Views\AdminTasks\Features.cshtml" + Write(Html.TextBoxFor(x => x.RosterMailDelayMinutes)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n Antal mi" + +"nuter att vänta innan laguttagningarna skickas ut per mail, efter en uppdatering" + +".\r\n \r\n
\r\n
\r\n"); + + + WriteLiteral("
\r\n
\r\n" + " \r\n (TCommand command) where TCommand : class { + Logger.Info("execute command {@command}", command); Type handlerType = typeof(ICommandHandler<>).MakeGenericType(command.GetType()); MethodInfo handleMethod = handlerType.GetMethod(nameof(ICommandHandler.Handle)); object handler = compositionRoot.Kernel.Resolve(handlerType); TaskPublisher taskPublisher = new( compositionRoot.CurrentTenant, - compositionRoot.Databases, + databases, compositionRoot.MsmqFactory, correlationId, causationId); IHandlerContext handlerContext = (IHandlerContext)Activator.CreateInstance( typeof(HandlerContext<>).MakeGenericType(command.GetType()), compositionRoot, + databases, command, compositionRoot.CurrentTenant, correlationId, diff --git a/Snittlistan.Web/Commands/CreateRosterMailCommandHandler.cs b/Snittlistan.Web/Commands/CreateRosterMailCommandHandler.cs index 93765f62..4c623d29 100644 --- a/Snittlistan.Web/Commands/CreateRosterMailCommandHandler.cs +++ b/Snittlistan.Web/Commands/CreateRosterMailCommandHandler.cs @@ -2,6 +2,7 @@ using Snittlistan.Queue.Messages; using Snittlistan.Web.Infrastructure; +using Snittlistan.Web.Models; namespace Snittlistan.Web.Commands; @@ -15,15 +16,17 @@ from rosterMail in CompositionRoot.Databases.Snittlistan.RosterMails && rosterMail.PublishedDate == null select rosterMail.RosterKey; string[] rosterIds = await query.ToArrayAsync(); + Logger.InfoFormat("roster mails found: {@rosterIds}", rosterIds); if (rosterIds.Any() == false) { + TenantFeatures features = await CompositionRoot.GetFeatures(); _ = CompositionRoot.Databases.Snittlistan.RosterMails.Add( new(context.Payload.RosterKey)); PublishRosterMailsTask task = new( context.Payload.RosterKey, context.Payload.RosterLink, context.Payload.UserProfileLink); - context.PublishMessage(task, DateTime.Now.AddMinutes(10)); + context.PublishMessage(task, DateTime.Now.AddMinutes(features.RosterMailDelayMinutes)); } } diff --git a/Snittlistan.Web/Commands/PublishRosterMailCommandHandler.cs b/Snittlistan.Web/Commands/PublishRosterMailCommandHandler.cs index ce591307..604ebcfa 100644 --- a/Snittlistan.Web/Commands/PublishRosterMailCommandHandler.cs +++ b/Snittlistan.Web/Commands/PublishRosterMailCommandHandler.cs @@ -2,6 +2,7 @@ using Snittlistan.Web.Areas.V2.Domain; using Snittlistan.Web.Infrastructure; +using Snittlistan.Web.Infrastructure.Database; using Snittlistan.Web.Models; namespace Snittlistan.Web.Commands; @@ -9,7 +10,7 @@ namespace Snittlistan.Web.Commands; public class PublishRosterMailCommandHandler : HandleMailCommandHandler { - protected override Task CreateEmail(HandlerContext context) + protected override async Task CreateEmail(HandlerContext context) { Roster roster = CompositionRoot.DocumentSession .Include(x => x.Players) @@ -23,6 +24,8 @@ protected override Task CreateEmail(HandlerContext c ? CompositionRoot.DocumentSession.Load(roster.TeamLeader).Name : string.Empty; bool needsAccept = roster.AcceptedPlayers.Contains(context.Payload.PlayerId) == false; + Bits_VMatchHeadInfo matchHeadInfo = await context.Databases.Bits.VMatchHeadInfo.SingleAsync( + x => x.ExternalMatchId == roster.BitsMatchId); UpdateRosterEmail email = new( context.Payload.RecipientEmail, context.Payload.RecipientName, @@ -34,8 +37,14 @@ protected override Task CreateEmail(HandlerContext c roster.Turn, context.Payload.RosterLink, context.Payload.UserProfileLink, - needsAccept); - return Task.FromResult(email); + needsAccept, + matchHeadInfo.HomeTeamAlias, + matchHeadInfo.AwayTeamAlias, + matchHeadInfo.HallName, + matchHeadInfo.OilProfileId, + matchHeadInfo.OilProfileName, + matchHeadInfo.MatchDateTime); + return email; } protected override RatePerSeconds GetRate(HandlerContext context) diff --git a/Snittlistan.Web/Commands/PublishRosterMailsCommandHandler.cs b/Snittlistan.Web/Commands/PublishRosterMailsCommandHandler.cs index a92a4a14..36482d72 100644 --- a/Snittlistan.Web/Commands/PublishRosterMailsCommandHandler.cs +++ b/Snittlistan.Web/Commands/PublishRosterMailsCommandHandler.cs @@ -47,7 +47,7 @@ public override async Task Handle(HandlerContext context) string[] propertyKeys = affectedPlayers.Select(UserSettings.GetKey).ToArray(); Dictionary properties = Enumerable.ToDictionary( - await CompositionRoot.Databases.Snittlistan.KeyValueProperties.Where( + await context.Databases.Snittlistan.KeyValueProperties.Where( x => propertyKeys.Contains(x.Key)) .ToArrayAsync(), x => x.Key, @@ -100,7 +100,7 @@ await CompositionRoot.Databases.Snittlistan.KeyValueProperties.Where( } RosterMail rosterMail = - await CompositionRoot.Databases.Snittlistan.RosterMails.SingleAsync( + await context.Databases.Snittlistan.RosterMails.SingleAsync( x => x.RosterKey == context.Payload.RosterKey && x.PublishedDate == null); rosterMail.MarkPublished(DateTime.Now); } diff --git a/Snittlistan.Web/Commands/UpdateFeaturesCommandHandler.cs b/Snittlistan.Web/Commands/UpdateFeaturesCommandHandler.cs index 2b718c6f..54064249 100644 --- a/Snittlistan.Web/Commands/UpdateFeaturesCommandHandler.cs +++ b/Snittlistan.Web/Commands/UpdateFeaturesCommandHandler.cs @@ -21,14 +21,17 @@ await CompositionRoot.Databases.Snittlistan.KeyValueProperties.SingleOrDefaultAs new( CompositionRoot.CurrentTenant.TenantId, TenantFeatures.Key, - new TenantFeatures(context.Payload.RosterMailEnabled))); + new TenantFeatures( + context.Payload.RosterMailEnabled, + context.Payload.RosterMailDelayMinutes))); } else { settingsProperty.ModifyValue( x => x with { - RosterMailEnabled = context.Payload.RosterMailEnabled + RosterMailEnabled = context.Payload.RosterMailEnabled, + RosterMailDelayMinutes = context.Payload.RosterMailDelayMinutes }, x => Logger.InfoFormat("before: {@x}", x), x => Logger.InfoFormat("after: {@x}", x)); @@ -36,5 +39,6 @@ await CompositionRoot.Databases.Snittlistan.KeyValueProperties.SingleOrDefaultAs } public record Command( - bool RosterMailEnabled); + bool RosterMailEnabled, + int RosterMailDelayMinutes); } diff --git a/Snittlistan.Web/Controllers/AbstractController.cs b/Snittlistan.Web/Controllers/AbstractController.cs index 12c5d5e9..618a9621 100644 --- a/Snittlistan.Web/Controllers/AbstractController.cs +++ b/Snittlistan.Web/Controllers/AbstractController.cs @@ -109,6 +109,7 @@ private CommandExecutor CreateCommandExecutor() { return new CommandExecutor( CompositionRoot, + CompositionRoot.Databases, CompositionRoot.CorrelationId, null, User.CustomIdentity.Name); diff --git a/Snittlistan.Web/Infrastructure/Attributes/SaveChangesAttribute.cs b/Snittlistan.Web/Infrastructure/Attributes/SaveChangesAttribute.cs index a9cd1481..99d1ba76 100644 --- a/Snittlistan.Web/Infrastructure/Attributes/SaveChangesAttribute.cs +++ b/Snittlistan.Web/Infrastructure/Attributes/SaveChangesAttribute.cs @@ -1,12 +1,18 @@ -using System.Web.Http.Filters; +#nullable enable + +using System.Web.Http.Filters; using Snittlistan.Web.Controllers; namespace Snittlistan.Web.Infrastructure.Attributes; + public class SaveChangesAttribute : ActionFilterAttribute { - public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) + public override async Task OnActionExecutedAsync( + HttpActionExecutedContext actionExecutedContext, + CancellationToken cancellationToken) { - if (actionExecutedContext.ActionContext.ControllerContext.Controller is not AbstractApiController controller + if (actionExecutedContext.ActionContext.ControllerContext.Controller + is not AbstractApiController controller || actionExecutedContext.Exception != null) { return; diff --git a/Snittlistan.Web/Infrastructure/CompositionRoot.cs b/Snittlistan.Web/Infrastructure/CompositionRoot.cs index 41a6a4b4..67f6863d 100644 --- a/Snittlistan.Web/Infrastructure/CompositionRoot.cs +++ b/Snittlistan.Web/Infrastructure/CompositionRoot.cs @@ -1,5 +1,6 @@ #nullable enable +using Castle.Core.Logging; using Castle.MicroKernel; using EventStoreLite; using Postal; @@ -15,11 +16,13 @@ public record CompositionRoot( Raven.Client.IDocumentSession DocumentSession, IEventStoreSession EventStoreSession, Databases Databases, + DatabasesFactory DatabasesFactory, MsmqFactory MsmqFactory, EventStore EventStore, Tenant CurrentTenant, IEmailService EmailService, - IBitsClient BitsClient) + IBitsClient BitsClient, + ILogger Logger) { public Guid CorrelationId { @@ -36,11 +39,28 @@ public Guid CorrelationId } } - public async Task GetFeatures() + public async Task GetFeatures() { KeyValueProperty? settingsProperty = await Databases.Snittlistan.KeyValueProperties.SingleOrDefaultAsync( x => x.Key == TenantFeatures.Key && x.TenantId == CurrentTenant.TenantId); - return settingsProperty?.Value as TenantFeatures; + do + { + if (settingsProperty is null) + { + Logger.Info("no tenant features found"); + break; + } + + if (settingsProperty.Value is TenantFeatures tenantFeatures) + { + Logger.InfoFormat("found tenant features {@tenantFeatures}", tenantFeatures); + return tenantFeatures; + } + } + while (false); + + Logger.Warn("no tenant features found, or unusable"); + return TenantFeatures.Default; } } diff --git a/Snittlistan.Web/Infrastructure/Database/BitsContext.cs b/Snittlistan.Web/Infrastructure/Database/BitsContext.cs index 1eea7dce..d9419058 100644 --- a/Snittlistan.Web/Infrastructure/Database/BitsContext.cs +++ b/Snittlistan.Web/Infrastructure/Database/BitsContext.cs @@ -1,15 +1,30 @@ -using Npgsql.NameTranslation; -using System.Data.Entity; +#nullable enable -#nullable enable +using Npgsql.NameTranslation; namespace Snittlistan.Web.Infrastructure.Database; public class BitsContext : DbContext, IBitsContext { - public IDbSet Teams { get; set; } = null!; + public BitsContext() + { + Configuration.AutoDetectChangesEnabled = false; + Configuration.LazyLoadingEnabled = false; + } + + public IDbSet Team { get; set; } = null!; + + public IDbSet HallRef { get; set; } = null!; + + public IDbSet Hall { get; set; } = null!; - public IDbSet Hallar { get; set; } = null!; + public IDbSet Match { get; set; } = null!; + + public IDbSet TeamRef { get; set; } = null!; + + public IDbSet OilProfile { get; set; } = null!; + + public IDbSet VMatchHeadInfo { get; set; } = null!; protected override void OnModelCreating(DbModelBuilder modelBuilder) { @@ -17,7 +32,18 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) System.Data.Entity.Database.SetInitializer(new NullDatabaseInitializer()); NpgsqlSnakeCaseNameTranslator mapper = new(); _ = modelBuilder.HasDefaultSchema("bits"); - modelBuilder.Properties().Configure(x => x.HasColumnName(mapper.TranslateMemberName(x.ClrPropertyInfo.Name))); - modelBuilder.Types().Configure(x => x.ToTable(mapper.TranslateMemberName(x.ClrType.Name.Replace("Bits", string.Empty)))); + modelBuilder.Properties().Configure(x => x.HasColumnName( + mapper.TranslateMemberName(x.ClrPropertyInfo.Name))); + modelBuilder.Types().Configure(x => x.ToTable( + mapper.TranslateMemberName(x.ClrType.Name.Replace("Bits_", string.Empty)))); + + //.WithRequiredPrincipal(x => x.Hall!); + // .Map(x => x.MapKey("hall_id")); + //_ = modelBuilder.Entity() + // .HasOptional(x => x.Hall) + // .WithOptionalDependent(x => x!.HallRef) + // .Map(x => x.MapKey("hall_id")); + + //_ = modelBuilder.Entity } } diff --git a/Snittlistan.Web/Infrastructure/Database/Bits_Hall.cs b/Snittlistan.Web/Infrastructure/Database/Bits_Hall.cs index d4a52e8c..42934325 100644 --- a/Snittlistan.Web/Infrastructure/Database/Bits_Hall.cs +++ b/Snittlistan.Web/Infrastructure/Database/Bits_Hall.cs @@ -1,10 +1,17 @@ #nullable enable +using System.ComponentModel.DataAnnotations; + namespace Snittlistan.Web.Infrastructure.Database; public class Bits_Hall { - public int ExternalHallId { get; set; } + [Key] + public int HallId { get; private set; } + + public int ExternalHallId { get; private set; } + + public string HallName { get; private set; } = string.Empty; - public string HallName { get; set; } = null!; + public Bits_HallRef HallRef { get; set; } = null!; } diff --git a/Snittlistan.Web/Infrastructure/Database/Bits_HallRef.cs b/Snittlistan.Web/Infrastructure/Database/Bits_HallRef.cs new file mode 100644 index 00000000..97c50762 --- /dev/null +++ b/Snittlistan.Web/Infrastructure/Database/Bits_HallRef.cs @@ -0,0 +1,17 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; + +namespace Snittlistan.Web.Infrastructure.Database; + +public class Bits_HallRef +{ + [Key] + public int HallRefId { get; private set; } + + public int ExternalHallId { get; private set; } + + public int? HallId { get; private set; } + + public string HallName { get; private set; } = string.Empty; +} diff --git a/Snittlistan.Web/Infrastructure/Database/Bits_Match.cs b/Snittlistan.Web/Infrastructure/Database/Bits_Match.cs new file mode 100644 index 00000000..912ea668 --- /dev/null +++ b/Snittlistan.Web/Infrastructure/Database/Bits_Match.cs @@ -0,0 +1,59 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Snittlistan.Web.Infrastructure.Database; + +/* + * SELECT + m.external_match_id, + tr_home.team_name AS home_team_name, + tr_home.team_alias AS home_team_alias, + tr_away.team_name AS away_team_name, + tr_away.team_alias AS away_team_alias, + op.oil_profile_name, + hr.hall_name +FROM + bits.match m + JOIN bits.team_ref tr_home ON m.match_home_team_ref_id = tr_home.team_ref_id + JOIN bits.team_ref tr_away ON m.match_away_team_ref_id = tr_away.team_ref_id + JOIN bits.oil_profile op ON m.match_oil_profile_id = op.oil_profile_id + JOIN bits.hall_ref hr ON m.match_hall_ref_id = hr.hall_ref_id +WHERE + m.external_match_id = 3235829; + + */ +public class Bits_Match +{ + [Key] + public int MatchId { get; private set; } + + public int ExternalMatchId { get; private set; } + + public DateTime MatchDateTime { get; private set; } + + [Column("match_home_team_ref_id")] + [ForeignKey(nameof(HomeTeamRef))] + public int HomeTeamRefId { get; private set; } + + public virtual Bits_TeamRef HomeTeamRef { get; private set; } = null!; + + [Column("match_away_team_ref_id")] + [ForeignKey(nameof(AwayTeamRef))] + public int AwayTeamRefId { get; private set; } + + public virtual Bits_TeamRef AwayTeamRef { get; private set; } = null!; + + [Column("match_oil_profile_id")] + [ForeignKey(nameof(OilProfile))] + public int OilProfileId { get; private set; } + + public virtual Bits_OilProfile OilProfile { get; private set; } = null!; + + [Column("match_hall_ref_id")] + [ForeignKey(nameof(HallRef))] + public int HallRefId { get; private set; } + + public Bits_HallRef HallRef { get; private set; } = null!; +} diff --git a/Snittlistan.Web/Infrastructure/Database/Bits_OilProfile.cs b/Snittlistan.Web/Infrastructure/Database/Bits_OilProfile.cs new file mode 100644 index 00000000..e534b3c1 --- /dev/null +++ b/Snittlistan.Web/Infrastructure/Database/Bits_OilProfile.cs @@ -0,0 +1,16 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Snittlistan.Web.Infrastructure.Database; + +public class Bits_OilProfile +{ + [Key] + public int OilProfileId { get; set; } + + [InverseProperty(nameof(Bits_Match.OilProfile))] + public virtual ICollection Matches { get; set; } = + new HashSet(); +} diff --git a/Snittlistan.Web/Infrastructure/Database/Bits_Team.cs b/Snittlistan.Web/Infrastructure/Database/Bits_Team.cs index 9a38ce71..7f2e978d 100644 --- a/Snittlistan.Web/Infrastructure/Database/Bits_Team.cs +++ b/Snittlistan.Web/Infrastructure/Database/Bits_Team.cs @@ -1,9 +1,14 @@ #nullable enable +using System.ComponentModel.DataAnnotations; + namespace Snittlistan.Web.Infrastructure.Database; public class Bits_Team { + [Key] + public int TeamId { get; set; } + public int ExternalTeamId { get; set; } public string TeamName { get; set; } = null!; diff --git a/Snittlistan.Web/Infrastructure/Database/Bits_TeamRef.cs b/Snittlistan.Web/Infrastructure/Database/Bits_TeamRef.cs new file mode 100644 index 00000000..c87011f1 --- /dev/null +++ b/Snittlistan.Web/Infrastructure/Database/Bits_TeamRef.cs @@ -0,0 +1,26 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Snittlistan.Web.Infrastructure.Database; + +public class Bits_TeamRef +{ + [Key] + public int TeamRefId { get; private set; } + + public int ExternalTeamId { get; set; } + + public string TeamName { get; private set; } = string.Empty; + + public string TeamAlias { get; set; } = string.Empty; + + [InverseProperty(nameof(Bits_Match.HomeTeamRef))] + public virtual ICollection HomeMatches { get; private set; } = + new HashSet(); + + [InverseProperty(nameof(Bits_Match.AwayTeamRef))] + public virtual ICollection AwayMatches { get; private set; } = + new HashSet(); +} diff --git a/Snittlistan.Web/Infrastructure/Database/Bits_VMatchHeadInfo.cs b/Snittlistan.Web/Infrastructure/Database/Bits_VMatchHeadInfo.cs new file mode 100644 index 00000000..eb6b747d --- /dev/null +++ b/Snittlistan.Web/Infrastructure/Database/Bits_VMatchHeadInfo.cs @@ -0,0 +1,27 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; + +namespace Snittlistan.Web.Infrastructure.Database; + +public class Bits_VMatchHeadInfo +{ + [Key] + public int ExternalMatchId { get; private set; } + + public DateTime MatchDateTime { get; private set; } + + public string HomeTeamName { get; private set; } = string.Empty; + + public string HomeTeamAlias { get; private set; } = string.Empty; + + public string AwayTeamName { get; private set; } = string.Empty; + + public string AwayTeamAlias { get; private set; } = string.Empty; + + public int OilProfileId { get; private set; } + + public string OilProfileName { get; private set; } = string.Empty; + + public string HallName { get; private set; } = string.Empty; +} diff --git a/Snittlistan.Web/Infrastructure/Database/Databases.cs b/Snittlistan.Web/Infrastructure/Database/Databases.cs index 7aa0ce7e..88445c9c 100644 --- a/Snittlistan.Web/Infrastructure/Database/Databases.cs +++ b/Snittlistan.Web/Infrastructure/Database/Databases.cs @@ -2,7 +2,7 @@ namespace Snittlistan.Web.Infrastructure.Database; -public class Databases +public class Databases : IDisposable { public Databases( ISnittlistanContext snittlistanContext, @@ -15,4 +15,10 @@ public Databases( public ISnittlistanContext Snittlistan { get; } public IBitsContext Bits { get; } + + public void Dispose() + { + Snittlistan.Dispose(); + Bits.Dispose(); + } } diff --git a/Snittlistan.Web/Infrastructure/Database/DatabasesFactory.cs b/Snittlistan.Web/Infrastructure/Database/DatabasesFactory.cs new file mode 100644 index 00000000..8e74bc0d --- /dev/null +++ b/Snittlistan.Web/Infrastructure/Database/DatabasesFactory.cs @@ -0,0 +1,19 @@ +#nullable enable + +namespace Snittlistan.Web.Infrastructure.Database; + +public class DatabasesFactory +{ + private readonly Func databasesFactory; + + public DatabasesFactory(Func databasesFactory) + { + this.databasesFactory = databasesFactory; + } + + public Databases Create() + { + Databases databases = databasesFactory.Invoke(); + return databases; + } +} diff --git a/Snittlistan.Web/Infrastructure/Database/IBitsContext.cs b/Snittlistan.Web/Infrastructure/Database/IBitsContext.cs index 6f2e820a..69929dcf 100644 --- a/Snittlistan.Web/Infrastructure/Database/IBitsContext.cs +++ b/Snittlistan.Web/Infrastructure/Database/IBitsContext.cs @@ -1,12 +1,20 @@ - -using System.Data.Entity; - -#nullable enable +#nullable enable namespace Snittlistan.Web.Infrastructure.Database; -public interface IBitsContext + +public interface IBitsContext : IDisposable { - public IDbSet Teams { get; } + public IDbSet Team { get; } + + public IDbSet HallRef { get; } + + public IDbSet Hall { get; } + + public IDbSet Match { get; } + + public IDbSet TeamRef { get; } + + public IDbSet OilProfile { get; } - public IDbSet Hallar { get; } + public IDbSet VMatchHeadInfo { get; } } diff --git a/Snittlistan.Web/Infrastructure/Database/ISnittlistanContext.cs b/Snittlistan.Web/Infrastructure/Database/ISnittlistanContext.cs index 6dbec398..58d7c6e7 100644 --- a/Snittlistan.Web/Infrastructure/Database/ISnittlistanContext.cs +++ b/Snittlistan.Web/Infrastructure/Database/ISnittlistanContext.cs @@ -1,11 +1,10 @@ #nullable enable -using System.Data.Entity; using System.Data.Entity.Infrastructure; namespace Snittlistan.Web.Infrastructure.Database; -public interface ISnittlistanContext +public interface ISnittlistanContext : IDisposable { IDbSet PublishedTasks { get; } diff --git a/Snittlistan.Web/Infrastructure/Database/SnittlistanContext.cs b/Snittlistan.Web/Infrastructure/Database/SnittlistanContext.cs index 0c693043..494499f8 100644 --- a/Snittlistan.Web/Infrastructure/Database/SnittlistanContext.cs +++ b/Snittlistan.Web/Infrastructure/Database/SnittlistanContext.cs @@ -1,7 +1,6 @@ #nullable enable using Npgsql.NameTranslation; -using System.Data.Entity; namespace Snittlistan.Web.Infrastructure.Database; diff --git a/Snittlistan.Web/Infrastructure/GuidBinder.cs b/Snittlistan.Web/Infrastructure/GuidBinder.cs index 9cc2e9d9..4a3012b2 100644 --- a/Snittlistan.Web/Infrastructure/GuidBinder.cs +++ b/Snittlistan.Web/Infrastructure/GuidBinder.cs @@ -1,6 +1,9 @@ -using System.Web.Mvc; +#nullable enable + +using System.Web.Mvc; namespace Snittlistan.Web.Infrastructure; + public class GuidBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) diff --git a/Snittlistan.Web/Infrastructure/HandlerContext.cs b/Snittlistan.Web/Infrastructure/HandlerContext.cs index a7eb8589..a101667b 100644 --- a/Snittlistan.Web/Infrastructure/HandlerContext.cs +++ b/Snittlistan.Web/Infrastructure/HandlerContext.cs @@ -11,18 +11,22 @@ public class HandlerContext : IHandlerContext public HandlerContext( CompositionRoot compositionRoot, + Databases databases, TPayload payload, Tenant tenant, Guid correlationId, Guid causationId) { this.compositionRoot = compositionRoot; + Databases = databases; Payload = payload; Tenant = tenant; CorrelationId = correlationId; CausationId = causationId; } + public Databases Databases { get; } + public TPayload Payload { get; } public Tenant Tenant { get; } @@ -40,6 +44,7 @@ public async Task ExecuteCommand(TCommand command) { CommandExecutor commandExecutor = new( compositionRoot, + Databases, CorrelationId, CausationId, "system"); diff --git a/Snittlistan.Web/Infrastructure/Installers/DatabaseContextInstaller.cs b/Snittlistan.Web/Infrastructure/Installers/DatabaseContextInstaller.cs index 8f7f71d2..3dd0d126 100644 --- a/Snittlistan.Web/Infrastructure/Installers/DatabaseContextInstaller.cs +++ b/Snittlistan.Web/Infrastructure/Installers/DatabaseContextInstaller.cs @@ -10,20 +10,20 @@ namespace Snittlistan.Web.Infrastructure.Installers; public class DatabaseContextInstaller : IWindsorInstaller { - private readonly Func databases; + private readonly Func databasesFactory; private readonly Func, ComponentRegistration> func; - public DatabaseContextInstaller(Func databases) + public DatabaseContextInstaller(Func databasesFactory) { func = x => x.LifestylePerWebRequest(); - this.databases = databases; + this.databasesFactory = databasesFactory; } - public DatabaseContextInstaller(Func databases, LifestyleType lifestyleType) + public DatabaseContextInstaller(Func databasesFactory, LifestyleType lifestyleType) { func = x => x.LifeStyle.Is(lifestyleType); - this.databases = databases; + this.databasesFactory = databasesFactory; } public void Install(IWindsorContainer container, IConfigurationStore store) @@ -31,6 +31,10 @@ public void Install(IWindsorContainer container, IConfigurationStore store) _ = container.Register( func.Invoke( Component.For() - .UsingFactoryMethod(databases))); + .UsingFactoryMethod(databasesFactory))); + _ = container.Register( + Component.For() + .Instance(new DatabasesFactory(databasesFactory)) + .LifestyleSingleton()); } } diff --git a/Snittlistan.Web/Infrastructure/TaskPublisher.cs b/Snittlistan.Web/Infrastructure/TaskPublisher.cs index 10ef3523..663dfc17 100644 --- a/Snittlistan.Web/Infrastructure/TaskPublisher.cs +++ b/Snittlistan.Web/Infrastructure/TaskPublisher.cs @@ -33,6 +33,7 @@ public TaskPublisher( public void PublishTask(TaskBase task, string createdBy) { + Logger.Info("{createdBy} publish task {@task}", createdBy, task); PublishedTask publishedTask = databases.Snittlistan.PublishedTasks.Add( PublishedTask.CreateImmediate( task, @@ -112,6 +113,11 @@ void DoPublishMessage(Tenant tenant, PublishedTask publishedTask) public void PublishDelayedTask(TaskBase task, DateTime publishDate, string createdBy) { + Logger.Info( + "{createdBy} publish delayed (@{publishDate}) task {@task}", + createdBy, + publishDate, + task); PublishedTask publishedTask = databases.Snittlistan.PublishedTasks.Add( PublishedTask.CreateDelayed( task, @@ -120,6 +126,6 @@ public void PublishDelayedTask(TaskBase task, DateTime publishDate, string creat causationId, publishDate, createdBy)); - Logger.Info("added delayed task: {@publishedTask}", publishedTask); + Logger.Info("added delayed task {@publishedTask}", publishedTask); } } diff --git a/Snittlistan.Web/Models/TenantFeatures.cs b/Snittlistan.Web/Models/TenantFeatures.cs index da828174..171920df 100644 --- a/Snittlistan.Web/Models/TenantFeatures.cs +++ b/Snittlistan.Web/Models/TenantFeatures.cs @@ -3,7 +3,12 @@ namespace Snittlistan.Web.Models; public record TenantFeatures( - bool RosterMailEnabled) + bool RosterMailEnabled, + int RosterMailDelayMinutes) { public static string Key { get; } = "TenantFeatures"; + + public static TenantFeatures Default = new( + true, + 10); }; diff --git a/Snittlistan.Web/Models/UpdateRosterEmail.cs b/Snittlistan.Web/Models/UpdateRosterEmail.cs index a91d4e87..a19f6b7d 100644 --- a/Snittlistan.Web/Models/UpdateRosterEmail.cs +++ b/Snittlistan.Web/Models/UpdateRosterEmail.cs @@ -19,7 +19,13 @@ public UpdateRosterEmail( int turn, Uri rosterLink, Uri userProfileLink, - bool needsAccept) + bool needsAccept, + string homeTeamAlias, + string awayTeamAlias, + string hallName, + int oilProfileId, + string oilProfileName, + DateTime matchDate) : base("UpdateRoster") { _state = new( @@ -33,7 +39,13 @@ public UpdateRosterEmail( turn, rosterLink, userProfileLink, - needsAccept); + needsAccept, + homeTeamAlias, + awayTeamAlias, + hallName, + oilProfileId, + oilProfileName, + matchDate); } public string PlayerEmail => _state.PlayerEmail; @@ -58,5 +70,17 @@ public UpdateRosterEmail( public bool NeedsAccept => _state.NeedsAccept; + public string HomeTeamAlias => _state.HomeTeamAlias; + + public string AwayTeamAlias => _state.AwayTeamAlias; + + public string HallName => _state.HallName; + + public int OilProfileId => _state.OilProfileId; + + public string OilProfileName => _state.OilProfileName; + + public DateTime MatchDate => _state.MatchDate; + public override EmailState State => _state; } diff --git a/Snittlistan.Web/Models/UpdateRosterEmail_State.cs b/Snittlistan.Web/Models/UpdateRosterEmail_State.cs index 29cb31c0..eef1b030 100644 --- a/Snittlistan.Web/Models/UpdateRosterEmail_State.cs +++ b/Snittlistan.Web/Models/UpdateRosterEmail_State.cs @@ -18,7 +18,13 @@ public UpdateRosterEmail_State( int turn, Uri rosterLink, Uri userProfileLink, - bool needsAccept) + bool needsAccept, + string homeTeamAlias, + string awayTeamAlias, + string hallName, + int oilProfileId, + string oilProfileName, + DateTime matchDate) : base(OwnerEmail, playerEmail, OwnerEmail, "Uttagning har uppdaterats") { PlayerEmail = playerEmail; @@ -32,6 +38,12 @@ public UpdateRosterEmail_State( RosterLink = rosterLink; UserProfileLink = userProfileLink; NeedsAccept = needsAccept; + HomeTeamAlias = homeTeamAlias; + AwayTeamAlias = awayTeamAlias; + HallName = hallName; + OilProfileId = oilProfileId; + OilProfileName = oilProfileName; + MatchDate = matchDate; } public string PlayerEmail { get; } @@ -56,6 +68,18 @@ public UpdateRosterEmail_State( public bool NeedsAccept { get; } + public string HomeTeamAlias { get; } + + public string AwayTeamAlias { get; } + + public string HallName { get; } + + public int OilProfileId { get; } + + public string OilProfileName { get; } + + public DateTime MatchDate { get; } + public override Email CreateEmail() { return new UpdateRosterEmail( @@ -69,6 +93,12 @@ public override Email CreateEmail() Turn, RosterLink, UserProfileLink, - NeedsAccept); + NeedsAccept, + HomeTeamAlias, + AwayTeamAlias, + HallName, + OilProfileId, + OilProfileName, + MatchDate); } } diff --git a/Snittlistan.Web/Snittlistan.Web.csproj b/Snittlistan.Web/Snittlistan.Web.csproj index a1397c3b..e77c3179 100644 --- a/Snittlistan.Web/Snittlistan.Web.csproj +++ b/Snittlistan.Web/Snittlistan.Web.csproj @@ -155,6 +155,12 @@ Index.cshtml + + + + + + diff --git a/Snittlistan.Web/TaskHandlers/TaskHandler.cs b/Snittlistan.Web/TaskHandlers/TaskHandler.cs index c784b8e1..43e8a945 100644 --- a/Snittlistan.Web/TaskHandlers/TaskHandler.cs +++ b/Snittlistan.Web/TaskHandlers/TaskHandler.cs @@ -18,6 +18,7 @@ public async Task Handle(HandlerContext context) TCommand command = CreateCommand(context.Payload); CommandExecutor commandExecutor = new( CompositionRoot, + CompositionRoot.Databases, context.CorrelationId, context.CausationId, "system"); diff --git a/Snittlistan.Web/Views/Emails/UpdateRoster.cshtml b/Snittlistan.Web/Views/Emails/UpdateRoster.cshtml index 532e99c0..e9a2c80c 100644 --- a/Snittlistan.Web/Views/Emails/UpdateRoster.cshtml +++ b/Snittlistan.Web/Views/Emails/UpdateRoster.cshtml @@ -30,7 +30,24 @@ Content-Type: text/html; charset=utf-8 }

-

@Model.FormattedAuditLog.Title

+
+
Hemmalag
+
@Model.HomeTeamAlias
+
Bortalag
+
@Model.AwayTeamAlias
+
Plats
+
@Model.HallName
+
Oljeprofil
+
@Model.OilProfileName
+
När
+
+ + + +
+
diff --git a/Snittlistan.Web/Web.config b/Snittlistan.Web/Web.config index 30a56855..d4a4034f 100644 --- a/Snittlistan.Web/Web.config +++ b/Snittlistan.Web/Web.config @@ -17,7 +17,7 @@ - +
Bord