Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

End date for 'shared governance professional - establishment' is now optional, per UAT #595

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ public async Task<ActionResult> SelectSharedGovernor(SelectSharedGovernorViewMod
{
newGovernor.Selected = previousGovernor.Selected;
}

// Retain the user-submitted selection (even if it is invalid):
if(newGovernor.Id.ToString() == model.SelectedGovernorId)
{
newGovernor.Selected = true;
}
}

model.Governors = governors;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Linq;
using Edubase.Services.Enums;
using Edubase.Web.UI.Validation;
using FluentValidation;
Expand All @@ -10,41 +9,93 @@ public class SelectSharedGovernorViewModelValidator : EdubaseAbstractValidator<S
{
public SelectSharedGovernorViewModelValidator()
{
// This section for "singular" governor roles (i.e., roles that can only have one appointee)
// On the web UI, these are displayed as radio buttons and the `Selected` value is ignored.
// Instead, the property `SelectedGovernorId` is used to track which governor is selected.
When(x => EnumSets.eSingularGovernorRoles.Contains(x.Role), () =>
{
RuleFor(x => x)
.Must(x => x.SelectedGovernorId != null && x.Governors.Count(g => g.Id == int.Parse(x.SelectedGovernorId)) == 1)
// Ensure the governor ID is selected and valid
RuleFor(x => x.SelectedGovernorId)
.Must(IsSelectedGovernorIdFound)
.WithMessage("Required")
.WithSummaryMessage("You must select a governor");

RuleFor(x => x)
.Must(x => x.SelectedGovernorId != null && x.Governors.Single(g => g.Id == int.Parse(x.SelectedGovernorId)).AppointmentStartDate.IsValid())
.WithMessage("Required")
.WithSummaryMessage("An appointment start date is required");

RuleFor(x => x)
.Must(x => x.SelectedGovernorId != null && x.Governors.Single(g => g.Id == int.Parse(x.SelectedGovernorId)).AppointmentEndDate.IsValid())
.WithMessage("Required")
.WithSummaryMessage("An appointment end date is required");
.WithSummaryMessage("Select a governor");
});

// This section for roles that can have multiple appointees.
// On the web UI, these are displayed as checkboxes and the `Selected` value for each governor
// is used to track which governors are selected (the `SelectedGovernorId` is ignored).
// In this case, validations need to be applied to all selected governors (not just a single "selected" governor).
When(x => !EnumSets.eSingularGovernorRoles.Contains(x.Role), () =>
{
RuleFor(x => x)
.Must(x => x.Governors.Any(g => g.Selected))
// Ensure at least one governor is selected (can be multiple, and at least one needs to be selected)
RuleFor(x => x.Governors)
.Must(x => x.Any(g => g.Selected))
.WithMessage("Required")
.WithSummaryMessage("You must select a governor");
.WithSummaryMessage("Select at least one governor");
});

RuleFor(x => x)
.Must(x => x.Governors.Where(g => g.Selected).All(g => g.AppointmentStartDate.IsValid()))
.WithMessage("Required")
.WithSummaryMessage("An appointment start date is required");

RuleFor(x => x)
.Must(x => x.Governors.Where(g => g.Selected).All(g => g.AppointmentEndDate.IsValid()))
.WithMessage("Required")
.WithSummaryMessage("An appointment end date is required");
});
// Custom validation for appointment end date.
// Using a custom rule is needed due to the hijinks/headaches with the `model.SelectedGovernorId`
// property versus `governor.Selected` property, and how the rules do not seem to share
// between the parent property in `RuleForEach` and the child property (governor) in `model.Governors`.
RuleFor(x => x)
.Custom((model, context) =>
{
// Due to the use of `SelectedGovernorId`, hijinks are required
// to attach validations to the selected governor with positional index,
// and have the error messages display next to the correct governor.
for (var index = 0; index < model.Governors.Count; index++)
{
var currentGovernor = model.Governors[index];
if (!IsGovernorSelected(currentGovernor, model.SelectedGovernorId))
{
// Skip governors that are not selected
continue;
}

// Validate appointment start date for all _shared_ establishment-side roles.
if (!currentGovernor.AppointmentStartDate.IsValid())
{
context.AddFailure(
$"Governors[{index}].{nameof(currentGovernor.AppointmentStartDate)}",
$"Enter a valid appointment start date for {currentGovernor.FullName} ({currentGovernor.Id})"
);
}

// Validate appointment end date roles, for all _shared_ establishment-side roles except `shared governance professional - establishment`.
// Note that end date is optional for this role, per #230911.
if (
model.Role != eLookupGovernorRole.Establishment_SharedGovernanceProfessional
&& !currentGovernor.AppointmentEndDate.IsValid()
)
{
context.AddFailure(
$"Governors[{index}].{nameof(currentGovernor.AppointmentEndDate)}",
$"Enter a valid appointment end date for {currentGovernor.FullName} ({currentGovernor.Id})"
);
}
}
});


}

private static bool IsGovernorSelected(SharedGovernorViewModel currentGovernor, string modelSelectedGovernorId)
{
// Note two ways to detect if current governor is selected:
// 1. "Singular" governor roles use `model.SelectedGovernorId` to track the selected governor.
// 2. "Multiple"/non-singular governor roles use `governor.Selected` to track the selected governor.

// Consider making this more robust by checking if the governor is selected based on the governor role
// (e.g., if the governor role is singular, then the governor is selected if the governor ID matches the selected governor ID).
return currentGovernor.Selected || currentGovernor.Id.ToString() == modelSelectedGovernorId;
}

private static bool IsSelectedGovernorIdFound(SelectSharedGovernorViewModel model, string selectedId)
{
return int.TryParse(selectedId, out var governorId)
&& model.Governors.SingleOrDefault(g => g.Id == governorId) != null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const GiasReOrderValidationSummary = function () {
const errorSummaryList = $errorSummary.find('ul');
const summaryMessageItems = $errorSummary.find('li');
const inPageErrorFields = $('#main-content').find('.govuk-form-group--error');

if (inPageErrorFields.length < 2 || window.blockReOrder) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
string tooltipUnderscoredTitle = null;
var describedByIds = "";

// Full field ID is required where there are multiple date fields on the same page.
// Previously, variations of `@legendPrefix + @ViewData.ModelMetadata.PropertyName`
// were used, but if there are multiple `StartDate` properties on the page then the
// IDs would clash. This is a more robust solution.
var fullFieldId = ViewData.TemplateInfo.GetFullHtmlFieldName(Model?.Label)
.Replace(".", "_")
.Replace("[", "_")
.Replace("]", "_");


if (ViewData["dateHint"] != null)
{
dateHint = ViewData["dateHint"].ToString();
Expand Down Expand Up @@ -48,7 +58,8 @@
else
{
classBinding = "class=\"govuk-form-group create-edit-form-group date-group "
+ Html.ValidationGroupCssClass(ViewData.ModelMetadata.PropertyName) + " "
// + Html.ValidationGroupCssClass(ViewData.ModelMetadata.PropertyName) + " "
+ Html.ValidationGroupCssClass(fullFieldId) + " "
+ Html.ValidationCssClassFor(x => x) + " "
+ Html.ValidationCssClassFor(x => x.Day) + " "
+ Html.ValidationCssClassFor(x => x.Month) + " "
Expand All @@ -72,19 +83,21 @@
tooltipText = ViewData["tooltipText"]?.ToString();
//}

var legendId = string.Concat(legendPrefix, fullFieldId);

}
<div @Html.Raw(classBinding)>

@{
describedByIds = describedByIds + " " + @legendPrefix + @ViewData.ModelMetadata.PropertyName + "-hint";
describedByIds = describedByIds + " " + legendId + "-hint";
}
@{
if (!disableHint)
{
describedByIds = describedByIds + " " + @ViewData.ModelMetadata.PropertyName + "-errors";
describedByIds = describedByIds + " " + @fullFieldId + "-errors";
}
}
<fieldset id="date-error" class="govuk-fieldset range-group @fieldsetClassName">
<fieldset id="@fullFieldId-date-error" class="govuk-fieldset range-group @fieldsetClassName">
@if (ViewData["inFilters"] != null)
{
<span class="govuk-error-message hidden">Please check the date(s) you have entered</span>
Expand All @@ -101,7 +114,7 @@
</div>
}

<legend class="govuk-fieldset__legend @Html.Conditional(hideLegend, "hidden")" id="@[email protected]">
<legend class="govuk-fieldset__legend @Html.Conditional(hideLegend, "hidden")" id="@fullFieldId">
@(string.IsNullOrWhiteSpace(ViewData["title"] as string) ? ViewData.ModelMetadata.DisplayName : ViewData["title"])
@if (!string.IsNullOrWhiteSpace(tooltipText))
{
Expand All @@ -113,12 +126,7 @@
</legend>

@{
var fullFieldId = ViewData.TemplateInfo.GetFullHtmlFieldName(Model?.Label)
.Replace(".", "_")
.Replace("[", "_")
.Replace("]", "_");

if (fullFieldId != string.Empty && !string.Concat(legendPrefix, ViewData.ModelMetadata.PropertyName).Equals(fullFieldId))
if (fullFieldId != string.Empty && !legendId.Equals(fullFieldId))
{
<div id="@fullFieldId"></div>
}
Expand All @@ -143,8 +151,8 @@
linkStateKey = linkStateKey.Replace(".Year", "_Year");
}

if (testStateKey.Equals(ViewData.ModelMetadata.PropertyName, StringComparison.InvariantCultureIgnoreCase) &&
!testStateKey.Equals(ViewData.ModelMetadata.PropertyName, StringComparison.InvariantCulture))
if (testStateKey.Equals(fullFieldId, StringComparison.InvariantCultureIgnoreCase) &&
!testStateKey.Equals(fullFieldId, StringComparison.InvariantCulture))
{
<div id="@linkStateKey"></div>
}
Expand All @@ -155,11 +163,11 @@
{
if (dateHint != string.Empty)
{
<p class="govuk-hint" id="@[email protected]-hint">For example, @dateHint</p>
<p class="govuk-hint" id="@legendId-hint">For example, @dateHint</p>
}
else
{
<p class="govuk-hint" id="@[email protected]-hint">For example, 20 03 2003</p>
<p class="govuk-hint" id="@legendId-hint">For example, 20 03 2003</p>
}
}

Expand All @@ -170,7 +178,7 @@
</div>
}

<span id="@ViewData.ModelMetadata.PropertyName-errors">
<span id="@legendId-errors">
@{
var errContent = new List<MvcHtmlString>
{
Expand All @@ -180,10 +188,10 @@
Html.ValidationMessageFor(x => x.Year, null, new {@class = "govuk-error-message"})
};

if (!Html.ViewData.ModelState.ContainsKey(ViewData.ModelMetadata.PropertyName) ||
!Html.ViewData.ModelState[ViewData.ModelMetadata.PropertyName].Errors.Any())
if (!Html.ViewData.ModelState.ContainsKey(fullFieldId) ||
!Html.ViewData.ModelState[fullFieldId].Errors.Any())
{
errContent.Insert(0, Html.ValidationMessageNested(ViewData.ModelMetadata.PropertyName));
errContent.Insert(0, Html.ValidationMessageNested(fullFieldId));
}

var enumerable = errContent.Select(x => x?.ToHtmlString());
Expand Down
4 changes: 1 addition & 3 deletions Web/Edubase.Web.UI/Views/Shared/_ValidationSummary.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,9 @@
// traditional model validation
foreach (var modelError in Model.Keys.SelectMany(key => this.Model[key].Errors.Select(x => Tuple.Create(key, x.ErrorMessage))))
{
var splitName = Regex.Replace(@modelError.Item2, "([a-z])([A-Z])", "$1 $2");
splitName = splitName.Substring(0, 1) + splitName.Substring(1).ToLower();
<li>
<a id="[email protected](@modelError.Item1)-list-item" href='#@modelError.Item1.Replace(".", "_").Replace("[", "_").Replace("]", "_")'>
@Html.HtmlNewlines(@splitName)
@Html.HtmlNewlines(@modelError.Item2)
</a>
</li>
}
Expand Down
Loading
Loading