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

Change induction status page #1755

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 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 @@ -322,6 +322,13 @@ public static bool ValidateInductionData(
return true;
}

public bool InductionStatusManagedByCpd(DateOnly? now)
{
var sevenYearsAgo = now?.AddYears(-7);
return InductionCompletedDate is not null
&& InductionCompletedDate < sevenYearsAgo;
}

private static void AssertInductionChangeIsValid(
InductionStatus status,
DateOnly? startDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ public class EditInductionState : IRegisterJourney
appendUniqueKey: true);

public InductionStatus InductionStatus { get; set; }
public InductionStatus InitialInductionStatus{ get; set; }
public DateOnly? StartDate { get; set; }
public DateOnly? CompletedDate { get; set; }
public InductionExemptionReasons? ExemptionReasons { get; set; }
public string? ChangeReason { get; set; }
public InductionJourneyPage? JourneyStartPage { get; set; }
public bool RecordManagedInCpd { get; set; }

public bool Initialized { get; set; }

Expand All @@ -27,7 +29,8 @@ public async Task EnsureInitializedAsync(TrsDbContext dbContext, Guid personId,
}
var person = await dbContext.Persons
.SingleAsync(q => q.PersonId == personId);
InductionStatus = person!.InductionStatus;

InitialInductionStatus = person!.InductionStatus;
if (JourneyStartPage == null)
{
JourneyStartPage = startPage;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@page "/persons/{PersonId}/edit-induction/status"
@model TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction.StatusModel
@{
ViewBag.Title = "Edit status: " + Model.InductionStatus.GetName();
ViewBag.Title = "What is their induction status?";
}

@section BeforeContent {
Expand All @@ -12,7 +12,17 @@

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<span data-testid="induction-status-caption" class="govuk-caption-l">Induction - @Model.PersonName</span>
<form data-testid="submit-form" action="@LinkGenerator.InductionEditStatus(Model.PersonId, Model.JourneyInstance!.InstanceId)" method="post">
<govuk-radios asp-for="InductionStatus">
<govuk-radios-fieldset>
<govuk-radios-fieldset-legend data-testid="status-choices-legend" class="govuk-fieldset__legend--m" />
@foreach (var inductionStatus in Model.StatusChoices)
{
<govuk-radios-item value="@inductionStatus.Value" checked="@Model.InductionStatus==inductionStatus.Value">@inductionStatus.Title</govuk-radios-item>
}
</govuk-radios-fieldset>
</govuk-radios>
<input type="hidden" name="InductionStatus" value="@Model.InductionStatus" />
<div class="govuk-button-group">
<govuk-button type="submit" data-testid="continue-button">Continue</govuk-button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using TeachingRecordSystem.Core.DataStore.Postgres;
using TeachingRecordSystem.SupportUi.ValidationAttributes;

namespace TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction;

[Journey(JourneyNames.EditInduction), ActivatesJourney, RequireJourneyInstance]
public class StatusModel : CommonJourneyPage
{
private static List<InductionStatus> ValidStatusesWhenManagedByCpd = new() { InductionStatus.RequiredToComplete, InductionStatus.Exempt, InductionStatus.FailedInWales };

protected TrsDbContext _dbContext;
protected IClock _clock;
protected bool InductionStatusManagedByCpd;

[BindProperty]
[Display(Name = "Select a status")]
[NotEqual(InductionStatus.None, ErrorMessage = "Select a status")]
public InductionStatus InductionStatus { get; set; }
public InductionStatus InitialInductionStatus { get; set; }
public string? PersonName { get; set; }
public IEnumerable<InductionStatusInfo> StatusChoices
{
get
{
return InductionStatusManagedByCpd ?
InductionStatusRegistry.All.Where(i => ValidStatusesWhenManagedByCpd.Contains(i.Value) && i.Value != InitialInductionStatus)
: InductionStatusRegistry.All.ToArray()[1..].Where(i => i.Value != InitialInductionStatus);
}
}

public InductionJourneyPage NextPage
{
Expand All @@ -26,18 +45,33 @@ _ when InductionStatus.RequiresStartDate() => InductionJourneyPage.StartDate,
}
public string BackLink => LinkGenerator.PersonInduction(PersonId);

public StatusModel(TrsLinkGenerator linkGenerator, TrsDbContext dbContext) : base(linkGenerator)
public StatusModel(TrsLinkGenerator linkGenerator, TrsDbContext dbContext, IClock clock) : base(linkGenerator)
{
_dbContext = dbContext;
_clock = clock;
}

public void OnGet()
public async Task OnGetAsync()
{
var person = await _dbContext.Persons.SingleAsync(q => q.PersonId == PersonId);
InductionStatusManagedByCpd = person.InductionStatusManagedByCpd(_clock.UtcNow.ToDateOnlyWithDqtBstFix(true)); // CML TODO understand date-time stuff
CathLass marked this conversation as resolved.
Show resolved Hide resolved
InductionStatus = JourneyInstance!.State.InductionStatus;
InitialInductionStatus = JourneyInstance!.State.InitialInductionStatus;
await JourneyInstance!.UpdateStateAsync(state =>
{
if (state.InitialInductionStatus == InductionStatus.None)
{
state.InitialInductionStatus = InitialInductionStatus;
}
});
}

public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return this.PageWithErrors();
}
await JourneyInstance!.UpdateStateAsync(state =>
{
state.InductionStatus = InductionStatus;
Expand All @@ -53,7 +87,10 @@ public async Task<IActionResult> OnPostAsync()
public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
await JourneyInstance!.State.EnsureInitializedAsync(_dbContext, PersonId, InductionJourneyPage.Status);


var personInfo = context.HttpContext.GetCurrentPersonFeature();
PersonId = personInfo.PersonId;
PersonName = personInfo.Name;
await next();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,12 @@ public async Task OnGetAsync()
StartDate = person!.InductionStartDate;
CompletionDate = person!.InductionCompletedDate;
ExemptionReasons = person!.InductionExemptionReasons;
_statusIsManagedByCpd = StatusManagedByCpdRule(person!.CpdInductionStatus, person.CpdInductionCompletedDate);
_statusIsManagedByCpd = person.InductionStatusManagedByCpd(clock.Today);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the correct date method?

_teacherHoldsQualifiedTeacherStatus = TeacherHoldsQualifiedTeacherStatusRule(result?.Contact.dfeta_QTSDate);
}

private bool TeacherHoldsQualifiedTeacherStatusRule(DateTime? qtsDate)
{
return qtsDate is null;
}

private bool StatusManagedByCpdRule(InductionStatus? status, DateOnly? inductionCompletedDate)
{
var sevenYearsAgo = clock.Today.AddYears(-7);
return status is not null
&& inductionCompletedDate is not null
&& inductionCompletedDate < sevenYearsAgo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace TeachingRecordSystem.SupportUi.ValidationAttributes;

using System.ComponentModel.DataAnnotations;

public class NotEqualAttribute : ValidationAttribute
{
private readonly object _notAllowedValue;

public NotEqualAttribute(object notAllowedValue)
{
_notAllowedValue = notAllowedValue;
}

protected override ValidationResult? IsValid(object value, ValidationContext validationContext)
{
if (value != null && value.Equals(_notAllowedValue))
{
return new ValidationResult(ErrorMessage ?? $"The value cannot be {_notAllowedValue}.");
}

return ValidationResult.Success;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
using TeachingRecordSystem.Core.Dqt;

namespace TeachingRecordSystem.Core.Tests.DataStore.Postgres.Models;

Expand Down Expand Up @@ -228,4 +229,33 @@ public void TrySetWelshInductionStatus_StatusIsAtLowerPriorityStatus_UpdatesStat
Assert.Equal(expectedStatus, person.InductionStatus);
Assert.Equal(expectedExemptionReasons, person.InductionExemptionReasons);
}

[Theory]
[InlineData(-3, false)]
[InlineData(-7, true)]
public void InductionManagedByCpd_ReturnsTrue(int yearsSinceCompleted, bool expected)
{
// CML TODO figure out the date-time types
// Arrange
var dateTimeCompleted = Clock.UtcNow.AddYears(yearsSinceCompleted).AddDays(-1);
var dateCompleted = dateTimeCompleted.ToDateOnlyWithDqtBstFix(true);
var person = new Person
{
PersonId = Guid.NewGuid(),
CreatedOn = dateTimeCompleted,
UpdatedOn = dateTimeCompleted,
Trn = "1234567",
FirstName = "Joe",
MiddleName = "",
LastName = "Bloggs",
DateOfBirth = new(1990, 1, 1),
};
person.SetInductionStatus(InductionStatus.Passed, dateCompleted, dateCompleted, InductionExemptionReasons.None, SystemUser.SystemUserId, Clock.UtcNow, out _);

// Act
var result = person.InductionStatusManagedByCpd(Clock.UtcNow.ToDateOnlyWithDqtBstFix(true));

// Assert
Assert.Equal(expected,result);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
using TeachingRecordSystem.SupportUi.Pages.Persons.PersonDetail.EditInduction;

namespace TeachingRecordSystem.SupportUi.Tests.PageTests.Persons.PersonDetail.EditInduction;

public class EditInductionStatusTests(HostFixture hostFixture) : TestBase(hostFixture)
{
[Fact]
public async Task ContinueAndCancelButtons_ExistOnPage()
public async Task Get_PageLegend_Expected()
{
// Arrange
InductionStatus inductionStatus = InductionStatus.Passed;
var person = await TestData.CreatePersonAsync(p => p
.WithQts()
.WithFirstName("Alfred")
.WithMiddleName("The")
.WithLastName("Great"));
var expectedCaption = "Induction - Alfred The Great";
var journeyInstance = await CreateJourneyInstanceAsync(
person.PersonId,
new EditInductionState()
{
Initialized = true,
InductionStatus = inductionStatus
});
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/edit-induction/status?{journeyInstance.GetUniqueIdQueryParameter()}");

// Act
var response = await HttpClient.SendAsync(request);

// Assert
var doc = await AssertEx.HtmlResponseAsync(response);
var caption = doc.GetElementByTestId("induction-status-caption");
Assert.Equal(expectedCaption, caption!.TextContent);
}

[Fact]
public async Task Get_ContinueAndCancelButtons_ExistOnPage()
{
// Arrange
InductionStatus inductionStatus = InductionStatus.Passed;
Expand Down Expand Up @@ -35,6 +66,113 @@ public async Task ContinueAndCancelButtons_ExistOnPage()
Assert.Equal("Cancel and return to record", buttons.ElementAt(1)!.TextContent);
}

[Theory]
[InlineData(InductionStatus.RequiredToComplete, new InductionStatus[] {InductionStatus.Exempt, InductionStatus.InProgress, InductionStatus.Passed, InductionStatus.Failed, InductionStatus.FailedInWales })]
[InlineData(InductionStatus.Exempt, new InductionStatus[] { InductionStatus.RequiredToComplete, InductionStatus.InProgress, InductionStatus.Passed, InductionStatus.Failed, InductionStatus.FailedInWales })]
[InlineData(InductionStatus.InProgress, new InductionStatus[] { InductionStatus.RequiredToComplete, InductionStatus.Exempt, InductionStatus.Passed, InductionStatus.Failed, InductionStatus.FailedInWales })]
[InlineData(InductionStatus.Passed, new InductionStatus[] { InductionStatus.RequiredToComplete, InductionStatus.Exempt, InductionStatus.InProgress, InductionStatus.Failed, InductionStatus.FailedInWales })]
[InlineData(InductionStatus.Failed, new InductionStatus[] { InductionStatus.RequiredToComplete, InductionStatus.Exempt, InductionStatus.InProgress, InductionStatus.Passed, InductionStatus.FailedInWales })]
[InlineData(InductionStatus.FailedInWales, new InductionStatus[] { InductionStatus.RequiredToComplete, InductionStatus.Exempt, InductionStatus.InProgress, InductionStatus.Passed, InductionStatus.Failed })]
public async Task Get_InductionNotManagedByCpd_ExpectedRadioButtonsExistOnPage(InductionStatus inductionStatus, InductionStatus[] expectedStatuses)
{
// Arrange
var expectedChoices = expectedStatuses.Select(s => s.ToString());

var person = await TestData.CreatePersonAsync(p => p.WithQts());

var journeyInstance = await CreateJourneyInstanceAsync(
person.PersonId,
new EditInductionState()
{
Initialized = true,
InitialInductionStatus = inductionStatus
});
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/edit-induction/status?{journeyInstance.GetUniqueIdQueryParameter()}");

// Act
var response = await HttpClient.SendAsync(request);

// Assert
var doc = await AssertEx.HtmlResponseAsync(response);
var statusChoices = doc.QuerySelectorAll<IHtmlInputElement>("[type=radio]").Select(r => r.Value);
var statusChoicesLegend = doc.GetElementByTestId("status-choices-legend");
Assert.Equal("Select a status", statusChoicesLegend!.TextContent);
Assert.Equal(expectedChoices, statusChoices);
}

[Theory]
[InlineData(InductionStatus.Passed, new InductionStatus[] { InductionStatus.RequiredToComplete, InductionStatus.Exempt, InductionStatus.FailedInWales })]
[InlineData(InductionStatus.Failed, new InductionStatus[] { InductionStatus.RequiredToComplete, InductionStatus.Exempt, InductionStatus.FailedInWales })]
[InlineData(InductionStatus.FailedInWales, new InductionStatus[] { InductionStatus.RequiredToComplete, InductionStatus.Exempt })]
public async Task Get_InductionManagedByCpd_ExpectedRadioButtonsExistOnPage(InductionStatus inductionStatus, InductionStatus[] expectedStatuses)
{
// Arrange
var expectedChoices = expectedStatuses.Select(s => s.ToString());
var overSevenYearsAgo = Clock.Today.AddYears(-7).AddDays(-1);
var person = await TestData.CreatePersonAsync(p => p.WithQts());
await WithDbContext(async dbContext =>
{
dbContext.Attach(person.Person);
person.Person.SetCpdInductionStatus(
InductionStatus.Passed,
startDate: Clock.Today.AddYears(-7).AddMonths(-6),
completedDate: overSevenYearsAgo,
cpdModifiedOn: Clock.UtcNow,
updatedBy: SystemUser.SystemUserId,
now: Clock.UtcNow,
out _);
await dbContext.SaveChangesAsync();
});

var journeyInstance = await CreateJourneyInstanceAsync(
person.PersonId,
new EditInductionState()
{
Initialized = true,
InitialInductionStatus = inductionStatus
});
var request = new HttpRequestMessage(HttpMethod.Get, $"/persons/{person.PersonId}/edit-induction/status?{journeyInstance.GetUniqueIdQueryParameter()}");

// Act
var response = await HttpClient.SendAsync(request);

// Assert
var doc = await AssertEx.HtmlResponseAsync(response);
var statusChoices = doc.QuerySelectorAll<IHtmlInputElement>("[type=radio]").Select(r => r.Value);
var statusChoicesLegend = doc.GetElementByTestId("status-choices-legend");
Assert.Equal("Select a status", statusChoicesLegend!.TextContent);
Assert.Equal(expectedChoices, statusChoices);
}

[Fact]
public async Task Post_SelectedStatus_PersistsStatus()
{
// Arrange
var person = await TestData.CreatePersonAsync(p => p.WithQts());

var journeyInstance = await CreateJourneyInstanceAsync(
person.PersonId,
new EditInductionState()
{
Initialized = true,
InductionStatus = InductionStatus.Passed
});
var postRequest = new HttpRequestMessage(HttpMethod.Post, $"/persons/{person.PersonId}/edit-induction/status?{journeyInstance.GetUniqueIdQueryParameter()}")
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["InductionStatus"] = "Exempt"
})
};

// Act
var response = await HttpClient.SendAsync(postRequest);

// Assert
journeyInstance = await ReloadJourneyInstance(journeyInstance);
Assert.Equal("Exempt", journeyInstance.State.InductionStatus.GetTitle());
}

private Task<JourneyInstance<EditInductionState>> CreateJourneyInstanceAsync(Guid personId, EditInductionState? state = null) =>
CreateJourneyInstance(
JourneyNames.EditInduction,
Expand Down
Loading