/// <summary> /// Imports excluded match dates from an Excel workbook. The first worksheet of the workbook will be used. /// 3 expected columns: <see cref="DateTime"/> DateFrom, <see cref="DateTime"/> DateTo, <see cref="string"/> Reason. /// </summary> /// <remarks> /// Excel date values are considered as local time and will be converted to UTC. /// A maximum of 1,000 rows will be imported. /// </remarks> /// <param name="xlPathAndFileName">The path to the Excel file.</param> /// <param name="dateLimits">The limits for dates, which will be imported.</param> /// <returns> /// Returns an <see cref="IEnumerable{T}" />of <see cref="ExcludeMatchDateEntity"/>. /// <see cref="ExcludeMatchDateEntity.TournamentId"/>, <see cref="ExcludeMatchDateEntity.RoundId"/> and <see cref="ExcludeMatchDateEntity.TeamId"/> will not be set. /// </returns> public IEnumerable <ExcludeMatchDateEntity> Import(string xlPathAndFileName, DateTimePeriod dateLimits) { var xlFile = new FileInfo(xlPathAndFileName); _logger.LogTrace("Opening Excel file '{0}'", xlPathAndFileName); using var package = new ExcelPackage(xlFile); var worksheet = package.Workbook.Worksheets.First(); _logger.LogTrace("Using the first worksheet, '{0}'", worksheet.Name); _logger.LogTrace("Date limits are {0} - {1}", dateLimits.Start, dateLimits.End); var row = 0; while (true) { row++; if (row == 1 && !(worksheet.Cells[row, 1].Value is DateTime)) { _logger.LogTrace("First cell is not a date, assume existing headline row"); continue; // may contain a headline row } if (!(worksheet.Cells[row, 1].Value is DateTime from && worksheet.Cells[row, 2].Value is DateTime to) || row > 1000) { _logger.LogTrace("Import finished with worksheet row {0}", row - 1); yield break; } from = _timeZoneConverter.ToUtc(from.Date); to = _timeZoneConverter.ToUtc(to.Date); if (!dateLimits.Overlaps(new DateTimePeriod(from, to))) { _logger.LogTrace("UTC Dates {0} - {1} are out of limits", from, to); continue; } var reason = worksheet.Cells[row, 3].Value as string ?? string.Empty; yield return(CreateEntity((from, to, reason))); _logger.LogTrace("Imported UTC {0} - {1} ({2})", from, to, reason); } }
private IEnumerable <ExcludeMatchDateEntity> Map(List <Axuno.Tools.GermanHoliday> holidays, DateTimePeriod dateLimits) { // sort short date ranges before big ranges var holidayGroups = holidays.ConsecutiveRanges() .OrderBy(tuple => tuple.From.Date).ThenBy(tuple => (tuple.To - tuple.From).Days); foreach (var holidayGroup in holidayGroups) { var entity = CreateEntity(holidayGroup); if (!dateLimits.Contains(entity.DateFrom) && !dateLimits.Contains(entity.DateTo)) { continue; } // convert from import time zone to UTC entity.DateFrom = _timeZoneConverter.ToUtc(entity.DateFrom.Date); entity.DateTo = _timeZoneConverter.ToUtc(entity.DateTo.AddDays(1).AddSeconds(-1)); yield return(entity); } }
public async Task <IActionResult> EditFixture([FromForm] EditFixtureViewModel model, CancellationToken cancellationToken) { // [FromBody] => 'content-type': 'application/json' // [FromForm] => 'content-type': 'application/x-www-form-urlencoded' model = new EditFixtureViewModel(await GetPlannedMatchFromDatabase(model.Id, cancellationToken), _timeZoneConverter) { Tournament = await GetPlanTournament(cancellationToken) }; if (model.PlannedMatch == null || model.Tournament == null) { var msg = $"No data for fixture id '{model.Id}'. User ID '{GetCurrentUserId()}'"; _logger.LogInformation(msg); return(NotFound(msg)); } if (!(await _authorizationService.AuthorizeAsync(User, new MatchEntity { HomeTeamId = model.PlannedMatch.HomeTeamId, GuestTeamId = model.PlannedMatch.GuestTeamId, VenueId = model.PlannedMatch.VenueId, OrigVenueId = model.PlannedMatch.OrigVenueId }, Authorization.MatchOperations.ChangeFixture)).Succeeded) { return(Forbid()); } // sync input with new model instance if (!await TryUpdateModelAsync(model)) { return(View(ViewNames.Match.EditFixture, await AddDisplayDataToEditFixtureViewModel(model, cancellationToken))); } // create a new MatchEntity for validation var match = FillMatchEntity(model.PlannedMatch); match.SetPlannedStart(model.MatchDate.HasValue && model.MatchTime.HasValue ? _timeZoneConverter.ToUtc(model.MatchDate.Value.Add(model.MatchTime.Value)) : null, _siteContext.FixtureRuleSet.PlannedDurationOfMatch); match.SetVenueId(model.VenueId); if (match.IsDirty) { match.ChangeSerial += 1; } ModelState.Clear(); // Todo: This business logic should rather go into settings _siteContext.FixtureRuleSet.PlannedMatchTimeMustStayInCurrentLegBoundaries = model.Tournament.IsPlanningMode; if (!await model.ValidateAsync( new FixtureValidator(match, (_siteContext, _timeZoneConverter, model.PlannedMatch), DateTime.UtcNow), ModelState)) { return(View(ViewNames.Match.EditFixture, await AddDisplayDataToEditFixtureViewModel(model, cancellationToken))); } var fixtureIsChanged = match.IsDirty; var fixtureMessage = new EditFixtureViewModel.FixtureMessage { MatchId = model.Id, ChangeSuccess = false }; // save the match entity try { fixtureMessage.ChangeSuccess = await _appDb.GenericRepository.SaveEntityAsync(match, false, false, cancellationToken); _logger.LogInformation($"Fixture for match id {match.Id} updated successfully for user ID '{0}'", GetCurrentUserId()); if (fixtureIsChanged) { SendFixtureNotification(match.Id); } } catch (Exception e) { fixtureMessage.ChangeSuccess = false; _logger.LogCritical(e, "Fixture update for match id {0} failed for user ID '{1}'", match.Id, GetCurrentUserId()); } // redirect to fixture overview, where success message is shown TempData.Put <EditFixtureViewModel.FixtureMessage>(nameof(EditFixtureViewModel.FixtureMessage), fixtureMessage); return(RedirectToAction(nameof(Fixtures), nameof(Match), new { Organization = _siteContext.UrlSegmentValue })); }
/// <summary> /// Generate available match dates for teams where /// <see cref="TeamEntity.MatchDayOfWeek"/>, <see cref="TeamEntity.MatchTime"/>, <see cref="TeamEntity.VenueId"/> /// are not <see langword="null"/>. /// </summary> /// <param name="round"></param> /// <param name="cancellationToken"></param> /// <returns></returns> internal async Task GenerateNewAsync(RoundEntity round, CancellationToken cancellationToken) { await Initialize(cancellationToken); var teamIdProcessed = new List <long>(); var listTeamsWithSameVenue = new List <EntityCollection <TeamEntity> >(); // Make a list of teams of the same round and with the same venue AND weekday AND match time // Venues will later be assigned to these teams alternately foreach (var team in round.TeamCollectionViaTeamInRound) { // the collection will contain at least one team var teams = GetTeamsWithSameVenueAndMatchTime(team, round); if (teamIdProcessed.Contains(teams[0].Id)) { continue; } listTeamsWithSameVenue.Add(teams); foreach (var t in teams) { if (!teamIdProcessed.Contains(t.Id)) { teamIdProcessed.Add(t.Id); } } } foreach (var roundLeg in round.RoundLegs) { var startDate = DateTime.SpecifyKind(roundLeg.StartDateTime, DateTimeKind.Utc); var endDate = DateTime.SpecifyKind(roundLeg.EndDateTime, DateTimeKind.Utc); foreach (var teamsWithSameVenue in listTeamsWithSameVenue) { var teamIndex = 0; // Make sure these values are not null if (!teamsWithSameVenue[teamIndex].MatchDayOfWeek.HasValue || !teamsWithSameVenue[teamIndex].MatchTime.HasValue || !teamsWithSameVenue[teamIndex].VenueId.HasValue) { continue; } // Create Tuple for non-nullable context var team = (Id : teamsWithSameVenue[teamIndex].Id, MatchDayOfWeek : (DayOfWeek)teamsWithSameVenue[teamIndex].MatchDayOfWeek !.Value, MatchTime : teamsWithSameVenue[teamIndex].MatchTime !.Value, VenueId : teamsWithSameVenue[teamIndex].VenueId !.Value); // get the first possible match date equal or after the leg's starting date var matchDate = IncrementDateUntilDayOfWeek(startDate, team.MatchDayOfWeek); // process the period of a leg while (matchDate <= endDate) { // if there is more than one team per venue with same weekday and match time, // match dates will be assigned alternately var matchDateAndTimeUtc = _timeZoneConverter.ToUtc(matchDate.Date.Add(team.MatchTime)); // check whether the calculated date // is within the borders of round legs (if any) and is not marked as excluded if (IsDateWithinRoundLegDateTime(roundLeg, matchDateAndTimeUtc) && !IsExcludedDate(matchDateAndTimeUtc, round.Id, team.Id) && !await IsVenueOccupiedByMatchAsync( new DateTimePeriod(matchDateAndTimeUtc, matchDateAndTimeUtc.Add(_tenantContext.TournamentContext.FixtureRuleSet .PlannedDurationOfMatch)), team.VenueId, cancellationToken)) { var av = new AvailableMatchDateEntity { TournamentId = _tenantContext.TournamentContext.MatchPlanTournamentId, HomeTeamId = team.Id, VenueId = team.VenueId, MatchStartTime = matchDateAndTimeUtc, MatchEndTime = matchDateAndTimeUtc.Add(_tenantContext.TournamentContext.FixtureRuleSet.PlannedDurationOfMatch), IsGenerated = true }; _generatedAvailableMatchDateEntities.Add(av); teamIndex = ++teamIndex >= teamsWithSameVenue.Count ? 0 : teamIndex; } matchDate = matchDate.Date.AddDays(7); } } } _logger.LogTrace("Generated {Count} UTC dates for HomeTeams:", _generatedAvailableMatchDateEntities.Count); _logger.LogTrace("{Generated}\n", _generatedAvailableMatchDateEntities.Select(gen => (gen.HomeTeamId, gen.MatchStartTime))); // save to the persistent storage // await _appDb.GenericRepository.SaveEntitiesAsync(_generatedAvailableMatchDateEntities, true, false, cancellationToken); }