Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7da04fe
TD-5361 Switch filter logic from AND to OR to match any selected filter
sherif-olaboye Aug 13, 2025
a314e68
TD-5552- Created a stored procedure to send retiring self assessment …
Auldrin-Possa Aug 19, 2025
5d48f64
TD-4624 Add regex validation of professional registration number fiel…
sherif-olaboye Aug 21, 2025
304cb3d
TD-4624 Add regex validation of professional registration number fiel…
sherif-olaboye Aug 21, 2025
e4edffc
TD-4624 Add regex validation of professional registration number fiel…
sherif-olaboye Aug 21, 2025
fb99988
TD-4624 error message to describe exactly what’s allowed
sherif-olaboye Aug 21, 2025
3d516df
TD-4624 removing unnecessary namespace
sherif-olaboye Aug 21, 2025
53c8784
TD-5552-Email format updated
Auldrin-Possa Sep 2, 2025
766149f
TD-5552-Moved general TEL team details to config table.
Auldrin-Possa Sep 10, 2025
800094e
Merge remote-tracking branch 'origin/DLS-Release-v1.2.3' into Develop…
sherif-olaboye Sep 10, 2025
de774a4
TD-5552- Removed 'While' loop and unused variables
Auldrin-Possa Sep 10, 2025
7c951ad
TD-6130-back link shown based on vocabulary on Learning portal and Su…
Auldrin-Possa Sep 10, 2025
24d4146
TD-4624 assign the error string to a variable and reference it
sherif-olaboye Sep 11, 2025
e8ba969
TD-5616-Added ActionConfirmed to MultiPageFormData to maintain form n…
Auldrin-Possa Sep 12, 2025
6fcac2e
Merge pull request #3345 from TechnologyEnhancedLearning/Develop/Fixe…
kevwhitt-hee Sep 15, 2025
ef45c1a
Merge pull request #3353 from TechnologyEnhancedLearning/Develop/feat…
kevwhitt-hee Sep 15, 2025
d5b19a4
Merge pull request #3355 from TechnologyEnhancedLearning/Develop/Feat…
kevwhitt-hee Sep 15, 2025
6501773
Merge pull request #3369 from TechnologyEnhancedLearning/Develop/Fix/…
kevwhitt-hee Sep 15, 2025
872033a
Merge pull request #3370 from TechnologyEnhancedLearning/Develop/Fix/…
kevwhitt-hee Sep 15, 2025
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
@@ -0,0 +1,31 @@
namespace DigitalLearningSolutions.Data.Migrations
{
using FluentMigrator;
[Migration(202508191440)]
public class CreateSendRetiringSelfAssessmentNotificationSP : Migration
{
public override void Up()
{
string generalTELTeam =
@"[
{ ""FirstName"":"""", ""LastName"":"""", ""Email"":""[email protected]"" },
{ ""FirstName"":""Anna"", ""LastName"":""Athanasopoulou"", ""Email"":""[email protected]"" },
{ ""FirstName"":""Benjamin"", ""LastName"":""Witton"", ""Email"":""[email protected]"" }
]";

Execute.Sql(@$"IF NOT EXISTS (SELECT ConfigID FROM Config WHERE ConfigName = 'GeneralTELTeam')
BEGIN
INSERT INTO Config VALUES ('GeneralTELTeam', '{generalTELTeam}', 0,GETDATE(), GETDATE())
END"
);

Execute.Sql(Properties.Resources.TD_5552_SendRetiringNotification);
}
public override void Down()
{
Execute.Sql(@"DELETE FROM Config WHERE ConfigName = N'GeneralTELTeam'");

Execute.Sql("DROP PROCEDURE [dbo].[SendRetiringSelfAssessmentNotification]");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -505,4 +505,7 @@
<data name="TD_5535_Alter_GetActivitiesForDelegateEnrolment_Up" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Scripts\TD-5535-Alter_GetActivitiesForDelegateEnrolment_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16</value>
</data>
<data name="TD_5552_SendRetiringNotification" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Scripts\TD-5552-SendRetiringNotification.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16</value>
</data>
</root>
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public class SessionEnrolOnRoleProfile
public int? SelfAssessmentID { get; set; }
public DateTime? CompleteByDate { get; set; }
public int? SelfAssessmentSupervisorRoleId { get; set; }
public bool ActionConfirmed { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public void LearnerInformationPost_updates_tempdata_correctly()
const string answer4 = "answer4";
const string answer5 = "answer5";
const string answer6 = "answer6";
const string professionalRegistrationNumber = "PRN1234";
const string professionalRegistrationNumber = "PR123456";
var model = new LearnerInformationViewModel
{
JobGroup = jobGroupId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public void Index_post_returns_view_with_model_error_with_invalid_prn()
result.As<ViewResult>().Model.Should().BeOfType<EditDelegateViewModel>();
AssertModelStateErrorIsExpected(
result,
"Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed"
ErrorMessagesTestHelper.InvalidFormatError
);
A.CallTo(() => userService.GetDelegateById(A<int>._)).MustNotHaveHappened();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
namespace DigitalLearningSolutions.Web.Tests.Helpers
{
using System.Linq;
using DigitalLearningSolutions.Web.Helpers;
using DigitalLearningSolutions.Web.Tests.TestHelpers;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NUnit.Framework;
using System.Linq;

public class ProfessionalRegistrationNumberHelperTests
{
Expand Down Expand Up @@ -70,7 +71,7 @@ public void ValidateProfessionalRegistrationNumber_does_not_set_errors_when_vali
{
// Given
var state = new ModelStateDictionary();
const string validPrn = "abc-123";
const string validPrn = "AB123456";

// When
ProfessionalRegistrationNumberHelper.ValidateProfessionalRegistrationNumber(
Expand Down Expand Up @@ -104,22 +105,13 @@ public void ValidateProfessionalRegistrationNumber_sets_error_when_hasPrn_is_not
}
}

[TestCase(null, "Enter a professional registration number")]
[TestCase("", "Enter a professional registration number")]
[TestCase("123", "Professional registration number must be between 5 and 20 characters")]
[TestCase("0123456789-0123456789", "Professional registration number must be between 5 and 20 characters")]
[TestCase(
"01234_",
"Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed"
)]
[TestCase(
"01234 ",
"Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed"
)]
[TestCase(
"01234$",
"Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed"
)]
[TestCase(null, ErrorMessagesTestHelper.MissingNumberError)]
[TestCase("", ErrorMessagesTestHelper.MissingNumberError)]
[TestCase("1234", ErrorMessagesTestHelper.LengthError)]
[TestCase("1234", ErrorMessagesTestHelper.LengthError)]
[TestCase("01234_", ErrorMessagesTestHelper.InvalidFormatError)]
[TestCase("01234 ", ErrorMessagesTestHelper.InvalidFormatError)]
[TestCase("01234$", ErrorMessagesTestHelper.InvalidFormatError)]
public void ValidateProfessionalRegistrationNumber_sets_error_when_prn_is_invalid(
string prn,
string expectedError
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

namespace DigitalLearningSolutions.Web.Tests.TestHelpers
{
public static class ErrorMessagesTestHelper
{
public const string InvalidFormatError =
"Invalid professional registration number format. " +
"Valid formats include: 7 digits (e.g., 1234567), 1–2 letters followed by 6 digits (e.g., AB123456), " +
"4–8 digits, an optional 'P' plus 5–6 digits, 'C' or 'P' plus 6 digits, " +
"an optional letter plus 5–6 digits, 'L' plus 4–6 digits, " +
"or 2 digits followed by a hyphen and 4–5 alphanumeric characters (e.g., 12-AB123).";

public const string MissingNumberError = "Enter a professional registration number";
public const string LengthError = "Professional registration number must be between 5 and 20 characters";

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ public IActionResult ReviewDelegateSelfAssessment(int supervisorDelegateId, int
foreach (var competency in competencies)
{
competency.CompetencyFlags = flags.Where(f => f.CompetencyId == competency.Id);
};
}

if (superviseDelegate.DelegateUserID != null)
{
Expand Down Expand Up @@ -784,48 +784,69 @@ public IActionResult EnrolSetRoleProfile(int supervisorDelegateId, int selfAsses
};
return View("EnrolDelegateOnProfileAssessment", model);
}
var retirementDate = selfAssessmentService.GetSelfAssessmentById(selfAssessmentID).RetirementDate;
if (retirementDate?.Date is DateTime date && date >= DateTime.Today &&
date <= DateTime.Today.AddDays(14))
{
var model = new RetiringSelfAssessmentViewModel()
{
SelfAssessmentID = selfAssessmentID,
SupervisorDelegateID = supervisorDelegateId,
RetirementDate = retirementDate
};
return View("ConfirmRetiringSelfAssessment", model);
}

sessionEnrolOnRoleProfile.SelfAssessmentID = selfAssessmentID;
multiPageFormService.SetMultiPageFormData(
sessionEnrolOnRoleProfile,
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
);

var retirementDate = selfAssessmentService.GetSelfAssessmentById(selfAssessmentID).RetirementDate;
if (CheckRetirementDate(retirementDate))
{
return RedirectToAction("ConfirmRetiringSelfAssessment", "Supervisor", new { supervisorDelegateId });
}

return RedirectToAction(
"EnrolDelegateCompleteBy",
"Supervisor",
new { supervisorDelegateId = supervisorDelegateId }
);
}

[HttpPost]
public IActionResult RetiringSelfAssessmentConfirmed(RetiringSelfAssessmentViewModel retiringSelfAssessment)
[Route("/Supervisor/Staff/{supervisorDelegateId}/ProfileAssessment/Enrol/Confirm")]
public IActionResult ConfirmRetiringSelfAssessment(int supervisorDelegateId)
{
if (ModelState.IsValid && retiringSelfAssessment.ActionConfirmed)
var sessionEnrolOnRoleProfile = multiPageFormService.GetMultiPageFormData<SessionEnrolOnRoleProfile>(
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
).GetAwaiter().GetResult();

var retirementDate = selfAssessmentService.GetSelfAssessmentById((int)sessionEnrolOnRoleProfile.SelfAssessmentID).RetirementDate;
if (!CheckRetirementDate((retirementDate)))
{
var sessionEnrolOnRoleProfile = multiPageFormService.GetMultiPageFormData<SessionEnrolOnRoleProfile>(
return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 });
}
var model = new RetiringSelfAssessmentViewModel()
{
SelfAssessmentID = (int)sessionEnrolOnRoleProfile.SelfAssessmentID,
SupervisorDelegateID = supervisorDelegateId,
RetirementDate = retirementDate,
ActionConfirmed = sessionEnrolOnRoleProfile.ActionConfirmed
};
return View("ConfirmRetiringSelfAssessment", model);
}

[HttpPost]
[Route("/Supervisor/Staff/{supervisorDelegateId}/ProfileAssessment/Enrol/Confirm")]
public IActionResult ConfirmRetiringSelfAssessment(RetiringSelfAssessmentViewModel retiringSelfAssessment)
{
var sessionEnrolOnRoleProfile = multiPageFormService.GetMultiPageFormData<SessionEnrolOnRoleProfile>(
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
).GetAwaiter().GetResult();

sessionEnrolOnRoleProfile.SelfAssessmentID = retiringSelfAssessment.SelfAssessmentID;
multiPageFormService.SetMultiPageFormData(
sessionEnrolOnRoleProfile,
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
);
sessionEnrolOnRoleProfile.SelfAssessmentID = retiringSelfAssessment.SelfAssessmentID;
sessionEnrolOnRoleProfile.ActionConfirmed = retiringSelfAssessment.ActionConfirmed;
multiPageFormService.SetMultiPageFormData(
sessionEnrolOnRoleProfile,
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
);

if (ModelState.IsValid && retiringSelfAssessment.ActionConfirmed)
{
return RedirectToAction(
"EnrolDelegateCompleteBy",
"Supervisor",
Expand All @@ -850,19 +871,21 @@ public IActionResult EnrolDelegateCompleteBy(int supervisorDelegateId, int? day,
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
).GetAwaiter().GetResult();
multiPageFormService.SetMultiPageFormData(
sessionEnrolOnRoleProfile,
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
);

var retirementDate = selfAssessmentService.GetSelfAssessmentById((int)sessionEnrolOnRoleProfile.SelfAssessmentID).RetirementDate;
if (CheckRetirementDate(retirementDate) && !sessionEnrolOnRoleProfile.ActionConfirmed)
{
return RedirectToAction("ConfirmRetiringSelfAssessment", "Supervisor", new { supervisorDelegateId });
}
var supervisorDelegate =
supervisorService.GetSupervisorDelegateDetailsById(supervisorDelegateId, GetAdminId(), 0);
var roleProfile = supervisorService.GetRoleProfileById((int)sessionEnrolOnRoleProfile.SelfAssessmentID);
var model = new EnrolDelegateSetCompletByDateViewModel()
{
SupervisorDelegateDetail = supervisorDelegate,
RoleProfile = roleProfile,
CompleteByDate = sessionEnrolOnRoleProfile.CompleteByDate
CompleteByDate = sessionEnrolOnRoleProfile.CompleteByDate,
ActionConfirmed = sessionEnrolOnRoleProfile.ActionConfirmed
};
if (day != null && month != null && year != null)
{
Expand Down Expand Up @@ -1008,6 +1031,13 @@ public IActionResult EnrolDelegateSummary(int supervisorDelegateId)
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
);

var retirementDate = selfAssessmentService.GetSelfAssessmentById((int)sessionEnrolOnRoleProfile.SelfAssessmentID).RetirementDate;
if (CheckRetirementDate(retirementDate) && !sessionEnrolOnRoleProfile.ActionConfirmed)
{
return RedirectToAction("ConfirmRetiringSelfAssessment", "Supervisor", new { supervisorDelegateId });
}

var supervisorDelegate =
supervisorService.GetSupervisorDelegateDetailsById(supervisorDelegateId, GetAdminId(), 0);
var roleProfile = supervisorService.GetRoleProfileById((int)sessionEnrolOnRoleProfile.SelfAssessmentID);
Expand Down Expand Up @@ -1035,6 +1065,7 @@ public IActionResult EnrolDelegateSummary(int supervisorDelegateId)
ViewBag.completeByMonth = TempData["completeByMonth"];
ViewBag.completeByYear = TempData["completeByYear"];
ViewBag.navigatedFrom = TempData["navigatedFrom"];
ViewBag.actionConfirmed = sessionEnrolOnRoleProfile.ActionConfirmed;
return View("EnrolDelegateSummary", model);
}

Expand Down Expand Up @@ -1558,5 +1589,15 @@ private static string RenderRazorViewToString(Controller controller, string view
return sw.GetStringBuilder().ToString();
}
}

private bool CheckRetirementDate(DateTime? date)
{
if (date == null)
return false;

DateTime retirementOffsetDate = DateTime.Today.AddDays(14);
DateTime today = DateTime.Today;
return (date >= today && date <= retirementOffsetDate);
}
}
}
12 changes: 6 additions & 6 deletions DigitalLearningSolutions.Web/Helpers/CompetencyFilterHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ private static void ApplyResponseStatusFilters(ref IEnumerable<Competency> compe
let searchTextMatchesCompetencyDescription = wordsInSearchText.All(w => c.Description?.Contains(w, StringComparison.CurrentCultureIgnoreCase) ?? false)
let searchTextMatchesCompetencyName = wordsInSearchText.All(w => c.Name?.Contains(w, StringComparison.CurrentCultureIgnoreCase) ?? false)
let responseStatusFilterMatchesAll =
(!filters.Contains((int)SelfAssessmentCompetencyFilter.RequiresSelfAssessment) || c.AssessmentQuestions.Any(q => q.ResultId == null))
&& (!filters.Contains((int)SelfAssessmentCompetencyFilter.SelfAssessed) || c.AssessmentQuestions.Any(q => q.ResultId != null && q.Requested == null && q.SignedOff == null))
&& (!filters.Contains((int)SelfAssessmentCompetencyFilter.ConfirmationRequested) || c.AssessmentQuestions.Any(q => q.Verified == null && q.Requested != null))
&& (!filters.Contains((int)SelfAssessmentCompetencyFilter.ConfirmationRejected) || c.AssessmentQuestions.Any(q => q.Verified.HasValue && q.SignedOff != true))
&& (!filters.Contains((int)SelfAssessmentCompetencyFilter.Verified) || c.AssessmentQuestions.Any(q => q.Verified.HasValue && q.SignedOff == true))
&& (!filters.Contains((int)SelfAssessmentCompetencyFilter.Optional) || c.Optional)
(filters.Contains((int)SelfAssessmentCompetencyFilter.RequiresSelfAssessment) && c.AssessmentQuestions.Any(q => q.ResultId == null))
|| (filters.Contains((int)SelfAssessmentCompetencyFilter.SelfAssessed) && c.AssessmentQuestions.Any(q => q.ResultId != null && q.Requested == null && q.SignedOff == null))
|| (filters.Contains((int)SelfAssessmentCompetencyFilter.ConfirmationRequested) && c.AssessmentQuestions.Any(q => q.Verified == null && q.Requested != null))
|| (filters.Contains((int)SelfAssessmentCompetencyFilter.ConfirmationRejected) && c.AssessmentQuestions.Any(q => q.Verified.HasValue && q.SignedOff != true))
|| (filters.Contains((int)SelfAssessmentCompetencyFilter.Verified) && c.AssessmentQuestions.Any(q => q.Verified.HasValue && q.SignedOff == true))
|| (filters.Contains((int)SelfAssessmentCompetencyFilter.Optional) && c.Optional)
where (wordsInSearchText.Count() == 0 || searchTextMatchesGroup || searchTextMatchesCompetencyDescription || searchTextMatchesCompetencyName)
&& (!appliedResponseStatusFilters.Any() || responseStatusFilterMatchesAll)
select c;
Expand Down
Loading
Loading