From e33f5bca11191cce510015d9319470818b696610 Mon Sep 17 00:00:00 2001 From: axunonb Date: Fri, 12 Apr 2024 16:07:25 +0200 Subject: [PATCH] Remove stoppingToken in ExecuteTaskChunkAndWait Reduce cognitive complexity outlined by SonarCloud Update build.yml for Node.js 20 Remove not required Axuno.Tools.Password classes --- .github/workflows/build.yml | 4 +- .../ConcurrentBackgroundQueueService.cs | 177 ++++--- Axuno.Tools/FileSystem/DelayedEvent.cs | 17 +- .../FileSystem/DelayedFileSystemWatcher.cs | 140 +++-- Axuno.Tools/GermanHoliday.cs | 14 +- Axuno.Tools/GermanHolidays.cs | 224 ++++---- Axuno.Tools/Password/Password_Checker.cs | 479 ------------------ Axuno.Tools/Password/Password_Reset.cs | 53 -- League/Controllers/Account.cs | 146 +++--- League/Identity/RoleStore.cs | 6 +- .../MatchViewModels/EnterResultViewModel.cs | 127 +++-- .../ExcludeDates/InternetCalendarImporter.cs | 1 + .../TournamentManager/Plan/MatchScheduler.cs | 71 ++- .../TournamentManager/Ranking/Ranking.cs | 105 ++-- .../RoundRobin/MatchesAnalyzer.cs | 41 +- 15 files changed, 569 insertions(+), 1036 deletions(-) delete mode 100644 Axuno.Tools/Password/Password_Checker.cs delete mode 100644 Axuno.Tools/Password/Password_Reset.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d450ea00..f180f912 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,9 +17,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 6.x - name: Restore dependencies diff --git a/Axuno.BackgroundTask/ConcurrentBackgroundQueueService.cs b/Axuno.BackgroundTask/ConcurrentBackgroundQueueService.cs index ed40bbdf..dcd72e87 100644 --- a/Axuno.BackgroundTask/ConcurrentBackgroundQueueService.cs +++ b/Axuno.BackgroundTask/ConcurrentBackgroundQueueService.cs @@ -45,10 +45,7 @@ public ConcurrentBackgroundQueueService(IBackgroundQueue taskQueue, /// A . protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - lock (_locker) - { - _resetEvent.WaitOne(); - } + await WaitForStartSignal(); var taskListReference = new Queue(); @@ -59,94 +56,112 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) stoppingToken.ThrowIfCancellationRequested(); await Task.Delay(Config.PollQueueDelay, stoppingToken); - if (_concurrentTaskCount < Config.MaxConcurrentCount && TaskQueue?.Count > 0) - { - Interlocked.Increment(ref _concurrentTaskCount); - _logger.LogDebug("Num of tasks: {concurrentTaskCount}", _concurrentTaskCount); - taskListReference.Enqueue(TaskQueue.DequeueTask()); - } - else - { - var taskChunk = new List(); - while (taskListReference.Count > 0) - { - try - { - stoppingToken.ThrowIfCancellationRequested(); - - // The service shall only be cancelled when the app shuts down - using (var taskCancellation = new CancellationTokenSource()) - using (var combinedCancellation = - CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, - taskCancellation.Token)) - { - var t = TaskQueue?.RunTaskAsync(taskListReference.Dequeue(), - combinedCancellation.Token); - if (t is null) - throw new NullReferenceException($"{nameof(TaskQueue)} cannot be null here."); - if (t.Exception != null) throw t.Exception; - taskChunk.Add(t); - } - - stoppingToken.ThrowIfCancellationRequested(); - } - catch (Exception e) - { - _logger.LogError(e, "Error occurred executing TaskItem."); - } - finally - { - Interlocked.Decrement(ref _concurrentTaskCount); - } - } - - if (taskChunk.Count == 0) continue; - - // Task.WhenAll will not throw all exceptions when it encounters them. - // Instead, it adds them to an AggregateException, that must be - // checked at the end of waiting for the tasks to complete - Task? allTasks = null; - try - { - allTasks = Task.WhenAll(taskChunk); - // re-throws an AggregateException if one exists - // after waiting for the tasks to complete - await allTasks.WaitAsync(stoppingToken); - } - catch (Exception e) - { - _logger.LogError(e, "Task chunk exception"); - if (allTasks?.Exception != null) - { - _logger.LogError(allTasks.Exception, "Task chunk aggregate exception"); - } - } - finally - { - taskListReference.Clear(); - } - } + EnqueuePendingTasks(taskListReference); + await ExecuteTaskChunk(taskListReference, stoppingToken); } } - catch (Exception e) when (e is TaskCanceledException) + catch (TaskCanceledException ex) { - _logger.LogError(e, $"{nameof(ConcurrentBackgroundQueueService)} was canceled."); + _logger.LogError(ex, "{Service} was canceled.", nameof(ConcurrentBackgroundQueueService)); } - catch(Exception e) + catch (Exception ex) { - _logger.LogError(e, $"{nameof(ConcurrentBackgroundQueueService)} failed."); - TaskQueue = null; // we can't process the queue any more + _logger.LogError(ex, "{Service} failed.", nameof(ConcurrentBackgroundQueueService)); + TaskQueue = null; // we can't process the queue anymore } finally { - lock (_locker) + SignalServiceStopped(); + } + } + + private Task WaitForStartSignal() + { + lock (_locker) + { + _resetEvent.WaitOne(); + } + + return Task.CompletedTask; + } + + private void EnqueuePendingTasks(Queue taskListReference) + { + if (_concurrentTaskCount >= Config.MaxConcurrentCount || TaskQueue == null || TaskQueue.Count == 0) + return; + + Interlocked.Increment(ref _concurrentTaskCount); + _logger.LogDebug("Num of tasks: {ConcurrentTaskCount}", _concurrentTaskCount); + + taskListReference.Enqueue(TaskQueue.DequeueTask()); + } + + private async Task ExecuteTaskChunk(Queue taskListReference, CancellationToken stoppingToken) + { + if (taskListReference.Count == 0) + return; + + var taskChunk = new List(); + + try + { + while (taskListReference.Count > 0) { - _resetEvent.Reset(); - _resetEvent.Set(); + stoppingToken.ThrowIfCancellationRequested(); + + using var taskCancellation = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); + var task = (TaskQueue?.RunTaskAsync(taskListReference.Dequeue(), taskCancellation.Token)) ?? + throw new NullReferenceException($"{nameof(TaskQueue)} cannot be null here."); + + if (task.Exception != null) + throw task.Exception; + + taskChunk.Add(task); + } + + await ExecuteTaskChunkAndWait(taskChunk); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred executing TaskItem."); + } + finally + { + Interlocked.Decrement(ref _concurrentTaskCount); + taskListReference.Clear(); + } + } + + private async Task ExecuteTaskChunkAndWait(List taskChunk) + { + if (taskChunk.Count == 0) + return; + + try + { + await Task.WhenAll(taskChunk); + } + catch (Exception ex) + { + _logger.LogError(ex, "Task chunk exception"); + var taskChunkExceptions = taskChunk.Where(task => task.Exception != null).Select(task => task.Exception!).ToList(); + if (taskChunkExceptions.Count > 0) + { + _logger.LogError(new AggregateException(taskChunkExceptions), "Task chunk aggregate exception"); } } } + private void SignalServiceStopped() + { + lock (_locker) + { + _resetEvent.Reset(); + _resetEvent.Set(); + } + } + + /// /// Stops the service. /// @@ -154,8 +169,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) /// public override async Task StopAsync(CancellationToken cancellationToken) { - _logger.LogDebug($"{nameof(ConcurrentBackgroundQueueService)} is stopping."); + _logger.LogDebug("{Service} is stopping.", nameof(ConcurrentBackgroundQueueService)); await base.StopAsync(cancellationToken); - _logger.LogDebug($"{nameof(ConcurrentBackgroundQueueService)} stopped."); + _logger.LogDebug("{Service} stopped.", nameof(ConcurrentBackgroundQueueService)); } } diff --git a/Axuno.Tools/FileSystem/DelayedEvent.cs b/Axuno.Tools/FileSystem/DelayedEvent.cs index ee82a29b..0d40b025 100644 --- a/Axuno.Tools/FileSystem/DelayedEvent.cs +++ b/Axuno.Tools/FileSystem/DelayedEvent.cs @@ -23,12 +23,11 @@ public DelayedEvent(FileSystemEventArgs args) /// public FileSystemEventArgs Args { get; } - public virtual bool IsDuplicate(object obj) + public virtual bool IsDuplicate(object? obj) { - if (!(obj is DelayedEvent delayedEvent)) + if (obj is not DelayedEvent delayedEvent) return false; - var allEventArgs = Args; var renamedEventArgs = Args as RenamedEventArgs; var allDelayedEventArgs = delayedEvent.Args; @@ -37,16 +36,16 @@ public virtual bool IsDuplicate(object obj) // We also eliminate Changed events that follow recent Created events // because many apps create new files by creating an empty file and then // update the file with the file content. - return (allEventArgs.ChangeType == allDelayedEventArgs.ChangeType - && allEventArgs.FullPath == allDelayedEventArgs.FullPath && - allEventArgs.Name == allDelayedEventArgs.Name) && + return (Args.ChangeType == allDelayedEventArgs.ChangeType + && Args.FullPath == allDelayedEventArgs.FullPath && + Args.Name == allDelayedEventArgs.Name) && ((renamedEventArgs == null && delayedRenamedEventArgs == null) || (renamedEventArgs != null && delayedRenamedEventArgs != null && renamedEventArgs.OldFullPath == delayedRenamedEventArgs.OldFullPath && renamedEventArgs.OldName == delayedRenamedEventArgs.OldName)) || - (allEventArgs.ChangeType == WatcherChangeTypes.Created + (Args.ChangeType == WatcherChangeTypes.Created && allDelayedEventArgs.ChangeType == WatcherChangeTypes.Changed - && allEventArgs.FullPath == allDelayedEventArgs.FullPath && - allEventArgs.Name == allDelayedEventArgs.Name); + && Args.FullPath == allDelayedEventArgs.FullPath && + Args.Name == allDelayedEventArgs.Name); } } diff --git a/Axuno.Tools/FileSystem/DelayedFileSystemWatcher.cs b/Axuno.Tools/FileSystem/DelayedFileSystemWatcher.cs index 4de50b4e..161f77a9 100644 --- a/Axuno.Tools/FileSystem/DelayedFileSystemWatcher.cs +++ b/Axuno.Tools/FileSystem/DelayedFileSystemWatcher.cs @@ -35,7 +35,7 @@ public class DelayedFileSystemWatcher : IDisposable private int _consolidationInterval = 1000; // initial value in milliseconds private readonly System.Timers.Timer _timer; - #region Delegate to FileSystemWatcher + #region *** Delegate to FileSystemWatcher *** /// /// Initializes a new instance of the class. @@ -289,7 +289,7 @@ public WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType, int ti #endregion - #region Implementation + #region *** Implementation *** private void Initialize(out System.Timers.Timer timer) { @@ -323,83 +323,79 @@ private void RenamedEventHandler(object sender, RenamedEventArgs e) private void ElapsedEventHandler(object? sender, ElapsedEventArgs e) { - // We don't fire the events inside the lock. - // We will queue them here until the code exits the locks. - Queue? eventsToBeFired = null; - if (Monitor.TryEnter(_enterThread)) + if (!Monitor.TryEnter(_enterThread)) + return; + + try { - // Only one thread at a time is processing the events - try - { - eventsToBeFired = new Queue(32); - // Lock the collection while processing the events - lock (_events.SyncRoot) - { - for (var i = 0; i < _events.Count; i++) - { - var current = _events[i] as DelayedEvent; - if (current is { Delayed: true }) - { - // This event has been delayed already so we can fire it - // We just need to remove any duplicates - for (var j = i + 1; j < _events.Count; j++) - { - if (current.IsDuplicate(_events[j]!)) - { - // Removing later duplicates - _events.RemoveAt(j); - j--; // Don't skip next event - } - } - - var raiseEvent = true; - if (current.Args.ChangeType is WatcherChangeTypes.Created or WatcherChangeTypes.Changed) - { - // check if the file can be opened for reading (i.e. copying has been completed) - FileStream? stream = null; - try - { - stream = File.Open(current.Args.FullPath, FileMode.Open, FileAccess.Read, - FileShare.None); - } - catch (IOException) - { - raiseEvent = false; - } - finally - { - stream?.Close(); - } - } - - if (!raiseEvent) continue; - - // Add the event to the list of events to be fired - eventsToBeFired.Enqueue(current); - // Remove it from the current list - _events.RemoveAt(i); - i--; // meaning: the next event won't be skipped - } - else - { - // This event was not delayed yet, so we will delay processing - // of this event for at least one timer interval - if (current != null) current.Delayed = true; - } - } - } - } - finally + var eventsToBeFired = ProcessEvents(); + if (eventsToBeFired != null) + RaiseEvents(eventsToBeFired); + } + finally + { + Monitor.Exit(_enterThread); + } + } + + private Queue? ProcessEvents() + { + var eventsToBeFired = new Queue(32); + + lock (_events.SyncRoot) + { + for (var i = 0; i < _events.Count; i++) { - Monitor.Exit(_enterThread); + if (_events[i] is not DelayedEvent current) + continue; + + if (current.Delayed) + ProcessDelayedEvent(current, eventsToBeFired, ref i); + else + current.Delayed = true; } } - // else - this timer event was skipped, processing will happen during the next timer event - // Now fire all the events if any events are in eventsToBeFired - if (eventsToBeFired != null) RaiseEvents(eventsToBeFired); + return eventsToBeFired.Count > 0 ? eventsToBeFired : null; + } + + private void ProcessDelayedEvent(DelayedEvent current, Queue eventsToBeFired, ref int i) + { + if (current.Args.ChangeType is not (WatcherChangeTypes.Created or WatcherChangeTypes.Changed)) + return; + + if (!CanOpenFile(current.Args.FullPath)) + return; + + RemoveDuplicates(current); + eventsToBeFired.Enqueue(current); + _events.RemoveAt(i); + i--; // Decrement i to process the next event correctly + } + + private bool CanOpenFile(string fullPath) + { + try + { + using var stream = File.Open(fullPath, FileMode.Open, FileAccess.Read, FileShare.None); + return true; + } + catch (IOException) + { + return false; + } + } + + private void RemoveDuplicates(DelayedEvent current) + { + for (var j = _events.Count - 1; j > 0; j--) + { + if (current.IsDuplicate(_events[j])) + _events.RemoveAt(j); + } } + /// /// Gets or sets the interval in milliseconds, after which events will be fired. /// diff --git a/Axuno.Tools/GermanHoliday.cs b/Axuno.Tools/GermanHoliday.cs index 0392c90a..8fdff97c 100644 --- a/Axuno.Tools/GermanHoliday.cs +++ b/Axuno.Tools/GermanHoliday.cs @@ -88,18 +88,8 @@ public bool IsPublicHoliday(GermanFederalStates.Id stateId) return PublicHolidayStateIds.Exists(s => s == stateId); } - public static bool operator ==(GermanHoliday? h1, GermanHoliday? h2) - { - if (h1 is null || h2 is null) return false; - return h1.Equals(h2); - } - - public static bool operator !=(GermanHoliday? h1, GermanHoliday? h2) - { - if (h1 is null || h2 is null) return true; - return !h1.Equals(h2); - } - + // Note: We don't have operator overloading for == and != because they should not be used on reference types. + public static bool operator <(GermanHoliday? h1, GermanHoliday? h2) { if (h1 is null || h2 is null) return false; diff --git a/Axuno.Tools/GermanHolidays.cs b/Axuno.Tools/GermanHolidays.cs index e216bcd5..4c57355e 100644 --- a/Axuno.Tools/GermanHolidays.cs +++ b/Axuno.Tools/GermanHolidays.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Xml.Linq; namespace Axuno.Tools; @@ -367,7 +368,7 @@ public List GetFiltered(Predicate filter) /// /// Loads holiday data for a year from an XML file and adds, merges, removes or replaces /// the standard German holidays which are calculated automatically. - /// All element and attribute names/values must match XML standards, but are not case sensitive. + /// All element and attribute names/values must match XML standards, but are not case-sensitive. /// Dates not in scope of the year given with CTOR will just be ignored. /// /// @@ -379,124 +380,143 @@ public List GetFiltered(Predicate filter) /// A URI string referencing the holidays XML file to load. public void Load(string path) { - // load holiday data from XML file - var holidayQuery = from holiday in XElement.Load(path).Elements() - where holiday.Name.ToString().ToLower() == "holiday" - select holiday; + var holidays = XElement.Load(path).Elements("holiday"); - foreach (var holiday in holidayQuery) + foreach (var holiday in holidays) { - // get the standard German holiday id (if any) - Id? holidayId = null; - if (holiday.Attributes().Any(e => e.Name.ToString().ToLower() == "id")) - holidayId = (Id) Enum.Parse(typeof(Id), - holiday.Attributes().First( - e => e.Name.ToString().ToLower() == "id").Value, - true); - - // what to do with this holiday? The default action is "Merge". - var action = ActionType.Merge; - if (holiday.Attributes().Any(e => e.Name.ToString().ToLower() == "action")) - action = (ActionType) Enum.Parse(typeof(ActionType), - holiday.Attributes().First( - e => e.Name.ToString().ToLower() == "action").Value, - true); - - // remove an existing (standard German) holiday - if (action == ActionType.Remove && holidayId.HasValue) - { - Remove(this[holidayId.Value]!); - continue; - } + var holidayId = GetHolidayId(holiday); + var action = GetActionType(holiday); + var (dateFrom, dateTo) = GetDateRange(holiday); + var holidayType = GetHolidayType(holiday); + var name = holiday.Element("name")?.Value ?? string.Empty; + var stateIds = GetStateIds(holiday); + + ProcessHoliday(holidayId, action, dateFrom, dateTo, holidayType, name, stateIds); + } + } - // get the dates (if any) - var dateFrom = DateTime.MinValue; - var dateTo = DateTime.MinValue; - if (holiday.Elements().Any(e => e.Name.ToString().ToLower() == "datefrom") && - holiday.Elements().Any(e => e.Name.ToString().ToLower() == "dateto")) - { - dateFrom = - DateTime.Parse( - holiday.Elements().First(e => e.Name.ToString().ToLower() == "datefrom").Value); - dateTo = - DateTime.Parse(holiday.Elements().First(e => e.Name.ToString().ToLower() == "dateto").Value); + private Id? GetHolidayId(XElement holiday) + { + if (holiday.Attribute("id") != null && Enum.TryParse(holiday.Attribute("id")?.Value, true, out var id)) + return id; + return null; + } - // Swap from/to dates, if mixed up - if (dateFrom > dateTo) (dateFrom, dateTo) = (dateTo, dateFrom); + private ActionType GetActionType(XElement holiday) + { + if (Enum.TryParse(holiday.Attribute("action")?.Value, true, out var action)) + return action; + return ActionType.Merge; + } - // holiday must be within the year given by CTOR - if (dateFrom.Year < Year && dateTo.Year == Year) - dateFrom = new DateTime(Year, 1, 1); + private (DateTime, DateTime) GetDateRange(XElement holiday) + { + var dateFrom = DateTime.MinValue; + var dateTo = DateTime.MinValue; - if (dateFrom.Year == Year && dateTo.Year > Year) - dateTo = new DateTime(Year, 12, 31); + if (holiday.Element("datefrom") != null && holiday.Element("dateto") != null) + { + dateFrom = DateTime.ParseExact(holiday.Element("datefrom")?.Value!, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None); + dateTo = DateTime.ParseExact(holiday.Element("dateto")?.Value!, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None); - if (dateFrom.Year != Year && dateTo.Year != Year) - continue; - } + if (dateFrom > dateTo) + (dateFrom, dateTo) = (dateTo, dateFrom); + } - // get the holiday type - var holidayType = - (Type) - Enum.Parse(typeof(Type), - holiday.Elements().First(e => e.Name.ToString().ToLower() == "type").Value, true); + return (dateFrom, dateTo); + } - var name = holiday.Elements().First(e => e.Name.ToString().ToLower() == "name").Value; + private Type GetHolidayType(XElement holiday) + { + if (Enum.TryParse(holiday.Element("type")?.Value, true, out var type)) + return type; + throw new InvalidOperationException("Missing or invalid holiday type."); + } - // get the federal state ids (if any) - XElement? stateIds = null; - var germanFederalStateIds = new List(); - if (holiday.Elements().Any(e => e.Name.ToString().ToLower() == "publicholidaystateids")) - { - stateIds = holiday.Elements().First(e => e.Name.ToString().ToLower() == "publicholidaystateids"); - if (stateIds.HasElements) - foreach (var stateId in stateIds.Elements()) - germanFederalStateIds.Add( - (GermanFederalStates.Id) Enum.Parse(typeof(GermanFederalStates.Id), stateId.Value, - true)); - } + private List GetStateIds(XElement holiday) + { + var stateIds = new List(); - // Only with Replace action the dates may be missing - if (action != ActionType.Replace && (dateFrom == DateTime.MinValue || dateTo == DateTime.MinValue)) - throw new InvalidOperationException("Missing 'date from' and/or 'date to' in XML data."); + var publicHolidayStateIds = holiday.Element("publicholidaystateids"); + if (publicHolidayStateIds != null && publicHolidayStateIds.HasElements) + { + stateIds.AddRange(publicHolidayStateIds.Elements().Select(stateId => + Enum.Parse(stateId.Value, true))); + } + + return stateIds; + } - while (dateFrom <= dateTo) + private void ProcessHoliday(Id? holidayId, ActionType action, DateTime dateFrom, DateTime dateTo, Type holidayType, string name, List stateIds) + { + if (action == ActionType.Remove && holidayId.HasValue) + { + RemoveHoliday(holidayId.Value); + return; + } + + ValidateDateRange(action, dateFrom, dateTo); + + while (dateFrom <= dateTo) + { + var tmpDateFrom = dateFrom; // no capture of modified closure + var germanHoliday = new GermanHoliday(holidayId, holidayType, name, () => tmpDateFrom) + { + PublicHolidayStateIds = stateIds + }; + + switch (action) { - var tmpDateFrom = new DateTime(dateFrom.Ticks); - var germanHoliday = new GermanHoliday(holidayId, holidayType, name, () => tmpDateFrom) - { - PublicHolidayStateIds = germanFederalStateIds - }; - switch (action) - { - case ActionType.Merge: - Merge(germanHoliday); - break; - case ActionType.Add: - Add(germanHoliday); - break; - case ActionType.Replace: - // The holiday must exist in the list - if (holidayId.HasValue && this[holidayId.Value] is not null) - { - // replace the existing standard date only if a new date was given - if (tmpDateFrom != DateTime.MinValue) - this[holidayId.Value]!.CalcDateFunc = () => tmpDateFrom; - this[holidayId.Value]!.Type = holidayType; - this[holidayId.Value]!.Name = name; - // replace state ids, if any were supplied - if (stateIds != null) - this[holidayId.Value]!.PublicHolidayStateIds = germanFederalStateIds; - } - - break; - } - - dateFrom = dateFrom.AddDays(1); + case ActionType.Merge: + MergeHoliday(germanHoliday); + break; + case ActionType.Add: + AddHoliday(germanHoliday); + break; + case ActionType.Replace: + ReplaceHoliday(holidayId, germanHoliday); + break; } + + dateFrom = dateFrom.AddDays(1); } } + private void ValidateDateRange(ActionType action, DateTime dateFrom, DateTime dateTo) + { + if (action != ActionType.Replace && (dateFrom == DateTime.MinValue || dateTo == DateTime.MinValue)) + throw new InvalidOperationException("Missing 'date from' and/or 'date to' in XML data."); + } + + private void RemoveHoliday(Id holidayId) + { + Remove(this[holidayId]!.Name); + } + + private void MergeHoliday(GermanHoliday germanHoliday) + { + Merge(germanHoliday); + } + + private void AddHoliday(GermanHoliday germanHoliday) + { + Add(germanHoliday); + } + + private void ReplaceHoliday(Id? holidayId, GermanHoliday newHoliday) + { + if (!holidayId.HasValue || this[holidayId.Value] == null) + throw new InvalidOperationException("Holiday to replace not found."); + + var existingHoliday = this[holidayId.Value]!; + + // Replace the existing holiday with the new one + existingHoliday.CalcDateFunc = newHoliday.CalcDateFunc; + existingHoliday.Type = newHoliday.Type; + existingHoliday.Name = newHoliday.Name; + existingHoliday.PublicHolidayStateIds = newHoliday.PublicHolidayStateIds; + } + #region *** Enums *** private enum ActionType diff --git a/Axuno.Tools/Password/Password_Checker.cs b/Axuno.Tools/Password/Password_Checker.cs deleted file mode 100644 index 6173032a..00000000 --- a/Axuno.Tools/Password/Password_Checker.cs +++ /dev/null @@ -1,479 +0,0 @@ -using System.Collections.ObjectModel; -using System.Data; -using System.Globalization; -using System.Text; -using System.Text.RegularExpressions; - -namespace Axuno.Tools.Password; - -/// -/// Derived from http://www.codeproject.com/Articles/59186/Password-Strength-Control -/// By Peter Tewkesbury, 22 Feb 2010 -/// Heavily modified by NB -/// -/// -/// Additions -/// -/// In the additions section of the code, we add to the overall score for things which make the password 'good'. In my code, we check the following: -/// -/// Score += (Password Length *4) -/// Score += ((Password Length - Number of Upper Case Letters)*2) -/// Score += ((Password Length - Number of Lower Case Letters)*2) -/// Score += (Number of Digits * 4) -/// Score += (Number of Symbols * 6) -/// Score += (Number of Digits or Symbols in the Middle of the Password) * 2 -/// If (Number of Requirements Met > 3) then Score += (Number of Requirements Met * 2) -/// -/// Requirements are: -/// -/// Password Length >= 8 -/// Contains Uppercase Letters (A-Z) -/// Contains Lowercase Letters (a-z) -/// Contains Digits (0-9) -/// Contains Symbols (Char.IsSymbol(ch) or Char.IsPunctuation(ch)) -/// -/// Deductions -/// -/// In the deductions section of the code, we subtract from the overall score for things which make the password 'weak'. -/// In my code, we check the following: -/// -/// IF Password is all letters THEN Score -= (Password length) -/// IF Password is all digits THEN Score -= (Password length) -/// IF Password has repeated characters THEN Score -= (Number of repeated characters * (Number of repeated characters -1) -/// IF Password has consecutive uppercase letters THEN Score -= (Number of consecutive uppercase characters * 2) -/// IF Password has consecutive lowercase letters THEN Score -= (Number of consecutive lowercase characters * 2) -/// IF Password has consecutive digits THEN Score -= (Number of consecutive digits * 2) -/// IF Password has sequential letters THEN Score -= (Number of sequential letters * 3) E.g.: ABCD or DCBA. -/// IF Password has sequential digits THEN Score -= (Number of sequential digits * 3) E.g.: 1234 or 4321. -/// -public class Checker -{ - private Checker() - { } - - internal static Checker Current { get; } = new(); - - - /// - /// Checks the password and determines the score. - /// - /// Requirements for the password. - /// Password to check. - public static async Task CheckAsync(Requirements? req, string pwd) - { - req ??= new Requirements(); - return await Task.Run(() => DoCheck(req, pwd)); - } - - /// - /// Checks the password and determines the score. - /// - /// Requirements for the password. - /// Password to check. - public static PasswordStrength Check(Requirements? req, string pwd) - { - req ??= new Requirements(); - return DoCheck(req, pwd); - } - - private static PasswordStrength DoCheck(Requirements req, string pwd) - { - const int numOfSequChars = 3; - var passwordStrength = new PasswordStrength(); - - var forbiddenWordsShare = 0; - const string alphaLower = "abcdefghijklmnopqrstuvwxyz"; - const string numeric = "01234567890"; - const string keyboard = "^1234567890ß´^!\"§$%&/()=?`qwertzuiopü+QWERTZUIOPÜ*asdfghjklöä#ASDFGHJKLÖÄ'YXCVBNM;:_"; - var sequAlphaCount = 0; - var sequDigitCount = 0; - var sequKeyboardCount = 0; - - // count digits and special characters which are embedded between characters - var embeddedCount = (new Regex("[a-zA-Z][^a-zA-Z]", RegexOptions.CultureInvariant | RegexOptions.Compiled)).Matches(pwd).Count; - var digitCount = (new Regex("[0-9]", RegexOptions.CultureInvariant | RegexOptions.Compiled)).Matches(pwd).Count; - var lowerCount = (new Regex("[a-z]", RegexOptions.CultureInvariant | RegexOptions.Compiled)).Matches(pwd).Count; - var upperCount = (new Regex("[A-Z]", RegexOptions.CultureInvariant | RegexOptions.Compiled)).Matches(pwd).Count; - var whiteSpaceCount = (new Regex(@"\s", RegexOptions.CultureInvariant | RegexOptions.Compiled)).Matches(pwd).Count; - var specialCount = pwd.Length - digitCount - lowerCount - upperCount; - - var consecutiveUpperCount = (new Regex("[A-Z]{2,}", RegexOptions.CultureInvariant | RegexOptions.Compiled)).Matches(pwd).Count; - var consecutiveLowerCount = (new Regex("[a-z]{2,}", RegexOptions.CultureInvariant | RegexOptions.Compiled)).Matches(pwd).Count; - var consecutiveDigitCount = (new Regex("[0-9]{2,}", RegexOptions.CultureInvariant | RegexOptions.Compiled)).Matches(pwd).Count; - - // Check for sequential alpha string patterns (like "abcd", forward and reverse) - for (var s = 0; s < alphaLower.Length - numOfSequChars; s++) - { - var fwd = alphaLower.Substring(s, numOfSequChars); - var rev = Reverse(fwd); - if (pwd.ToLower(CultureInfo.InvariantCulture).IndexOf(fwd, StringComparison.InvariantCulture) != -1) - { - passwordStrength.SequCharsFound.Add(fwd); - sequAlphaCount++; - } - if (pwd.ToLower(CultureInfo.InvariantCulture).IndexOf(rev, StringComparison.InvariantCulture) != -1) - { - passwordStrength.SequCharsFound.Add(rev); - sequAlphaCount++; - } - } - - // Check for sequential numeric string patterns (like "1234" forward and reverse) - for (var s = 0; s < numeric.Length - numOfSequChars; s++) - { - var fwd = numeric.Substring(s, numOfSequChars); - var rev = Reverse(fwd); - if (pwd.ToLower(CultureInfo.InvariantCulture).IndexOf(fwd, StringComparison.InvariantCulture) != -1) - { - passwordStrength.SequCharsFound.Add(fwd); - sequDigitCount++; - } - if (pwd.ToLower(CultureInfo.InvariantCulture).IndexOf(rev, StringComparison.InvariantCulture) != -1) - { - passwordStrength.SequCharsFound.Add(rev); - sequDigitCount++; - } - } - - // Check for sequential German keyboard string patterns (like "asdfghjkl" forward and reverse) - for (var s = 0; s < keyboard.Length - numOfSequChars; s++) - { - var fwd = keyboard.Substring(s, numOfSequChars); - var rev = Reverse(fwd); - if (pwd.ToLower(CultureInfo.InvariantCulture).IndexOf(fwd, StringComparison.InvariantCulture) != -1) - { - passwordStrength.SequCharsFound.Add(fwd); - sequKeyboardCount++; - } - if (pwd.ToLower(CultureInfo.InvariantCulture).IndexOf(rev, StringComparison.InvariantCulture) != -1) - { - passwordStrength.SequCharsFound.Add(rev); - sequKeyboardCount++; - } - } - - // Check for usage of forbidden words (forward and reverse) - foreach (var word in req.ForbiddenWords) - { - var fwd = word.ToLower(CultureInfo.InvariantCulture); - var rev = Reverse(word).ToLower(CultureInfo.InvariantCulture); - if (pwd.ToLower(CultureInfo.InvariantCulture).IndexOf(fwd, StringComparison.InvariantCulture) != -1) - { - forbiddenWordsShare += (int) 100f/pwd.Length * word.Length; - passwordStrength.MissedRequirements.ForbiddenWords.Add(fwd); - } - if (pwd.ToLower().IndexOf(rev, StringComparison.InvariantCulture) != -1) - { - forbiddenWordsShare += (int) 100f / pwd.Length * word.Length; - passwordStrength.MissedRequirements.ForbiddenWords.Add(rev); - } - } - - - // Score += 4 * Password Length - AddAnalysisResult(passwordStrength, "Password Length", "(n*4)", pwd.Length, pwd.Length * 4, 0); - - // if we have uppercase letters Score += (number of uppercase letters *2) - if (upperCount > 0) - { - AddAnalysisResult(passwordStrength, "Uppercase Letters", "+((len-n)*2)", upperCount, ((pwd.Length - upperCount) * 2), 0); - } - else - AddAnalysisResult(passwordStrength, "Uppercase Letters", "+((len-n)*2)", upperCount, 0, 0); - - // if we have lowercase letters Score += (number of lowercase letters *2) - if (lowerCount > 0) - { - AddAnalysisResult(passwordStrength, "Lowercase Letters", "+((len-n)*2)", lowerCount, ((pwd.Length - lowerCount)*2), 0); - } - else - { - AddAnalysisResult(passwordStrength, "Lowercase Letters", "+((len-n)*2)", lowerCount, 0, 0); - } - - - // Score += (Number of digits * 4) - AddAnalysisResult(passwordStrength, "Numbers", "+(n*4)", digitCount, (digitCount * 4), 0); - - // Score += (Number of Symbols * 6) - AddAnalysisResult(passwordStrength, "Symbols", "+(n*6)", specialCount, (specialCount * 6), 0); - - // Score += (Number of embedded digits or symbols in middle of password *2) - AddAnalysisResult(passwordStrength, "Embedded Numbers or Symbols", "+(n*2)", embeddedCount, (embeddedCount * 2), 0); - - var requirements = (req[RequiredCriteria.MinLength] >= 0 ? 1 : 0) + - (req[RequiredCriteria.Uppercase] > 0 ? 1 : 0) + - (req[RequiredCriteria.Lowercase] > 0 ? 1 : 0) + - (req[RequiredCriteria.Digits] > 0 ? 1 : 0) + - (req[RequiredCriteria.Special] > 0 ? 1 : 0); - - if (pwd.Length >= req[RequiredCriteria.MinLength]) - { - passwordStrength.Score += pwd.Length; - AddAnalysisResult(passwordStrength, "Requirement. Length > " + req[RequiredCriteria.MinLength], "+(n)", pwd.Length, pwd.Length, 0); - } - else - { - passwordStrength.MissedRequirements.Add(RequiredCriteria.MinLength, pwd.Length); - } - - if (req[RequiredCriteria.Uppercase] > 0) - { - if (upperCount >= req[RequiredCriteria.Uppercase]) - { - AddAnalysisResult(passwordStrength, "Requirement. Uppercase", "+(n)", upperCount, upperCount, 0); - } - else - { - passwordStrength.MissedRequirements.Add(RequiredCriteria.Uppercase, upperCount); - } - } - - if (req[RequiredCriteria.Lowercase] > 0) - { - if (lowerCount >= req[RequiredCriteria.Lowercase]) - { - AddAnalysisResult(passwordStrength, "Requirement. Lowercase", "+(n)", lowerCount, lowerCount, 0); - } - else - { - passwordStrength.MissedRequirements.Add(RequiredCriteria.Lowercase, lowerCount); - } - } - - if (req[RequiredCriteria.Digits] > 0) - { - if (digitCount >= req[RequiredCriteria.Digits]) - { - AddAnalysisResult(passwordStrength, "Requirement. Digit", "+(n)", digitCount, digitCount, 0); - } - else - { - passwordStrength.MissedRequirements.Add(RequiredCriteria.Digits, digitCount); - } - } - - if (req[RequiredCriteria.Special] > 0) - { - if (specialCount >= req[RequiredCriteria.Special]) - { - AddAnalysisResult(passwordStrength, "Requirement. Special", "+(2n)", specialCount, specialCount*2, 0); - } - else - { - passwordStrength.MissedRequirements.Add(RequiredCriteria.Special, specialCount); - } - } - - // special treatment, because this is not a quality requirement - if (req.NoWhitespaceAllowed && whiteSpaceCount > 0) - { - AddAnalysisResult(passwordStrength, "Requirement. No Control or Whitespace", string.Empty, whiteSpaceCount, 0, 0); - passwordStrength.MissedRequirements.NoWhitespaceAllowed = false; - } - - // If we have more than 3 requirments then - if (requirements > 3) - { - AddAnalysisResult(passwordStrength, "3 or more requirements", "+(n*2)", requirements, (requirements * 2), 0); - } - else - AddAnalysisResult(passwordStrength, "Less than 3 requirements", "-(5-n)*2", requirements, 0, (5 - requirements) * 2); - - // - // Deductions - // - - // If only letters then score -= password length - if (digitCount == 0 && specialCount == 0) - { - AddAnalysisResult(passwordStrength, "Letters only", "-n", pwd.Length, 0, pwd.Length); - } - else - AddAnalysisResult(passwordStrength, "Letters only", "-n", 0, 0, 0); - - // Check for number of distinct characters used in the password - var distinctChars = pwd.ToCharArray().Distinct().Count(); - AddAnalysisResult(passwordStrength, "Distinct Characters", "-(% of not distinct chars)", distinctChars, 0, (100 - (int)(100f / pwd.Length * distinctChars)) / 4); - - // If only digits then score -= password length - if (digitCount == pwd.Length) - { - AddAnalysisResult(passwordStrength, "Numbers only", "-n", pwd.Length, 0, pwd.Length); - } - else - AddAnalysisResult(passwordStrength, "Numbers only", "-n", 0, 0, 0); - - // If Consecutive uppercase letters then score -= (consecutiveUpperCount * 2); - AddAnalysisResult(passwordStrength, "Consecutive Uppercase Letters", "-(n*2)", consecutiveUpperCount, 0, consecutiveUpperCount * 2); - - // If Consecutive lowercase letters then score -= (consecutiveLowerCount * 2); - AddAnalysisResult(passwordStrength, "Consecutive Lowercase Letters", "-(n*2)", consecutiveLowerCount, 0, consecutiveLowerCount * 2); - - // If Consecutive digits used then score -= (consecutiveDigitCount * 2); - AddAnalysisResult(passwordStrength, "Consecutive Numbers", "-(n*2)", consecutiveDigitCount, 0, consecutiveDigitCount * 2); - - // If password contains sequence of letters then score -= (100 / pwd.Length * sequAlphaCount) - AddAnalysisResult(passwordStrength, "Sequential Letters (3+)", "-(% of pw length)", sequAlphaCount, 0, (int) 100f / pwd.Length * sequAlphaCount); - - // If password contains sequence of digits then score -= (100 / pwd.Length * sequDigitCount) - AddAnalysisResult(passwordStrength, "Sequential Numbers (3+)", "-(% of pw length)", sequDigitCount, 0, (int) 100f / pwd.Length * sequDigitCount); - - // If password contains sequence of keyboard keys then score -= (100 / pwd.Length * sequDigitCount) - AddAnalysisResult(passwordStrength, "Sequential Keyboard Keys (3+)", "-(% of pw length)", sequDigitCount, 0, (int)100f / pwd.Length * sequKeyboardCount); - - // If password contains sequence of digits then score -= forbiddenWordsShare - AddAnalysisResult(passwordStrength, "Forbidden Words", "-(% of words of pw length)", forbiddenWordsShare, 0, forbiddenWordsShare); - - - passwordStrength.Score = passwordStrength.Details.Sum(d => d.Bonus) - passwordStrength.Details.Sum(d => d.Malus); - - if (passwordStrength.Score > 100) { passwordStrength.Score = 100; } else if (passwordStrength.Score < 0) { passwordStrength.Score = 0; } - if (passwordStrength.Score < 20) { passwordStrength.Rate = StrengthRate.Unsafe; } - else if (passwordStrength.Score >= 20 && passwordStrength.Score < 40) { passwordStrength.Rate = StrengthRate.Weak; } - else if (passwordStrength.Score >= 40 && passwordStrength.Score < 60) { passwordStrength.Rate = StrengthRate.Fair; } - else if (passwordStrength.Score >= 60 && passwordStrength.Score < 80) { passwordStrength.Rate = StrengthRate.Strong; } - else if (passwordStrength.Score >= 80) { passwordStrength.Rate = StrengthRate.Secure; } - - return passwordStrength; - } - - /// - /// Reverse a string. - /// - /// The string to reverse - /// A string - private static string Reverse(string input) - { - if (input.Length <= 1) - return input; - - var c = input.ToCharArray(); - var sb = new StringBuilder(c.Length); - for (var i = c.Length - 1; i > -1; i--) - sb.Append(c[i]); - - return sb.ToString(); - } - - /// - /// Log results of analysis - /// - /// - /// - /// - /// - /// - /// - /// - private static void AddAnalysisResult(PasswordStrength strength, string description, string rate, int count, int bonus, int malus) - { - var dr = new StrengthDetail - { - Description = description ?? "", - Rate = rate ?? "", - Count = count, - Bonus = bonus, - Malus = malus - }; - strength.Details.Add(dr); - } -} - - -public enum RequiredCriteria -{ - MinLength, Uppercase, Lowercase, Digits, Special -} - - -public class Requirements : Dictionary -{ - public Requirements() - { - Add(RequiredCriteria.MinLength, 5); - Add(RequiredCriteria.Uppercase, 0); - Add(RequiredCriteria.Lowercase, 0); - Add(RequiredCriteria.Digits, 0); - Add(RequiredCriteria.Special, 0); - - NoWhitespaceAllowed = true; - ForbiddenWords = new List(); - } - - /// - /// Specify whether control characters or whitespace are allowed - /// - public bool NoWhitespaceAllowed { get; set; } - - /// - /// Words or parts of words which are forbidden as password - /// - public List ForbiddenWords { get; set; } -} - -public class MissedRequirements : Requirements -{ - public MissedRequirements() - { - Clear(); - } -} - - -public class PasswordStrength -{ - public PasswordStrength() - { - Details = new ObservableCollection(); - Rate = StrengthRate.Unknown; - SequCharsFound = new List(); - MissedRequirements = new MissedRequirements(); - } - - public ObservableCollection Details { get; internal set; } - - public int Score { get; internal set; } - public StrengthRate Rate { get; internal set; } - public List SequCharsFound { get; private set; } - public MissedRequirements MissedRequirements { get; private set; } - - public bool RequirementsMet => - MissedRequirements.Count == 0 && MissedRequirements.ForbiddenWords.Count == 0 && - MissedRequirements.NoWhitespaceAllowed; - - public DataTable GetDetailsAsDatatable() - { - var dt = new DataTable("Details"); - dt.Columns.Add("Description", typeof(string)); - dt.Columns.Add("Rate", typeof(string)); - dt.Columns.Add("Count", typeof(int)); - dt.Columns.Add("Bonus", typeof(int)); - dt.Columns.Add("Malus", typeof(int)); - - foreach (var d in Details) - { - dt.Rows.Add(d.Description, d.Rate, d.Count, d.Bonus, d.Malus); - } - - return dt; - } -} - - -/// -/// Analysis result for one rule -/// -public class StrengthDetail -{ - public string Description { get; set; } = string.Empty; - public string Rate { get; set; } = string.Empty; - public int Count { get; set; } - public int Bonus { get; set; } - public int Malus { get; set; } -} - -public enum StrengthRate -{ - Unknown, Unsafe, Weak, Fair, Strong, Secure -} diff --git a/Axuno.Tools/Password/Password_Reset.cs b/Axuno.Tools/Password/Password_Reset.cs deleted file mode 100644 index f5fd698d..00000000 --- a/Axuno.Tools/Password/Password_Reset.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Globalization; - -namespace Axuno.Tools.Password; - -public class Reset -{ - // exactly 24 bytes for Encryption Key - private const string _key = "!?aht97+ _tripleDes = new(_key, _iv); - - private Reset() - { - _tripleDes.ToContainer = ToContainer; - _tripleDes.ToText = ToText; - } - - internal static Reset Current { get; } = new(); - - private string ToText(ResetModel m) - { - return string.Join("\0", m.Id.ToString(CultureInfo.InvariantCulture), - m.UsernameCrc32.ToString(CultureInfo.InvariantCulture), - m.PasswordCrc32.ToString(CultureInfo.InvariantCulture)); - } - - private static ResetModel ToContainer(string text, ResetModel m) - { - var split = (text ?? string.Empty).Split(new[] { '\0' }, 3); - m.Id = long.Parse(split[0]); - m.UsernameCrc32 = uint.Parse(split[1]); - m.PasswordCrc32 = uint.Parse(split[2]); - return m; - } - - public string GetResetKey(ResetModel model, DateTime expiresOn) - { - return _tripleDes.Encrypt(model, expiresOn: expiresOn); - } - - public ExpiringAesEncryptor.DecryptionResult DecryptResetKey(string encrypted, ResetModel model) - { - return _tripleDes.Decrypt(encrypted, model); - } -} - -public class ResetModel -{ - public long Id { get; set; } - public uint UsernameCrc32 { get; set; } - public uint PasswordCrc32 { get; set; } -} diff --git a/League/Controllers/Account.cs b/League/Controllers/Account.cs index dfb7b9fb..c6cea74b 100644 --- a/League/Controllers/Account.cs +++ b/League/Controllers/Account.cs @@ -133,7 +133,7 @@ public async Task SignIn(SignInViewModel model, string? returnUrl if (user == null) { - _logger.LogInformation("No account found for '{user}'.", model.EmailOrUsername); + _logger.LogInformation("No account found for '{User}'.", model.EmailOrUsername); ModelState.AddModelError(string.Empty, _localizer["No account found for these credentials."].Value); return View(model); } @@ -142,7 +142,7 @@ public async Task SignIn(SignInViewModel model, string? returnUrl if (result.Succeeded) { - _logger.LogInformation("User Id '{userId}' signed in.", user.Id); + _logger.LogInformation("User Id '{UserId}' signed in.", user.Id); return RedirectToLocal(returnUrl ?? "/" + _tenantContext.SiteContext.UrlSegmentValue); } @@ -151,16 +151,16 @@ public async Task SignIn(SignInViewModel model, string? returnUrl if (!await _signInManager.UserManager.IsEmailConfirmedAsync(user) && _signInManager.UserManager.Options.SignIn.RequireConfirmedEmail) { - _logger.LogInformation("Sign-in not allowed: Email for user id '{userId}' is not confirmed", user.Id); + _logger.LogInformation("Sign-in not allowed: Email for user id '{UserId}' is not confirmed", user.Id); return Redirect(TenantLink.Action(nameof(Message), nameof(Account), new { messageTypeText = MessageType.SignInRejectedEmailNotConfirmed })!); } - _logger.LogInformation("Account for user id '{userId}': {result}", user.Id, result); + _logger.LogInformation("Account for user id '{UserId}': {Result}", user.Id, result); return Redirect(TenantLink.Action(nameof(Message), nameof(Account), new { messageTypeText = MessageType.SignInRejected })!); } // PasswordSignIn failed - _logger.LogInformation("Wrong password for '{user}'.", model.EmailOrUsername); + _logger.LogInformation("Wrong password for '{User}'.", model.EmailOrUsername); ModelState.AddModelError(string.Empty, _localizer["No account found for these credentials."].Value); return View(model); } @@ -273,7 +273,7 @@ public async Task Register(RegisterViewModel model, CancellationT if (result.Succeeded) { - _logger.LogInformation("User (email: {userEmail}, username: {userName}) created a new account with password.", user.Email, user.UserName); + _logger.LogInformation("User (email: {UserEmail}, username: {UserName}) created a new account with password.", user.Email, user.UserName); if (await _signInManager.CanSignInAsync(user)) { await _signInManager.SignInAsync(user, isPersistent: false); @@ -282,7 +282,7 @@ public async Task Register(RegisterViewModel model, CancellationT return Redirect(TenantLink.Action(nameof(Manage.Index), nameof(Manage))!); } - _logger.LogError("User (email: {userEmail}) could not be created. {errors}", user.Email, result.Errors); + _logger.LogError("User (email: {UserEmail}) could not be created. {Errors}", user.Email, result.Errors); AddErrors(result); return View(model); } @@ -305,79 +305,40 @@ public async Task ExternalSignInCallback(string? returnUrl = null { if (remoteError != null) { - _logger.LogInformation("{method} failed. Remote error was supplied.", nameof(ExternalSignInCallback)); + _logger.LogInformation("{Method} failed. Remote error was supplied.", nameof(ExternalSignInCallback)); return SocialMediaSignInFailure(_localizer["Failed to sign-in with the social network account"].Value + $" ({remoteError})"); } + var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) { - _logger.LogInformation("{method} failed to get external sign-in information", nameof(ExternalSignInCallback)); + _logger.LogInformation("{Method} failed to get external sign-in information", nameof(ExternalSignInCallback)); return SocialMediaSignInFailure(_localizer["Failed to sign-in with the social network account"].Value); } - - // Sign-in user if provider represents a valid already registered user - // There is no external login stored for this user? - if (await _signInManager.UserManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey) == null) + if (await IsExternalLoginStoredAsync(info)) { - ApplicationUser? existingUser = null; - // if the current user is signed in - if (User.Identity!.IsAuthenticated) - { - existingUser = await _signInManager.UserManager.FindByNameAsync(User.Identity.Name); - } - else - { - // see if there is a user account for any external provider email - var allExternalEmails = info.Principal.FindAll(ClaimTypes.Email).Select(ct => ct.Value); - foreach (var externalEmail in allExternalEmails) - { - existingUser = await _signInManager.UserManager.FindByEmailAsync(externalEmail); - if (existingUser != null) break; - } - } - if (existingUser != null) + var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false); + if (result.Succeeded) { - // Add external user login for this user. - if (await _signInManager.UserManager.AddLoginAsync(existingUser, info) != IdentityResult.Success) - { - _logger.LogInformation("{method} {email} is already associated with another account", nameof(ExternalSignInCallback), existingUser.Email); - return SocialMediaSignInFailure(_localizer.GetString("Your {0} account is already associated with another account at {1}", info.LoginProvider, _tenantContext.OrganizationContext.ShortName).Value); - } + await _signInManager.UpdateExternalAuthenticationTokensAsync(info); - await UpdateUserFromExternalLogin(existingUser, info); + _logger.LogInformation("User signed-in in with login provider '{LoginProvider}'.", info.LoginProvider); + return RedirectToLocal(returnUrl ?? "/" + _tenantContext.SiteContext.UrlSegmentValue); } - } - - // sign in the user with this external login provider if the user already has a registered external login. - var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false); - if (result.Succeeded) - { - // Update any authentication tokens if login succeeded - await _signInManager.UpdateExternalAuthenticationTokensAsync(info); - _logger.LogInformation("User signed-in in with login provider '{loginProvider}'.", info.LoginProvider); - return RedirectToLocal(returnUrl ?? "/" + _tenantContext.SiteContext.UrlSegmentValue); - } - if (result.IsLockedOut || result.IsNotAllowed) - { - _logger.LogInformation("Account for username '{identityName}': {result}", info.Principal.Identity?.Name ?? "(null)", result); - return View(ViewNames.Account.SignInRejected, result); + if (result.IsLockedOut || result.IsNotAllowed) + { + _logger.LogInformation("Account for username '{IdentityName}': {Result}", info.Principal.Identity?.Name ?? "(null)", result); + return View(ViewNames.Account.SignInRejected, result); + } } - // If the user does not have an account, then ask the user to create an account. ViewData["ReturnUrl"] = returnUrl; ViewData["LoginProvider"] = info.LoginProvider; - // We use the first email returned by the provider for the new account + var model = CreateExternalSignInConfirmationViewModelAsync(info.Principal); - return View(ViewNames.Account.ExternalSignInConfirmation, - new ExternalSignConfirmationViewModel - { - Email = info.Principal.FindFirstValue(ClaimTypes.Email), - Gender = string.Empty, - FirstName = info.Principal.FindFirstValue(ClaimTypes.GivenName) ?? string.Empty, - LastName = info.Principal.FindFirstValue(ClaimTypes.Surname) ?? string.Empty - }); + return View(ViewNames.Account.ExternalSignInConfirmation, model); } [HttpPost(nameof(ExternalSignInConfirmation))] @@ -395,7 +356,7 @@ public async Task ExternalSignInConfirmation(ExternalSignConfirma var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) { - _logger.LogInformation($"{nameof(ExternalSignInConfirmation)} failed to get external sign-in information"); + _logger.LogInformation("{Method} failed to get external sign-in information", nameof(ExternalSignInConfirmation)); return Redirect(TenantLink.Action(nameof(Message), nameof(Account), new { messageTypeText = MessageType.ExternalSignInFailure })!); } var user = new ApplicationUser @@ -420,13 +381,13 @@ public async Task ExternalSignInConfirmation(ExternalSignConfirma { await _signInManager.SignInAsync(user, isPersistent: false); _logger.LogInformation( - "User created an account for email '{userEmail}' using {loginProvider} provider.", user.Email, info.LoginProvider); + "User created an account for email '{UserEmail}' using {LoginProvider} provider.", user.Email, info.LoginProvider); // Update any authentication tokens as well if (await _signInManager.UpdateExternalAuthenticationTokensAsync(info) != IdentityResult.Success) _logger.LogWarning( - "External authentication tokens could not be updated for email '{userEmail}' using {loginProvider} provider.", user.Email, info.LoginProvider); + "External authentication tokens could not be updated for email '{UserEmail}' using {LoginProvider} provider.", user.Email, info.LoginProvider); // email is flagged as confirmed if local email and external email are equal if (user.EmailConfirmed) @@ -469,7 +430,7 @@ public async Task ForgotPassword(ForgotPasswordViewModel model) if (user == null || (!await _signInManager.UserManager.IsEmailConfirmedAsync(user) && _signInManager.UserManager.Options.SignIn.RequireConfirmedEmail)) { - _logger.LogInformation("No account found for '{user}'.", model.EmailOrUsername); + _logger.LogInformation("No account found for '{User}'.", model.EmailOrUsername); ModelState.AddModelError(string.Empty, _localizer["No account found for these credentials."]); await Task.Delay(5000); return View(model); @@ -517,7 +478,7 @@ public async Task ResetPassword(ResetPasswordViewModel model) if (user == null) { - _logger.LogInformation("No account found for '{user}'.", model.EmailOrUsername); + _logger.LogInformation("No account found for '{User}'.", model.EmailOrUsername); ModelState.AddModelError(string.Empty, _localizer["No account found for these credentials."]); await Task.Delay(5000); return View(); @@ -569,10 +530,57 @@ public IActionResult Message(string messageTypeText) } } - _logger.LogWarning("Undefined {messageType}: '{messageText}'", nameof(MessageType), messageTypeText); + _logger.LogWarning("Undefined {MessageType}: '{MessageText}'", nameof(MessageType), messageTypeText); return RedirectToLocal("/" + _tenantContext.SiteContext.UrlSegmentValue); } + private async Task IsExternalLoginStoredAsync(ExternalLoginInfo info) + { + var existingUser = await FindExistingUserAsync(info); + if (existingUser == null) return true; + + await AssociateExternalLoginAsync(existingUser, info); + return false; + } + private async Task FindExistingUserAsync(ExternalLoginInfo info) + { + if (User.Identity?.IsAuthenticated ?? false) + { + return await _signInManager.UserManager.FindByNameAsync(User.Identity.Name); + } + + var allExternalEmails = info.Principal.FindAll(ClaimTypes.Email).Select(ct => ct.Value); + foreach (var externalEmail in allExternalEmails) + { + var user = await _signInManager.UserManager.FindByEmailAsync(externalEmail); + if (user != null) return user; + } + + return null; + } + + private async Task AssociateExternalLoginAsync(ApplicationUser user, ExternalLoginInfo info) + { + var result = await _signInManager.UserManager.AddLoginAsync(user, info); + if (result != IdentityResult.Success) + { + _logger.LogInformation("{Method} {Email} is already associated with another account", nameof(ExternalSignInCallback), user.Email); + throw new InvalidOperationException(_localizer.GetString("Your {0} account is already associated with another account at {1}", info.LoginProvider, _tenantContext.OrganizationContext.ShortName).Value); + } + await UpdateUserFromExternalLogin(user, info); + } + + private static ExternalSignConfirmationViewModel CreateExternalSignInConfirmationViewModelAsync(ClaimsPrincipal principal) + { + return new ExternalSignConfirmationViewModel + { + Email = principal.FindFirstValue(ClaimTypes.Email), + Gender = string.Empty, + FirstName = principal.FindFirstValue(ClaimTypes.GivenName) ?? string.Empty, + LastName = principal.FindFirstValue(ClaimTypes.Surname) ?? string.Empty + }; + } + #region ** Helpers ** private void AddErrors(IdentityResult result) @@ -673,7 +681,7 @@ private async Task SendCodeByEmail(ApplicationUser user, EmailPurpose purpose) }); break; default: - _logger.LogError($"Illegal enum type for {nameof(EmailPurpose)}"); + _logger.LogError("Illegal enum type for {Purpose}", nameof(EmailPurpose)); break; } diff --git a/League/Identity/RoleStore.cs b/League/Identity/RoleStore.cs index 41573afe..9beee7e7 100644 --- a/League/Identity/RoleStore.cs +++ b/League/Identity/RoleStore.cs @@ -179,7 +179,7 @@ public Task SetRoleNameAsync(ApplicationRole role, string roleName, Cancellation return Task.CompletedTask; } - public async Task> GetClaimsAsync(ApplicationRole role, CancellationToken cancellationToken) + public async Task> GetClaimsAsync(ApplicationRole role, CancellationToken cancellationToken = default) { if (role == null) throw new ArgumentNullException(nameof(role)); @@ -188,7 +188,7 @@ public async Task> GetClaimsAsync(ApplicationRole role, Cancellatio return claimEntities.Select(claimEntity => new Claim(claimEntity.ClaimType, claimEntity.ClaimValue, claimEntity.ValueType, claimEntity.Issuer)).ToList(); } - public async Task AddClaimAsync(ApplicationRole role, Claim claim, CancellationToken cancellationToken) + public async Task AddClaimAsync(ApplicationRole role, Claim claim, CancellationToken cancellationToken = default) { if (role == null) throw new ArgumentNullException(nameof(role)); @@ -221,7 +221,7 @@ public async Task AddClaimAsync(ApplicationRole role, Claim claim, CancellationT } } - public async Task RemoveClaimAsync(ApplicationRole role, Claim claim, CancellationToken cancellationToken) + public async Task RemoveClaimAsync(ApplicationRole role, Claim claim, CancellationToken cancellationToken = default) { if (role == null) throw new ArgumentNullException(nameof(role)); diff --git a/League/Models/MatchViewModels/EnterResultViewModel.cs b/League/Models/MatchViewModels/EnterResultViewModel.cs index b80e22e5..979f7131 100644 --- a/League/Models/MatchViewModels/EnterResultViewModel.cs +++ b/League/Models/MatchViewModels/EnterResultViewModel.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using HarfBuzzSharp; using Microsoft.AspNetCore.Mvc.ModelBinding; using TournamentManager; using TournamentManager.DAL; @@ -212,6 +213,18 @@ private string ComputeInputHash() } public async Task ValidateAsync(MatchResultValidator validator, ModelStateDictionary modelState) + { + await ValidateWithValidatorAsync(validator, modelState); + + if (!modelState.IsValid) + { + HandleInvalidModelState(modelState); + } + + return modelState.IsValid; + } + + private async Task ValidateWithValidatorAsync(MatchResultValidator validator, ModelStateDictionary modelState) { await validator.CheckAsync(CancellationToken.None); @@ -219,69 +232,89 @@ public async Task ValidateAsync(MatchResultValidator validator, ModelState { if (fact.Type == FactType.Critical || fact.Type == FactType.Error) { - if (fact.FieldNames.Contains(nameof(MatchResultValidator.Model.RealStart)) - || fact.FieldNames.Contains(nameof(MatchResultValidator.Model.RealEnd))) - { - modelState.AddModelError(nameof(EnterResultViewModel.MatchDate), fact.Message); - } + HandleCriticalOrErrorFact(fact, validator, modelState); + } + else + { + HandleWarningFact(fact, modelState); + } + } + } - if (fact.Id == MatchResultValidator.FactId.SetsValidatorSuccessful) - { - foreach (var setsError in validator.SetsValidator.GetFailedFacts()) - { - // This is the hint for existing errors in single sets - if (setsError.Id == SetsValidator.FactId.AllSetsAreValid) - { - foreach (var singleSet in validator.SetsValidator.SingleSetErrors) - { - modelState.AddModelError( - singleSet.FactId == SingleSetValidator.FactId.SetPointsAreValid // we have just one set error - ? $"{nameof(SetPoints)}-{singleSet.SequenceNo - 1}" - : $"{nameof(BallPoints)}-{singleSet.SequenceNo - 1}", - string.Concat(string.Format(_localizer["Set #{0}"], singleSet.SequenceNo), ": ", - singleSet.ErrorMessage)); - } - } - else - { - // Errors about sets in general - modelState.AddModelError("", setsError.Message); - } - } - } + private void HandleCriticalOrErrorFact(Fact fact, MatchResultValidator validator, ModelStateDictionary modelState) + { + if (fact.FieldNames.Contains(nameof(MatchResultValidator.Model.RealStart)) || + fact.FieldNames.Contains(nameof(MatchResultValidator.Model.RealEnd))) + { + AddModelErrorForMatchDate(fact, modelState); + } + else if (fact.Id == MatchResultValidator.FactId.SetsValidatorSuccessful) + { + AddModelErrorForSets(validator, modelState); + } + else if (fact.Id == MatchResultValidator.FactId.MatchPointsAreValid) + { + AddModelErrorForMatchPoints(fact, modelState); + } + else + { + AddModelErrorForOtherFacts(fact, modelState); + } + } - if (fact.Id == MatchResultValidator.FactId.MatchPointsAreValid) + private void AddModelErrorForMatchDate(Fact fact, ModelStateDictionary modelState) + { + modelState.AddModelError(nameof(EnterResultViewModel.MatchDate), fact.Message); + } + + private void AddModelErrorForSets(MatchResultValidator validator, ModelStateDictionary modelState) + { + foreach (var setsError in validator.SetsValidator.GetFailedFacts()) + { + if (setsError.Id == SetsValidator.FactId.AllSetsAreValid) + { + foreach (var singleSet in validator.SetsValidator.SingleSetErrors) { - modelState.AddModelError($"{nameof(HomePoints)}", fact.Message); + var fieldName = singleSet.FactId == SingleSetValidator.FactId.SetPointsAreValid + ? $"{nameof(SetPoints)}-{singleSet.SequenceNo - 1}" + : $"{nameof(BallPoints)}-{singleSet.SequenceNo - 1}"; + + modelState.AddModelError(fieldName, $"{string.Format(_localizer["Set #{0}"], singleSet.SequenceNo)}: {singleSet.ErrorMessage}"); } } else { - modelState.AddModelError(string.Empty, fact.Message); - // Validator generates FactType.Warning only, if no errors exist - IsWarning = true; + modelState.AddModelError("", setsError.Message); } } + } + + private void AddModelErrorForMatchPoints(Fact fact, ModelStateDictionary modelState) + { + modelState.AddModelError($"{nameof(HomePoints)}", fact.Message); + } - // The Hash is re-calculated with the new submitted values. - // We have to compare to the original hidden Hash field value, - // because to override warnings, form fields must be unchanged since last post + private void AddModelErrorForOtherFacts(Fact fact, ModelStateDictionary modelState) + { + modelState.AddModelError(string.Empty, fact.Message); + } + + private void HandleWarningFact(Fact fact, ModelStateDictionary modelState) + { + modelState.AddModelError(string.Empty, fact.Message); + IsWarning = true; + } + + private void HandleInvalidModelState(ModelStateDictionary modelState) + { var newHash = ComputeInputHash(); if (IsWarning && OverrideWarnings && newHash == Hash) { modelState.Clear(); IsWarning = false; } - - if (!modelState.IsValid) - { - // Show checkbox unchecked - OverrideWarnings = false; - // Set hash field value to latest form fields content - Hash = newHash; - } - - return modelState.IsValid; + OverrideWarnings = false; + Hash = newHash; } public class MatchResultMessage diff --git a/TournamentManager/TournamentManager/Importers/ExcludeDates/InternetCalendarImporter.cs b/TournamentManager/TournamentManager/Importers/ExcludeDates/InternetCalendarImporter.cs index ae7e9be5..93e3c5ea 100644 --- a/TournamentManager/TournamentManager/Importers/ExcludeDates/InternetCalendarImporter.cs +++ b/TournamentManager/TournamentManager/Importers/ExcludeDates/InternetCalendarImporter.cs @@ -37,6 +37,7 @@ public IEnumerable Import(DateTimePeriod fromToTimePeriod) { _iCalendarStreamReader.BaseStream.Position = 0; var iCal = Ical.Net.Calendar.Load(_iCalendarStreamReader); + _logger.LogInformation("Imported {Count} events from iCalendar", iCal.Events.Count); return Map(iCal, fromToTimePeriod); } diff --git a/TournamentManager/TournamentManager/Plan/MatchScheduler.cs b/TournamentManager/TournamentManager/Plan/MatchScheduler.cs index baa6e685..1c94e7a2 100644 --- a/TournamentManager/TournamentManager/Plan/MatchScheduler.cs +++ b/TournamentManager/TournamentManager/Plan/MatchScheduler.cs @@ -365,64 +365,57 @@ private static List GetOccupiedMatchDates(ParticipantCombination /// +#pragma warning disable S3776 // Method is already refactored and should not be split further private static List FindBestDatePerCombination(List> availableMatchDates) { - var bestMatchDatePerCombination = new List(); - // Cross-compute the number of dates between matches of a turn. // Goal: Found match dates are as close to each other as possible - // start with 1st dates, end with last but one dates - for (var i = 0; i < availableMatchDates.Count - 1; i++) + var bestMatchDatePerCombination = new List(); + + // Loop through each team's available match dates + for (var i = 0; i < availableMatchDates.Count; i++) { - // start with 2nd dates, end with last dates - for (var j = 1; j < availableMatchDates.Count; j++) + // Loop through each opponent team's available match dates + for (var j = 0; j < availableMatchDates.Count; j++) { - // compare each date in the first list... - foreach (var dates1 in availableMatchDates[i]) + // Skip comparing the team's own dates with itself + if (i != j) { - // ... with the dates in the second list - foreach (var dates2 in availableMatchDates[j]) + // Compare each date in the team's list with dates in the opponent's list + foreach (var dates1 in availableMatchDates[i]) { - var daysDiff = Math.Abs((dates1.MatchStartTime.Date - dates2.MatchStartTime.Date).Days); - - // save minimum dates found for later reference - if (daysDiff < dates1.MinTimeDiff.Days) - dates1.MinTimeDiff = new TimeSpan(daysDiff, 0, 0, 0); - - if (daysDiff < dates2.MinTimeDiff.Days) - dates2.MinTimeDiff = new TimeSpan(daysDiff, 0, 0, 0); - } // end dates2 - } // end dates1 - } // end j + foreach (var dates2 in availableMatchDates[j]) + { + // Calculate the time difference between the match dates + var daysDiff = Math.Abs((dates1.MatchStartTime.Date - dates2.MatchStartTime.Date).Days); + + // Update the minimum time difference for each date + if (daysDiff < dates1.MinTimeDiff.Days) + dates1.MinTimeDiff = new TimeSpan(daysDiff, 0, 0, 0); + + if (daysDiff < dates2.MinTimeDiff.Days) + dates2.MinTimeDiff = new TimeSpan(daysDiff, 0, 0, 0); + } + } + } + } - // Get the date that has the smallest distance to the smallest date in the other turn(s). + // Find the best match date for this team based on the minimum time difference // Note: If no match dates could be determined for a team, bestDate will be null. var bestDate = availableMatchDates[i] - .Where(md => md.MinTimeDiff == availableMatchDates[i] - .Min(d => d.MinTimeDiff)) + .Where(md => md.MinTimeDiff == availableMatchDates[i].Min(d => d.MinTimeDiff)) .MinBy(md => md.MinTimeDiff); - bestMatchDatePerCombination.Add(bestDate); - - // process the last combination - // in case comparisons took place, - // now the "j-loop" group is not processed yet: - if (i + 1 >= availableMatchDates.Count - 1) - { - bestDate = availableMatchDates[^1] - .Where(md => md.MinTimeDiff == availableMatchDates[^1]. - Min(d => d.MinTimeDiff)) - .MinBy(md => md.MinTimeDiff); - // the last "j-increment" is always the same as "matchDates[^1]" (loop condition) - bestMatchDatePerCombination.Add(bestDate); - } - } // end i + // Add the best match date to the list + bestMatchDatePerCombination.Add(bestDate); + } // returns the best match date found per combination, // so the number of elements is the same as the number of combinations return bestMatchDatePerCombination; } +#pragma warning restore S3776 /// /// Desired s are assigned to round turns mathematically, diff --git a/TournamentManager/TournamentManager/Ranking/Ranking.cs b/TournamentManager/TournamentManager/Ranking/Ranking.cs index 0d1c5ed8..d40a21b7 100644 --- a/TournamentManager/TournamentManager/Ranking/Ranking.cs +++ b/TournamentManager/TournamentManager/Ranking/Ranking.cs @@ -97,8 +97,23 @@ internal RankingList GetList(IEnumerable teamIds, DateTime upperDateLimit) private RankingList GetUnsortedList(IEnumerable teamIds, DateTime upperDateLimit, out DateTime lastUpdatedOn) { - var teamIdList = teamIds.ToList(); // no multiple enumerations - lastUpdatedOn = DateTime.UtcNow; + var teamIdList = teamIds.ToList(); + lastUpdatedOn = GetLastUpdatedOn(teamIdList); + + var rankingList = new RankingList { UpperDateLimit = upperDateLimit, LastUpdatedOn = lastUpdatedOn }; + + foreach (var teamId in teamIdList) + { + var rank = CalculateRank(teamId, upperDateLimit); + rankingList.Add(rank); + } + + return rankingList; + } + + private DateTime GetLastUpdatedOn(ICollection teamIdList) + { + var lastUpdatedOn = DateTime.UtcNow; if (MatchesPlayed.Any()) lastUpdatedOn = MatchesPlayed .Where(m => teamIdList.Contains(m.HomeTeamId) || teamIdList.Contains(m.GuestTeamId)) @@ -108,55 +123,51 @@ private RankingList GetUnsortedList(IEnumerable teamIds, DateTime upperDat .Where(m => teamIdList.Contains(m.HomeTeamId) || teamIdList.Contains(m.GuestTeamId)) .Max(m => m.ModifiedOn); - var rankingList = new RankingList {UpperDateLimit = upperDateLimit, LastUpdatedOn = lastUpdatedOn}; + return lastUpdatedOn; + } - foreach (var teamId in teamIdList) + private Rank CalculateRank(long teamId, DateTime upperDateLimit) + { + var rank = new Rank { Number = -1, TeamId = teamId }; + + foreach (var match in GetMatchesPlayedForTeam(teamId, upperDateLimit)) { - var rank = new Rank {Number = -1, TeamId = teamId, MatchesPlayed = MatchesPlayed.Count(m => m.HomeTeamId == teamId || m.GuestTeamId == teamId), MatchesToPlay = MatchesToPlay.Count(m => m.HomeTeamId == teamId || m.GuestTeamId == teamId)}; - - // upperDateLimit contains the date part (without time), so the MatchDate must also be compared with the date part! - foreach (var match in MatchesPlayed.Where(m => (m.HomeTeamId == teamId || m.GuestTeamId == teamId) && m.MatchDate.HasValue && m.MatchDate.Value.Date <= upperDateLimit.Date)) - { - if (match.HomeTeamId == teamId) - { - rank.MatchPoints.Home += match.HomeMatchPoints ?? 0; - rank.MatchPoints.Guest += match.GuestMatchPoints ?? 0; - - rank.SetPoints.Home += match.HomeSetPoints ?? 0; - rank.SetPoints.Guest += match.GuestSetPoints ?? 0; - - rank.BallPoints.Home += match.HomeBallPoints ?? 0; - rank.BallPoints.Guest += match.GuestBallPoints ?? 0; - - rank.MatchesWon.Home += match.HomeMatchPoints > match.GuestMatchPoints ? 1 : 0; - rank.MatchesWon.Guest += match.HomeMatchPoints < match.GuestMatchPoints ? 1 : 0; - - rank.SetsWon.Home += match.HomeSetPoints; - rank.SetsWon.Guest += match.GuestSetPoints; - } - else - { - rank.MatchPoints.Home += match.GuestMatchPoints ?? 0; - rank.MatchPoints.Guest += match.HomeMatchPoints ?? 0; - - rank.SetPoints.Home += match.GuestSetPoints ?? 0; - rank.SetPoints.Guest += match.HomeSetPoints ?? 0; - - rank.BallPoints.Home += match.GuestBallPoints ?? 0; - rank.BallPoints.Guest += match.HomeBallPoints ?? 0; - - rank.MatchesWon.Home += match.HomeMatchPoints < match.GuestMatchPoints ? 1 : 0; - rank.MatchesWon.Guest += match.HomeMatchPoints > match.GuestMatchPoints ? 1 : 0; - - // Todo: This only correct if SetRule.PointsSetWon = 1 and SetRule.PointsSetLost = 0 and SetRule.PointSetTie = 0 - rank.SetsWon.Home += match.GuestSetPoints; - rank.SetsWon.Guest += match.HomeSetPoints; - } - } - rankingList.Add(rank); + UpdateRankStatistics(rank, match, teamId); } - return rankingList; + return rank; + } + + private IEnumerable GetMatchesPlayedForTeam(long teamId, DateTime upperDateLimit) + { + return MatchesPlayed + .Where(m => (m.HomeTeamId == teamId || m.GuestTeamId == teamId) && + m.MatchDate.HasValue && + m.MatchDate.Value.Date <= upperDateLimit.Date); + } + + private void UpdateRankStatistics(Rank rank, MatchCompleteRawRow match, long teamId) + { + var isHomeTeam = match.HomeTeamId == teamId; + + var homeMatchPoints = isHomeTeam ? match.HomeMatchPoints ?? 0 : match.GuestMatchPoints ?? 0; + var guestMatchPoints = isHomeTeam ? match.GuestMatchPoints ?? 0 : match.HomeMatchPoints ?? 0; + var homeSetPoints = isHomeTeam ? match.HomeSetPoints ?? 0 : match.GuestSetPoints ?? 0; + var guestSetPoints = isHomeTeam ? match.GuestSetPoints ?? 0 : match.HomeSetPoints ?? 0; + var homeBallPoints = isHomeTeam ? match.HomeBallPoints ?? 0 : match.GuestBallPoints ?? 0; + var guestBallPoints = isHomeTeam ? match.GuestBallPoints ?? 0 : match.HomeBallPoints ?? 0; + + rank.MatchPoints.Home += homeMatchPoints; + rank.MatchPoints.Guest += guestMatchPoints; + rank.SetPoints.Home += homeSetPoints; + rank.SetPoints.Guest += guestSetPoints; + rank.BallPoints.Home += homeBallPoints; + rank.BallPoints.Guest += guestBallPoints; + + rank.MatchesWon.Home += homeMatchPoints > guestMatchPoints ? 1 : 0; + rank.MatchesWon.Guest += homeMatchPoints < guestMatchPoints ? 1 : 0; + rank.SetsWon.Home += homeSetPoints > guestSetPoints ? 1 : 0; + rank.SetsWon.Guest += guestSetPoints > homeSetPoints ? 1 : 0; } /// diff --git a/TournamentManager/TournamentManager/RoundRobin/MatchesAnalyzer.cs b/TournamentManager/TournamentManager/RoundRobin/MatchesAnalyzer.cs index 357cbac1..a2f788ee 100644 --- a/TournamentManager/TournamentManager/RoundRobin/MatchesAnalyzer.cs +++ b/TournamentManager/TournamentManager/RoundRobin/MatchesAnalyzer.cs @@ -74,34 +74,33 @@ public static (int HomeCount, int GuestCount) GetMaxConsecutiveHomeGuestCount(TP public static IEnumerable GetLastConsecutiveCounts(TP participant, bool forHome, IList<(int Turn, TP Home, TP Guest)> matches) { using var e = matches.Reverse().GetEnumerator(); - for (var more = e.MoveNext(); more;) + + while (e.MoveNext()) { var first = forHome ? e.Current.Home : e.Current.Guest; - if (first.Equals(participant)) + if (!first.Equals(participant)) { - var count = 1; - while (more && e.MoveNext()) - { - first = forHome ? e.Current.Home : e.Current.Guest; - var second = forHome ? e.Current.Guest : e.Current.Home; + yield return 0; + continue; + } + + var count = 1; + while (e.MoveNext()) + { + first = forHome ? e.Current.Home : e.Current.Guest; + var second = forHome ? e.Current.Guest : e.Current.Home; - if (first.Equals(participant)) - { - count++; - } + if (first.Equals(participant)) + { + count++; + } - if (second.Equals(participant)) - { - break; - } + if (second.Equals(participant)) + { + break; } - yield return count; - } - else - { - yield return 0; } - more = e.MoveNext(); + yield return count; } } }