diff --git a/DigitalLearningSolutions.Web.Tests/ControllerHelpers/DateValidatorTests.cs b/DigitalLearningSolutions.Web.Tests/ControllerHelpers/DateValidatorTests.cs index 2de5ff7e84..a39fb49151 100644 --- a/DigitalLearningSolutions.Web.Tests/ControllerHelpers/DateValidatorTests.cs +++ b/DigitalLearningSolutions.Web.Tests/ControllerHelpers/DateValidatorTests.cs @@ -1,149 +1,237 @@ -namespace DigitalLearningSolutions.Web.Tests.ControllerHelpers -{ - using DigitalLearningSolutions.Web.ControllerHelpers; - using FluentAssertions; - using NUnit.Framework; - - public class DateValidatorTests - { - [Test] - public void Date_validator_returns_valid_for_valid_date() - { - // When - var result = DateValidator.ValidateDate(1, 1, 3020); - - // Then - result.DateValid.Should().BeTrue(); - result.DayValid.Should().BeTrue(); - result.MonthValid.Should().BeTrue(); - result.YearValid.Should().BeTrue(); - } - - [Test] - public void Date_validator_returns_valid_for_empty_date() - { - // When - var result = DateValidator.ValidateDate(0, 0, 0); - - // Then - result.DateValid.Should().BeTrue(); - result.DayValid.Should().BeTrue(); - result.MonthValid.Should().BeTrue(); - result.YearValid.Should().BeTrue(); - } - - [Test] - public void Date_validator_returns_invalid_for_a_date_with_day_missing() - { - // When - var result = DateValidator.ValidateDate(0, 1, 2020); - - // Then - result.DateValid.Should().BeFalse(); - result.DayValid.Should().BeFalse(); - result.ErrorMessage.Should().Be("Complete by date must include a day"); - } - - [Test] - public void Date_validator_returns_invalid_for_a_date_with_day_and_month_missing() - { - // When - var result = DateValidator.ValidateDate(0, 0, 2020); - - // Then - result.DateValid.Should().BeFalse(); - result.DayValid.Should().BeFalse(); - result.ErrorMessage.Should().Be("Complete by date must include a day and month"); - } - - [Test] - public void Date_validator_returns_invalid_for_a_date_with_day_and_year_missing() - { - // When - var result = DateValidator.ValidateDate(0, 1, 0); - - // Then - result.DateValid.Should().BeFalse(); - result.DayValid.Should().BeFalse(); - result.MonthValid.Should().BeFalse(); - result.YearValid.Should().BeFalse(); - result.ErrorMessage.Should().Be("Complete by date must include a day and year"); - } - - [Test] - public void Date_validator_returns_invalid_for_a_date_with_month_missing() - { - // When - var result = DateValidator.ValidateDate(1, 0, 2020); - - // Then - result.DateValid.Should().BeFalse(); - result.MonthValid.Should().BeFalse(); - result.ErrorMessage.Should().Be("Complete by date must include a month"); - } - - [Test] - public void Date_validator_returns_invalid_for_a_date_with_month_and_year_missing() - { - // When - var result = DateValidator.ValidateDate(1, 0, 0); - - // Then - result.DateValid.Should().BeFalse(); - result.DayValid.Should().BeFalse(); - result.MonthValid.Should().BeFalse(); - result.YearValid.Should().BeFalse(); - result.ErrorMessage.Should().Be("Complete by date must include a month and year"); - } - - [Test] - public void Date_validator_returns_invalid_for_a_date_with_bad_day() - { - // When - var result = DateValidator.ValidateDate(32, 1, 2020); - - // Then - result.DateValid.Should().BeFalse(); - result.DayValid.Should().BeFalse(); - result.ErrorMessage.Should().Be("Complete by date must be a real date"); - } - - [Test] - public void Date_validator_returns_invalid_for_a_date_with_bad_month() - { - // When - var result = DateValidator.ValidateDate(31, 13, 2020); - - // Then - result.DateValid.Should().BeFalse(); - result.MonthValid.Should().BeFalse(); - result.ErrorMessage.Should().Be("Complete by date must be a real date"); - } - - [Test] - public void Date_validator_returns_invalid_for_a_date_with_bad_year() - { - // When - var result = DateValidator.ValidateDate(31, 1, 20201); - - // Then - result.DateValid.Should().BeFalse(); - result.YearValid.Should().BeFalse(); - result.ErrorMessage.Should().Be("Complete by date must be a real date"); - } - - [Test] - public void Date_validator_returns_invalid_for_a_date_with_bad_date() - { - // When - var result = DateValidator.ValidateDate(31, 2, 2020); - - // Then - result.DateValid.Should().BeFalse(); - result.DayValid.Should().BeFalse(); - result.MonthValid.Should().BeFalse(); - result.YearValid.Should().BeFalse(); - result.ErrorMessage.Should().Be("Complete by date must be a real date"); - } - } -} +namespace DigitalLearningSolutions.Web.Tests.ControllerHelpers +{ + using System.Linq; + using DigitalLearningSolutions.Web.Helpers; + using FluentAssertions; + using NUnit.Framework; + + public class DateValidatorTests + { + [Test] + public void ValidateDate_returns_valid_for_valid_date() + { + // When + var result = DateValidator.ValidateDate(1, 1, 3000); + + // Then + result.HasDayError.Should().BeFalse(); + result.HasMonthError.Should().BeFalse(); + result.HasYearError.Should().BeFalse(); + result.ErrorMessage.Should().BeNull(); + } + + [Test] + public void ValidateDate_returns_valid_for_empty_non_required_date() + { + // When + var result = DateValidator.ValidateDate(null, null, null, required: false); + + // Then + result.HasDayError.Should().BeFalse(); + result.HasMonthError.Should().BeFalse(); + result.HasYearError.Should().BeFalse(); + result.ErrorMessage.Should().BeNull(); + } + + [Test] + public void ValidateDate_returns_appropriate_error_for_empty_required_date() + { + // When + var result = DateValidator.ValidateDate(null, null, null, required: true); + + // Then + result.HasDayError.Should().BeTrue(); + result.HasMonthError.Should().BeTrue(); + result.HasYearError.Should().BeTrue(); + result.ErrorMessage.Should().Be("Date is required"); + } + + [TestCase(null, 1, 3000, "a day")] + [TestCase(1, null, 3000, "a month")] + [TestCase(1, 1, null, "a year")] + [TestCase(null, null, 3000, "a day and a month")] + [TestCase(null, 1, null, "a day and a year")] + [TestCase(1, null, null, "a month and a year")] + public void ValidateDate_returns_appropriate_error_if_some_values_missing( + int? day, + int? month, + int? year, + string errorMessageEnding + ) + { + // When + var result = DateValidator.ValidateDate(day, month, year); + + // Then + result.HasDayError.Should().Be(!day.HasValue); + result.HasMonthError.Should().Be(!month.HasValue); + result.HasYearError.Should().Be(!year.HasValue); + result.ErrorMessage.Should().Be("Date must include " + errorMessageEnding); + } + + [Test] + public void ValidateDate_returns_appropriate_error_if_all_values_invalid() + { + // When + var result = DateValidator.ValidateDate(0, 0, 0, required: true); + + // Then + result.HasDayError.Should().BeTrue(); + result.HasMonthError.Should().BeTrue(); + result.HasYearError.Should().BeTrue(); + result.ErrorMessage.Should().Be("Date must be a real date"); + } + + [TestCase(0, 1, 3000, true, false, false, "day")] + [TestCase(32, 1, 3000, true, false, false, "day")] + [TestCase(1, 0, 3000, false, true, false, "month")] + [TestCase(1, 13, 3000, false, true, false, "month")] + [TestCase(1, 1, 0, false, false, true, "year")] + [TestCase(1, 1, 10000, false, false, true, "year")] + [TestCase(0, 0, 3000, true, true, false, "day and month")] + [TestCase(0, 1, 0, true, false, true, "day and year")] + [TestCase(1, 0, 0, false, true, true, "month and year")] + public void ValidateDate_returns_appropriate_error_if_some_values_invalid( + int day, + int month, + int year, + bool dayError, + bool monthError, + bool yearError, + string errorMessageEnding + ) + { + // When + var result = DateValidator.ValidateDate(day, month, year); + + // Then + result.HasDayError.Should().Be(dayError); + result.HasMonthError.Should().Be(monthError); + result.HasYearError.Should().Be(yearError); + result.ErrorMessage.Should().Be("Date must include a real " + errorMessageEnding); + } + + [Test] + public void ValidateDate_returns_appropriate_error_if_date_not_real() + { + // When + var result = DateValidator.ValidateDate(30, 2, 3000); + + // Then + result.HasDayError.Should().BeTrue(); + result.HasMonthError.Should().BeTrue(); + result.HasYearError.Should().BeTrue(); + result.ErrorMessage.Should().Be("Date must be a real date"); + } + + [Test] + public void ValidateDate_returns_appropriate_error_if_date_not_in_future() + { + // When + var result = DateValidator.ValidateDate(1, 1, 2000); + + // Then + result.HasDayError.Should().BeTrue(); + result.HasMonthError.Should().BeTrue(); + result.HasYearError.Should().BeTrue(); + result.ErrorMessage.Should().Be("Date must be in the future"); + } + + [Test] + public void ValidateDate_uses_name_correctly_in_error_message() + { + // When + var result = DateValidator.ValidateDate(null, null, null, "What's required", true); + + // Then + result.HasDayError.Should().BeTrue(); + result.HasMonthError.Should().BeTrue(); + result.HasYearError.Should().BeTrue(); + result.ErrorMessage.Should().Be("What's required is required"); + } + + [TestCase(true, false, false, "Day")] + [TestCase(false, true, false, "Month")] + [TestCase(false, false, true, "Year")] + [TestCase(true, true, false, "Day", "Month")] + [TestCase(true, false, true, "Day", "Year")] + [TestCase(false, true, true, "Month", "Year")] + [TestCase(true, true, true, "Day", "Month", "Year")] + public void ToValidationResultList_includes_one_error_for_each_erroneous_part_of_date( + bool hasDayError, + bool hasMonthError, + bool hasYearError, + params string[] errorMemberNames + ) + { + // Given + var dateValidationResult = new DateValidator.DateValidationResult( + hasDayError, + hasMonthError, + hasYearError, + "msg" + ); + + // When + var result = dateValidationResult.ToValidationResultList("Day", "Month", "Year"); + + // Then + result.Should().HaveCount(errorMemberNames.Length); + foreach (var memberName in errorMemberNames) + { + result.Should().Contain( + validationResult => + validationResult.MemberNames.Count() == 1 && validationResult.MemberNames.Contains(memberName) + ); + } + } + + [Test] + public void ToValidationResultList_includes_errors_in_appropriate_order() + { + // Given + var dateValidationResult = new DateValidator.DateValidationResult(true, true, true, "msg"); + + // When + var result = dateValidationResult.ToValidationResultList("Day", "Month", "Year"); + + // Then + result[0].MemberNames.Should().Contain("Day"); + result[1].MemberNames.Should().Contain("Month"); + result[2].MemberNames.Should().Contain("Year"); + } + + [Test] + public void ToValidationResultList_only_includes_message_for_first_erroneous_part_of_date() + { + // Given + const string errorMessage = "msg"; + var dateValidationResult = new DateValidator.DateValidationResult(false, true, true, errorMessage); + + // When + var result = dateValidationResult.ToValidationResultList("Day", "Month", "Year"); + + // Then + result[0].ErrorMessage.Should().Be(errorMessage); + result[1].ErrorMessage.Should().Be(string.Empty); + } + + [Test] + public void ToValidationResultList_uses_correct_member_names_for_errors() + { + // Given + var dateValidationResult = new DateValidator.DateValidationResult(true, true, true, "msg"); + const string dayId = "TheDay"; + const string monthId = "TheMonth"; + const string yearId = "TheYear"; + + // When + var result = dateValidationResult.ToValidationResultList(dayId, monthId, yearId); + + // Then + result[0].MemberNames.Should().Contain(dayId); + result[1].MemberNames.Should().Contain(monthId); + result[2].MemberNames.Should().Contain(yearId); + } + } +} diff --git a/DigitalLearningSolutions.Web.Tests/ControllerHelpers/OldDateValidatorTests.cs b/DigitalLearningSolutions.Web.Tests/ControllerHelpers/OldDateValidatorTests.cs new file mode 100644 index 0000000000..cd31ccf4f3 --- /dev/null +++ b/DigitalLearningSolutions.Web.Tests/ControllerHelpers/OldDateValidatorTests.cs @@ -0,0 +1,149 @@ +namespace DigitalLearningSolutions.Web.Tests.ControllerHelpers +{ + using DigitalLearningSolutions.Web.Helpers; + using FluentAssertions; + using NUnit.Framework; + + public class OldDateValidatorTests + { + [Test] + public void Date_validator_returns_valid_for_valid_date() + { + // When + var result = OldDateValidator.ValidateDate(1, 1, 3020); + + // Then + result.DateValid.Should().BeTrue(); + result.DayValid.Should().BeTrue(); + result.MonthValid.Should().BeTrue(); + result.YearValid.Should().BeTrue(); + } + + [Test] + public void Date_validator_returns_valid_for_empty_date() + { + // When + var result = OldDateValidator.ValidateDate(0, 0, 0); + + // Then + result.DateValid.Should().BeTrue(); + result.DayValid.Should().BeTrue(); + result.MonthValid.Should().BeTrue(); + result.YearValid.Should().BeTrue(); + } + + [Test] + public void Date_validator_returns_invalid_for_a_date_with_day_missing() + { + // When + var result = OldDateValidator.ValidateDate(0, 1, 2020); + + // Then + result.DateValid.Should().BeFalse(); + result.DayValid.Should().BeFalse(); + result.ErrorMessage.Should().Be("Complete by date must include a day"); + } + + [Test] + public void Date_validator_returns_invalid_for_a_date_with_day_and_month_missing() + { + // When + var result = OldDateValidator.ValidateDate(0, 0, 2020); + + // Then + result.DateValid.Should().BeFalse(); + result.DayValid.Should().BeFalse(); + result.ErrorMessage.Should().Be("Complete by date must include a day and month"); + } + + [Test] + public void Date_validator_returns_invalid_for_a_date_with_day_and_year_missing() + { + // When + var result = OldDateValidator.ValidateDate(0, 1, 0); + + // Then + result.DateValid.Should().BeFalse(); + result.DayValid.Should().BeFalse(); + result.MonthValid.Should().BeFalse(); + result.YearValid.Should().BeFalse(); + result.ErrorMessage.Should().Be("Complete by date must include a day and year"); + } + + [Test] + public void Date_validator_returns_invalid_for_a_date_with_month_missing() + { + // When + var result = OldDateValidator.ValidateDate(1, 0, 2020); + + // Then + result.DateValid.Should().BeFalse(); + result.MonthValid.Should().BeFalse(); + result.ErrorMessage.Should().Be("Complete by date must include a month"); + } + + [Test] + public void Date_validator_returns_invalid_for_a_date_with_month_and_year_missing() + { + // When + var result = OldDateValidator.ValidateDate(1, 0, 0); + + // Then + result.DateValid.Should().BeFalse(); + result.DayValid.Should().BeFalse(); + result.MonthValid.Should().BeFalse(); + result.YearValid.Should().BeFalse(); + result.ErrorMessage.Should().Be("Complete by date must include a month and year"); + } + + [Test] + public void Date_validator_returns_invalid_for_a_date_with_bad_day() + { + // When + var result = OldDateValidator.ValidateDate(32, 1, 2020); + + // Then + result.DateValid.Should().BeFalse(); + result.DayValid.Should().BeFalse(); + result.ErrorMessage.Should().Be("Complete by date must be a real date"); + } + + [Test] + public void Date_validator_returns_invalid_for_a_date_with_bad_month() + { + // When + var result = OldDateValidator.ValidateDate(31, 13, 2020); + + // Then + result.DateValid.Should().BeFalse(); + result.MonthValid.Should().BeFalse(); + result.ErrorMessage.Should().Be("Complete by date must be a real date"); + } + + [Test] + public void Date_validator_returns_invalid_for_a_date_with_bad_year() + { + // When + var result = OldDateValidator.ValidateDate(31, 1, 20201); + + // Then + result.DateValid.Should().BeFalse(); + result.YearValid.Should().BeFalse(); + result.ErrorMessage.Should().Be("Complete by date must be a real date"); + } + + [Test] + public void Date_validator_returns_invalid_for_a_date_with_bad_date() + { + // When + var result = OldDateValidator.ValidateDate(31, 2, 2020); + + // Then + result.DateValid.Should().BeFalse(); + result.DayValid.Should().BeFalse(); + result.MonthValid.Should().BeFalse(); + result.YearValid.Should().BeFalse(); + result.ErrorMessage.Should().Be("Complete by date must be a real date"); + } + } +} diff --git a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Current.cs b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Current.cs index a1dfcecfe9..7dd2ca8a70 100644 --- a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Current.cs +++ b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Current.cs @@ -2,7 +2,6 @@ { using System; using System.Linq; - using DigitalLearningSolutions.Web.ControllerHelpers; using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; using DigitalLearningSolutions.Web.ViewModels.LearningPortal.Current; @@ -56,7 +55,7 @@ public IActionResult SetCurrentCourseCompleteByDate(int id, int day, int month, return RedirectToAction("Current"); } - var validationResult = DateValidator.ValidateDate(day, month, year); + var validationResult = OldDateValidator.ValidateDate(day, month, year); if (!validationResult.DateValid) { return RedirectToAction("SetCurrentCourseCompleteByDate", new { id, day, month, year }); @@ -92,7 +91,7 @@ public IActionResult SetCurrentCourseCompleteByDate(int id, int? day, int? month if (day != null && month != null && year != null) { - model.CompleteByValidationResult = DateValidator.ValidateDate(day.Value, month.Value, year.Value); + model.CompleteByValidationResult = OldDateValidator.ValidateDate(day.Value, month.Value, year.Value); } return View("Current/SetCompleteByDate", model); diff --git a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs index 8f3b61b0e1..4267fdefc4 100644 --- a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs +++ b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using DigitalLearningSolutions.Data.Models.SelfAssessments; - using DigitalLearningSolutions.Web.ControllerHelpers; using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.ViewModels.LearningPortal.SelfAssessments; using Microsoft.AspNetCore.Mvc; @@ -140,7 +139,7 @@ public IActionResult SetSelfAssessmentCompleteByDate(int selfAssessmentId, int d return RedirectToAction("Current"); } - var validationResult = DateValidator.ValidateDate(day, month, year); + var validationResult = OldDateValidator.ValidateDate(day, month, year); if (!validationResult.DateValid) { return RedirectToAction("SetSelfAssessmentCompleteByDate", new { selfAssessmentId, day, month, year }); @@ -165,7 +164,7 @@ public IActionResult SetSelfAssessmentCompleteByDate(int selfAssessmentId, int? if (day != null && month != null && year != null) { - model.CompleteByValidationResult = DateValidator.ValidateDate(day.Value, month.Value, year.Value); + model.CompleteByValidationResult = OldDateValidator.ValidateDate(day.Value, month.Value, year.Value); } return View("Current/SetCompleteByDate", model); diff --git a/DigitalLearningSolutions.Web/Controllers/Register/RegisterDelegateByCentreController.cs b/DigitalLearningSolutions.Web/Controllers/Register/RegisterDelegateByCentreController.cs index 947ebadb4b..4e9ff0fa3e 100644 --- a/DigitalLearningSolutions.Web/Controllers/Register/RegisterDelegateByCentreController.cs +++ b/DigitalLearningSolutions.Web/Controllers/Register/RegisterDelegateByCentreController.cs @@ -5,7 +5,6 @@ namespace DigitalLearningSolutions.Web.Controllers.Register using System.Linq; using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Services; - using DigitalLearningSolutions.Web.ControllerHelpers; using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.Models; @@ -155,8 +154,8 @@ public IActionResult WelcomeEmail(WelcomeEmailViewModel model) var data = TempData.Peek()!; model.ClearDateIfNotSendEmail(); - SetWelcomeEmailValidationResult(model); - if (model.DateValidationResult is { DateValid: false }) + + if (!ModelState.IsValid) { return View(model); } @@ -298,22 +297,6 @@ private void ValidatePersonalInformation(PersonalInformationViewModel model) } } - private void SetWelcomeEmailValidationResult(WelcomeEmailViewModel model) - { - if (!model.ShouldSendEmail) - { - return; - } - - var validationResult = DateValidator.ValidateRequiredDate( - model.Day, - model.Month, - model.Year, - "Email delivery date" - ); - model.DateValidationResult = validationResult; - } - private IEnumerable GetEditCustomFieldsFromModel( LearnerInformationViewModel model, int centreId diff --git a/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs b/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs index 0a11f349ab..2b90575add 100644 --- a/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs +++ b/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs @@ -14,7 +14,6 @@ using System.Collections.Generic; using DigitalLearningSolutions.Web.ViewModels.Common.SearchablePage; using DigitalLearningSolutions.Data.Models.SessionData.Supervisor; - using DigitalLearningSolutions.Web.ControllerHelpers; public partial class SupervisorController { @@ -302,7 +301,7 @@ public IActionResult EnrolDelegateCompleteBy(int supervisorDelegateId, int? day, }; if (day != null && month != null && year != null) { - model.CompleteByValidationResult = DateValidator.ValidateDate(day.Value, month.Value, year.Value); + model.CompleteByValidationResult = OldDateValidator.ValidateDate(day.Value, month.Value, year.Value); } return View("EnrolDelegateSetCompleteBy", model); } @@ -313,7 +312,7 @@ public IActionResult EnrolDelegateSetCompleteBy(int supervisorDelegateId, int da SessionEnrolOnRoleProfile sessionEnrolOnRoleProfile = TempData.Peek(); if (day != 0 | month != 0 | year != 0) { - var validationResult = DateValidator.ValidateDate(day, month, year); + var validationResult = OldDateValidator.ValidateDate(day, month, year); if (!validationResult.DateValid) { return RedirectToAction("EnrolDelegateCompleteBy", new { supervisorDelegateId, day, month, year }); diff --git a/DigitalLearningSolutions.Web/Helpers/DateValidator.cs b/DigitalLearningSolutions.Web/Helpers/DateValidator.cs new file mode 100644 index 0000000000..eabc083095 --- /dev/null +++ b/DigitalLearningSolutions.Web/Helpers/DateValidator.cs @@ -0,0 +1,173 @@ +namespace DigitalLearningSolutions.Web.Helpers +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + + public static class DateValidator + { + public static DateValidationResult ValidateDate( + int? day, + int? month, + int? year, + string name = "Date", + bool required = false + ) + { + if (!day.HasValue && !month.HasValue && !year.HasValue) + { + return required ? new DateValidationResult(name + " is required") : new DateValidationResult(); + } + + if (day.HasValue && month.HasValue && year.HasValue) + { + return ValidateDate(day.Value, month.Value, year.Value, name); + } + + var errorMessage = GetMissingValuesErrorMessage(day, month, year, name); + return new DateValidationResult(!day.HasValue, !month.HasValue, !year.HasValue, errorMessage); + } + + private static DateValidationResult ValidateDate(int day, int month, int year, string name) + { + // note: the minimum year the DB can store is 1753 + var invalidDay = day < 1 || day > 31; + var invalidMonth = month < 1 || month > 12; + var invalidYear = year < 1 || year > 9999; + + if (invalidDay || invalidMonth || invalidYear) + { + var errorMessage = GetInvalidValuesErrorMessage(invalidDay, invalidMonth, invalidYear, name); + return new DateValidationResult(invalidDay, invalidMonth, invalidYear, errorMessage); + } + + try + { + var date = new DateTime(year, month, day); + if (date <= DateTime.Today) + { + return new DateValidationResult(name + " must be in the future"); + } + } + catch (ArgumentOutOfRangeException) + { + return new DateValidationResult(name + " must be a real date"); + } + + return new DateValidationResult(); + } + + private static string GetMissingValuesErrorMessage(int? day, int? month, int? year, string name) + { + var missingValues = new List(); + + if (!day.HasValue) + { + missingValues.Add("day"); + } + + if (!month.HasValue) + { + missingValues.Add("month"); + } + + if (!year.HasValue) + { + missingValues.Add("year"); + } + + return name + " must include a " + string.Join(" and a ", missingValues); + } + + private static string GetInvalidValuesErrorMessage( + bool invalidDay, + bool invalidMonth, + bool invalidYear, + string name + ) + { + var invalidValues = new List(); + + if (invalidDay) + { + invalidValues.Add("day"); + } + + if (invalidMonth) + { + invalidValues.Add("month"); + } + + if (invalidYear) + { + invalidValues.Add("year"); + } + + if (invalidValues.Count == 3) + { + return name + " must be a real date"; + } + + return name + " must include a real " + string.Join(" and ", invalidValues); + } + + public class DateValidationResult + { + public readonly string? ErrorMessage; + public readonly bool HasDayError; + public readonly bool HasMonthError; + public readonly bool HasYearError; + + public DateValidationResult() { } + + public DateValidationResult(string errorMessage) + { + HasDayError = true; + HasMonthError = true; + HasYearError = true; + ErrorMessage = errorMessage; + } + + public DateValidationResult(bool hasDayError, bool hasMonthError, bool hasYearError, string? errorMessage) + { + HasDayError = hasDayError; + HasMonthError = hasMonthError; + HasYearError = hasYearError; + ErrorMessage = errorMessage; + } + + public List ToValidationResultList( + string dayId, + string monthId, + string yearId + ) + { + var results = new List(); + var errorMessageAdded = false; + + if (HasDayError) + { + results.Add(new ValidationResult(ErrorMessage, new[] { dayId })); + errorMessageAdded = true; + } + + if (HasMonthError) + { + // Should only add error message once per date to avoid duplicates in error summary component, but still highlight all inputs with errors + var errorMessage = !errorMessageAdded ? ErrorMessage : ""; + results.Add(new ValidationResult(errorMessage, new[] { monthId })); + errorMessageAdded = true; + } + + if (HasYearError) + { + // Should only add error message once per date to avoid duplicates in error summary component, but still highlight all inputs with errors + var errorMessage = !errorMessageAdded ? ErrorMessage : ""; + results.Add(new ValidationResult(errorMessage, new[] { yearId })); + } + + return results; + } + } + } +} diff --git a/DigitalLearningSolutions.Web/ControllerHelpers/DateValidator.cs b/DigitalLearningSolutions.Web/Helpers/OldDateValidator.cs similarity index 78% rename from DigitalLearningSolutions.Web/ControllerHelpers/DateValidator.cs rename to DigitalLearningSolutions.Web/Helpers/OldDateValidator.cs index 81c0df903a..7feb1a5813 100644 --- a/DigitalLearningSolutions.Web/ControllerHelpers/DateValidator.cs +++ b/DigitalLearningSolutions.Web/Helpers/OldDateValidator.cs @@ -1,172 +1,146 @@ -namespace DigitalLearningSolutions.Web.ControllerHelpers -{ - using System; - using System.Collections.Generic; - using System.Linq; - - public static class DateValidator - { - public class ValidationResult - { - public ValidationResult() - { - DateValid = true; - ErrorMessage = ""; - DayValid = true; - MonthValid = true; - YearValid = true; - } - - public ValidationResult(int day, int month, int year) - { - DateValid = true; - ErrorMessage = ""; - DayValid = true; - MonthValid = true; - YearValid = true; - Day = day; - Month = month; - Year = year; - } - - public bool DateValid { get; set; } - public string ErrorMessage { get; set; } - public bool DayValid { get; set; } - public bool MonthValid { get; set; } - public bool YearValid { get; set; } - public int? Day { get; } - public int? Month { get; } - public int? Year { get; } - } - - public static ValidationResult ValidateRequiredDate(int? day, int? month, int? year, string name) - { - day ??= 0; - month ??= 0; - year ??= 0; - - try - { - var newDate = new DateTime(year.Value, month.Value, day.Value); - if (newDate <= DateTime.Today) - { - return new ValidationResult(day.Value, month.Value, year.Value) - { - DateValid = false, - DayValid = false, - MonthValid = false, - YearValid = false, - ErrorMessage = "Email delivery date must be in the future" - }; - } - - return new ValidationResult(day.Value, month.Value, year.Value); - } - catch (ArgumentOutOfRangeException) - { - return GetValidationError(day.Value, month.Value, year.Value, name); - } - } - - public static ValidationResult ValidateDate(int day, int month, int year) - { - if (day == 0 && month == 0 && year == 0) - { - return new ValidationResult(day, month, year); - } - - try - { - if (year < 1753) - { - // The minimum year the DB can store is 1753 - throw new ArgumentOutOfRangeException(); - } - - var newDate = new DateTime(year, month, day); - if (newDate <= DateTime.Today) - { - return new ValidationResult(day, month, year) - { - DateValid = false, - DayValid = false, - MonthValid = false, - YearValid = false, - ErrorMessage = "Complete by date must be in the future" - }; - } - return new ValidationResult(day, month, year); - } - catch (ArgumentOutOfRangeException) - { - return GetValidationError(day, month, year, "Complete by date"); - } - } - - private static ValidationResult GetValidationError(int day, int month, int year, string name) - { - var error = new ValidationResult(day, month, year) { DateValid = false }; - - error.DayValid = DayIsValid(day); - error.MonthValid = MonthIsValid(month); - error.YearValid = YearIsValid(year); - if ( - !error.DayValid && !error.MonthValid - || !error.DayValid && !error.YearValid - || !error.MonthValid && !error.YearValid - || error.DayValid && error.MonthValid && error.YearValid - ) - { - error.DayValid = false; - error.MonthValid = false; - error.YearValid = false; - } - - error.ErrorMessage = ConstructErrorMessage(day, month, year, name); - - return error; - } - - private static bool DayIsValid(int day) - { - return day > 0 && day < 32; - } - - private static bool MonthIsValid(int day) - { - return day > 0 && day < 13; - } - - private static bool YearIsValid(int year) - { - return year > 1752 && year < 10000; - } - - private static string ConstructErrorMessage(int day, int month, int year, string name) - { - List emptyElements = new List(); - - if (day == 0) - { - emptyElements.Add("day"); - } - - if (month == 0) - { - emptyElements.Add("month"); - } - - if (year == 0) - { - emptyElements.Add("year"); - } - - if (emptyElements.Any()) - { - return name + " must include a " + string.Join(" and ", emptyElements); - } - - return name + " must be a real date"; - } - } -} +namespace DigitalLearningSolutions.Web.Helpers +{ + using System; + using System.Collections.Generic; + using System.Linq; + + // This helper class is currently used in LearningPortal pages + // Switch over to the new DateValidator class in HEEDLS-560 + + public static class OldDateValidator + { + public class ValidationResult + { + public ValidationResult() + { + DateValid = true; + ErrorMessage = ""; + DayValid = true; + MonthValid = true; + YearValid = true; + } + + public ValidationResult(int day, int month, int year) + { + DateValid = true; + ErrorMessage = ""; + DayValid = true; + MonthValid = true; + YearValid = true; + Day = day; + Month = month; + Year = year; + } + + public bool DateValid { get; set; } + public string ErrorMessage { get; set; } + public bool DayValid { get; set; } + public bool MonthValid { get; set; } + public bool YearValid { get; set; } + public int? Day { get; } + public int? Month { get; } + public int? Year { get; } + } + + public static ValidationResult ValidateDate(int day, int month, int year) + { + if (day == 0 && month == 0 && year == 0) + { + return new ValidationResult(day, month, year); + } + + try + { + if (year < 1753) + { + // The minimum year the DB can store is 1753 + throw new ArgumentOutOfRangeException(); + } + + var newDate = new DateTime(year, month, day); + if (newDate <= DateTime.Today) + { + return new ValidationResult(day, month, year) + { + DateValid = false, + DayValid = false, + MonthValid = false, + YearValid = false, + ErrorMessage = "Complete by date must be in the future" + }; + } + return new ValidationResult(day, month, year); + } + catch (ArgumentOutOfRangeException) + { + return GetValidationError(day, month, year, "Complete by date"); + } + } + + private static ValidationResult GetValidationError(int day, int month, int year, string name) + { + var error = new ValidationResult(day, month, year) { DateValid = false }; + + error.DayValid = DayIsValid(day); + error.MonthValid = MonthIsValid(month); + error.YearValid = YearIsValid(year); + if ( + !error.DayValid && !error.MonthValid + || !error.DayValid && !error.YearValid + || !error.MonthValid && !error.YearValid + || error.DayValid && error.MonthValid && error.YearValid + ) + { + error.DayValid = false; + error.MonthValid = false; + error.YearValid = false; + } + + error.ErrorMessage = ConstructErrorMessage(day, month, year, name); + + return error; + } + + private static bool DayIsValid(int day) + { + return day > 0 && day < 32; + } + + private static bool MonthIsValid(int day) + { + return day > 0 && day < 13; + } + + private static bool YearIsValid(int year) + { + return year > 1752 && year < 10000; + } + + private static string ConstructErrorMessage(int day, int month, int year, string name) + { + List emptyElements = new List(); + + if (day == 0) + { + emptyElements.Add("day"); + } + + if (month == 0) + { + emptyElements.Add("month"); + } + + if (year == 0) + { + emptyElements.Add("year"); + } + + if (emptyElements.Any()) + { + return name + " must include a " + string.Join(" and ", emptyElements); + } + + return name + " must be a real date"; + } + } +} diff --git a/DigitalLearningSolutions.Web/ViewComponents/DateInputViewComponent.cs b/DigitalLearningSolutions.Web/ViewComponents/DateInputViewComponent.cs index 462492afdc..d58589918b 100644 --- a/DigitalLearningSolutions.Web/ViewComponents/DateInputViewComponent.cs +++ b/DigitalLearningSolutions.Web/ViewComponents/DateInputViewComponent.cs @@ -1,20 +1,19 @@ namespace DigitalLearningSolutions.Web.ViewComponents { - using DigitalLearningSolutions.Web.ControllerHelpers; using DigitalLearningSolutions.Web.ViewModels.Common.ViewComponents; using Microsoft.AspNetCore.Mvc; public class DateInputViewComponent : ViewComponent { /// - /// Render DateInput view component. + /// Render DateInput view component. /// /// /// /// /// /// - /// DateValidator.ValidationResult + /// Leave blank for no custom css class. /// Leave blank for no hint. /// public IViewComponentResult Invoke( @@ -23,7 +22,7 @@ public IViewComponentResult Invoke( string dayId, string monthId, string yearId, - DateValidator.ValidationResult validationResult, + string cssClass, string hintText ) { @@ -35,6 +34,13 @@ string hintText var dayValue = dayProperty?.GetValue(model)?.ToString(); var monthValue = monthProperty?.GetValue(model)?.ToString(); var yearValue = yearProperty?.GetValue(model)?.ToString(); + var dayErrors = ViewData.ModelState[dayProperty?.Name]?.Errors; + var monthErrors = ViewData.ModelState[monthProperty?.Name]?.Errors; + var yearErrors = ViewData.ModelState[yearProperty?.Name]?.Errors; + + var errorMessage = dayErrors?.Count > 0 ? dayErrors[0].ErrorMessage : + monthErrors?.Count > 0 ? monthErrors[0].ErrorMessage : + yearErrors?.Count > 0 ? yearErrors[0].ErrorMessage : null; var viewModel = new DateInputViewModel( id, @@ -45,7 +51,11 @@ string hintText dayValue, monthValue, yearValue, - validationResult, + dayErrors?.Count > 0, + monthErrors?.Count > 0, + yearErrors?.Count > 0, + errorMessage, + string.IsNullOrEmpty(cssClass) ? null : cssClass, string.IsNullOrEmpty(hintText) ? null : hintText ); return View(viewModel); diff --git a/DigitalLearningSolutions.Web/ViewModels/Common/ViewComponents/DateInputViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Common/ViewComponents/DateInputViewModel.cs index 2e0ccde744..181515b2fc 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Common/ViewComponents/DateInputViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Common/ViewComponents/DateInputViewModel.cs @@ -1,7 +1,5 @@ namespace DigitalLearningSolutions.Web.ViewModels.Common.ViewComponents { - using DigitalLearningSolutions.Web.ControllerHelpers; - public class DateInputViewModel { public readonly bool HasDayError; @@ -11,29 +9,34 @@ public class DateInputViewModel public DateInputViewModel( string id, string label, - string? dayId, - string? monthId, - string? yearId, + string dayId, + string monthId, + string yearId, string? dayValue, string? monthValue, string? yearValue, - DateValidator.ValidationResult? validationResult, + bool hasDayError, + bool hasMonthError, + bool hasYearError, + string? errorMessage, + string? cssClass = null, string? hintText = null ) { Id = id; Label = label; - DayId = dayId ?? "Day"; - MonthId = monthId ?? "Month"; - YearId = yearId ?? "Year"; + DayId = dayId; + MonthId = monthId; + YearId = yearId; DayValue = dayValue; MonthValue = monthValue; YearValue = yearValue; + CssClass = cssClass; HintText = hintText; - ErrorMessage = validationResult?.ErrorMessage; - HasDayError = validationResult is { DayValid: false }; - HasMonthError = validationResult is { MonthValid: false }; - HasYearError = validationResult is { YearValid: false }; + HasDayError = hasDayError; + HasMonthError = hasMonthError; + HasYearError = hasYearError; + ErrorMessage = errorMessage; } public string Id { get; set; } @@ -44,6 +47,7 @@ public DateInputViewModel( public string? DayValue { get; set; } public string? MonthValue { get; set; } public string? YearValue { get; set; } + public string? CssClass { get; set; } public string? HintText { get; set; } public bool HasError => HasDayError || HasMonthError || HasYearError; public string? ErrorMessage { get; set; } diff --git a/DigitalLearningSolutions.Web/ViewModels/LearningPortal/Current/CurrentLearningItemViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/Current/CurrentLearningItemViewModel.cs index 3bbe660801..66fd24eed1 100644 --- a/DigitalLearningSolutions.Web/ViewModels/LearningPortal/Current/CurrentLearningItemViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/Current/CurrentLearningItemViewModel.cs @@ -2,12 +2,12 @@ { using System; using DigitalLearningSolutions.Data.Models; - using DigitalLearningSolutions.Web.ControllerHelpers; + using DigitalLearningSolutions.Web.Helpers; public class CurrentLearningItemViewModel : StartedLearningItemViewModel { public DateTime? CompleteByDate { get; } - public DateValidator.ValidationResult? CompleteByValidationResult { get; set; } + public OldDateValidator.ValidationResult? CompleteByValidationResult { get; set; } public CurrentLearningItemViewModel( CurrentLearningItem course diff --git a/DigitalLearningSolutions.Web/ViewModels/Register/RegisterDelegateByCentre/WelcomeEmailViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Register/RegisterDelegateByCentre/WelcomeEmailViewModel.cs index 39c183a020..2447ae15fc 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Register/RegisterDelegateByCentre/WelcomeEmailViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Register/RegisterDelegateByCentre/WelcomeEmailViewModel.cs @@ -1,9 +1,11 @@ namespace DigitalLearningSolutions.Web.ViewModels.Register.RegisterDelegateByCentre { - using DigitalLearningSolutions.Web.ControllerHelpers; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.Models; - public class WelcomeEmailViewModel + public class WelcomeEmailViewModel : IValidatableObject { public WelcomeEmailViewModel() { } @@ -22,7 +24,17 @@ public WelcomeEmailViewModel(DelegateRegistrationByCentreData data) public int? Month { get; set; } public int? Year { get; set; } public bool ShouldSendEmail { get; set; } - public DateValidator.ValidationResult? DateValidationResult { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (!ShouldSendEmail) + { + return new List(); + } + + return DateValidator.ValidateDate(Day, Month, Year, "Email delivery date", true) + .ToValidationResultList(nameof(Day), nameof(Month), nameof(Year)); + } public void ClearDateIfNotSendEmail() { diff --git a/DigitalLearningSolutions.Web/ViewModels/Supervisor/EnrolDelegateSetCompletByDateViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Supervisor/EnrolDelegateSetCompletByDateViewModel.cs index 2787f9c706..cac66da25f 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Supervisor/EnrolDelegateSetCompletByDateViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Supervisor/EnrolDelegateSetCompletByDateViewModel.cs @@ -2,13 +2,14 @@ { using DigitalLearningSolutions.Data.Models.RoleProfiles; using DigitalLearningSolutions.Data.Models.Supervisor; - using DigitalLearningSolutions.Web.ControllerHelpers; + using DigitalLearningSolutions.Web.Helpers; using System; + public class EnrolDelegateSetCompletByDateViewModel { public SupervisorDelegateDetail SupervisorDelegateDetail { get; set; } public RoleProfile RoleProfile { get; set; } public DateTime? CompleteByDate { get; set; } - public DateValidator.ValidationResult? CompleteByValidationResult { get; set; } + public OldDateValidator.ValidationResult? CompleteByValidationResult { get; set; } } } diff --git a/DigitalLearningSolutions.Web/Views/RegisterDelegateByCentre/WelcomeEmail.cshtml b/DigitalLearningSolutions.Web/Views/RegisterDelegateByCentre/WelcomeEmail.cshtml index 00e5cbaf2c..b95eb87fd5 100644 --- a/DigitalLearningSolutions.Web/Views/RegisterDelegateByCentre/WelcomeEmail.cshtml +++ b/DigitalLearningSolutions.Web/Views/RegisterDelegateByCentre/WelcomeEmail.cshtml @@ -2,13 +2,13 @@ @model WelcomeEmailViewModel @{ - var errorHasOccurred = Model.DateValidationResult is { DateValid: false }; + var errorHasOccurred = !ViewData.ModelState.IsValid; ViewData["Title"] = errorHasOccurred ? "Error: Register Delegate - Welcome Email" : "Register Delegate - Welcome Email"; const string dateInputId = "welcome-email-date"; - const string conditionalDateInputId = "conditional-welcome-email-date"; var exampleDate = DateTime.Today; - var emailDateCss = "nhsuk-checkboxes__conditional" + (Model.ShouldSendEmail ? "" : " nhsuk-checkboxes__conditional--hidden"); - var errorCss = errorHasOccurred ? "nhsuk-form-group--error nhsuk-u-padding-left-5" : ""; + var emailDateCss = "nhsuk-checkboxes__conditional" + + (Model.ShouldSendEmail ? "" : " nhsuk-checkboxes__conditional--hidden") + + (errorHasOccurred ? " nhsuk-u-padding-left-5" : ""); } @section NavMenuItems { @@ -18,18 +18,7 @@
@if (errorHasOccurred) { - + }

Welcome email

@@ -43,22 +32,20 @@ asp-for="ShouldSendEmail" type="checkbox" value="true" - aria-controls="@conditionalDateInputId" + aria-controls="@dateInputId" aria-expanded="@Model.ShouldSendEmail">
-
- -
+
diff --git a/DigitalLearningSolutions.Web/Views/Shared/Components/DateInput/Default.cshtml b/DigitalLearningSolutions.Web/Views/Shared/Components/DateInput/Default.cshtml index 03f39c5a34..25e78ca2a0 100644 --- a/DigitalLearningSolutions.Web/Views/Shared/Components/DateInput/Default.cshtml +++ b/DigitalLearningSolutions.Web/Views/Shared/Components/DateInput/Default.cshtml @@ -2,57 +2,66 @@ @model DateInputViewModel @{ + var errorCss = Model.HasError ? "nhsuk-form-group--error" : ""; var dayErrorCss = Model.HasDayError ? "nhsuk-input--error" : ""; var monthErrorCss = Model.HasMonthError ? "nhsuk-input--error" : ""; var yearErrorCss = Model.HasYearError ? "nhsuk-input--error" : ""; } -
- - @Model.Label - - @if (Model.HintText != null) { -
- @Model.HintText -
- } +
+
+ + @Model.Label + + @if (Model.HintText != null) { +
+ @Model.HintText +
+ } - @if (Model.HasError) { - - Error: @Model.ErrorMessage - - } + @if (Model.HasError) { + + Error: @Model.ErrorMessage + + } -
-
-
- - +
+
+
+ + +
-
-
-
- - +
+
+ + +
-
-
-
- - +
+
+ + +
-
-
+
+