Skip to content

Commit 618ee37

Browse files
authored
Merge pull request #3352 from TechnologyEnhancedLearning/Develop/Feature/TD-5339-SelfAssessmentProcessAgreement
TD-5339 Self Assessment Process Agreement
2 parents 88d3659 + c4b040b commit 618ee37

File tree

10 files changed

+241
-1
lines changed

10 files changed

+241
-1
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace DigitalLearningSolutions.Data.Migrations
2+
{
3+
using FluentMigrator;
4+
5+
[Migration(202508190845)]
6+
public class AddSelfAssessmentProcessAgreed : Migration
7+
{
8+
public override void Up()
9+
{
10+
Alter.Table("CandidateAssessments").AddColumn("SelfAssessmentProcessAgreed").AsDateTime().Nullable();
11+
}
12+
13+
public override void Down()
14+
{
15+
Delete.Column("SelfAssessmentProcessAgreed").FromTable("CandidateAssessments");
16+
}
17+
}
18+
}

DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ CandidateAssessments AS CA LEFT OUTER JOIN
193193
SA.LinearNavigation,
194194
SA.UseDescriptionExpanders,
195195
SA.ManageOptionalCompetenciesPrompt,
196+
CAST(CASE WHEN CA.SelfAssessmentProcessAgreed IS NOT NULL THEN 1 ELSE 0 END AS BIT) AS SelfAssessmentProcessAgreed,
196197
CAST(CASE WHEN SA.SupervisorSelfAssessmentReview = 1 OR SA.SupervisorResultsReview = 1 THEN 1 ELSE 0 END AS BIT) AS IsSupervised,
197198
CASE
198199
WHEN (SELECT COUNT(*) FROM SelfAssessmentSupervisorRoles WHERE SelfAssessmentID = @selfAssessmentId AND AllowDelegateNomination = 1) > 0
@@ -241,7 +242,7 @@ GROUP BY
241242
CA.LaunchCount, CA.SubmittedDate, SA.LinearNavigation, SA.UseDescriptionExpanders,
242243
SA.ManageOptionalCompetenciesPrompt, SA.SupervisorSelfAssessmentReview, SA.SupervisorResultsReview,
243244
SA.ReviewerCommentsLabel,SA.EnforceRoleRequirementsForSignOff, SA.ManageSupervisorsDescription,CA.NonReportable,
244-
U.FirstName , U.LastName,SA.MinimumOptionalCompetencies",
245+
U.FirstName , U.LastName,SA.MinimumOptionalCompetencies, CA.SelfAssessmentProcessAgreed",
245246
new { delegateUserId, selfAssessmentId }
246247
);
247248
}
@@ -325,6 +326,22 @@ public void SetCompleteByDate(int selfAssessmentId, int delegateUserId, DateTime
325326
}
326327
}
327328

329+
public void MarkProgressAgreed(int selfAssessmentId, int delegateUserId)
330+
{
331+
var numberOfAffectedRows = connection.Execute(
332+
@"UPDATE CandidateAssessments SET SelfAssessmentProcessAgreed = GETDATE()
333+
WHERE SelfAssessmentID = @selfAssessmentId AND DelegateUserID = @delegateUserId",
334+
new { selfAssessmentId, delegateUserId }
335+
);
336+
if (numberOfAffectedRows < 1)
337+
{
338+
logger.LogWarning(
339+
"SelfAssessmentProcessAgreed not set as db update failed. " +
340+
$"Self assessment id: {selfAssessmentId}, Delegate User id: {delegateUserId}"
341+
);
342+
}
343+
}
344+
328345
public void SetUpdatedFlag(int selfAssessmentId, int delegateUserId, bool status)
329346
{
330347
var numberOfAffectedRows = connection.Execute(

DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ int competencyId
8989

9090
void SetBookmark(int selfAssessmentId, int delegateUserId, string bookmark);
9191

92+
void MarkProgressAgreed(int selfAssessmentId, int delegateUserId);
9293
IEnumerable<CandidateAssessment> GetCandidateAssessments(int delegateUserId, int selfAssessmentId);
9394

9495
// SelfAssessmentSupervisorDataService

DigitalLearningSolutions.Data/Models/SelfAssessments/CurrentSelfAssessment.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ public class CurrentSelfAssessment : SelfAssessment
2424
public int? DelegateUserId { get; set; }
2525
public string? DelegateName { get; set; }
2626
public string? EnrolledByFullName { get; set; }
27+
public bool SelfAssessmentProcessAgreed { get; set; }
2728
}
2829
}

DigitalLearningSolutions.Web.Tests/Controllers/LearningPortal/SelfAssessmentTests.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,5 +870,86 @@ public void SelfAssessmentOverview_Should_Return_View_With_Optional_Filter_Appli
870870

871871
result.Should().BeViewResult().ModelAs<SelfAssessmentOverviewViewModel>().CompetencyGroups.ToList()[0].Count().Should().Be(1);
872872
}
873+
874+
[Test]
875+
public void SelfAssessment_should_return_process_agreement_view_when_not_agreed_and_supervised()
876+
{
877+
// Given
878+
var selfAssessment = SelfAssessmentTestHelper.CreateDefaultSelfAssessment();
879+
selfAssessment.IsSupervised = true;
880+
selfAssessment.SelfAssessmentProcessAgreed = false;
881+
A.CallTo(() => selfAssessmentService.GetSelfAssessmentForCandidateById(DelegateUserId, SelfAssessmentId))
882+
.Returns(selfAssessment);
883+
A.CallTo(() => selfAssessmentService.GetAllSupervisorsForSelfAssessmentId(SelfAssessmentId, DelegateUserId))
884+
.Returns(new List<SelfAssessmentSupervisor>());
885+
886+
// When
887+
var result = controller.SelfAssessment(SelfAssessmentId);
888+
889+
// Then
890+
result.Should().BeViewResult()
891+
.WithViewName("SelfAssessments/AgreeSelfAssessmentProcess")
892+
.Model.Should().BeOfType<SelfAssessmentProcessViewModel>()
893+
.Which.SelfAssessmentID.Should().Be(SelfAssessmentId);
894+
}
895+
896+
[Test]
897+
public void SelfAssessment_should_return_description_view_when_process_agreed_or_not_supervised()
898+
{
899+
// Given
900+
var selfAssessment = SelfAssessmentTestHelper.CreateDefaultSelfAssessment();
901+
selfAssessment.IsSupervised = false; // or set SelfAssessmentProcessAgreed = true
902+
A.CallTo(() => selfAssessmentService.GetSelfAssessmentForCandidateById(DelegateUserId, SelfAssessmentId))
903+
.Returns(selfAssessment);
904+
A.CallTo(() => selfAssessmentService.GetAllSupervisorsForSelfAssessmentId(SelfAssessmentId, DelegateUserId))
905+
.Returns(new List<SelfAssessmentSupervisor>());
906+
var expectedModel = new SelfAssessmentDescriptionViewModel(selfAssessment, new List<SelfAssessmentSupervisor>());
907+
908+
// When
909+
var result = controller.SelfAssessment(SelfAssessmentId);
910+
911+
// Then
912+
result.Should().BeViewResult()
913+
.WithViewName("SelfAssessments/SelfAssessmentDescription")
914+
.Model.Should().BeEquivalentTo(expectedModel);
915+
}
916+
917+
[Test]
918+
public void ProcessAgreed_should_return_agree_view_when_modelstate_invalid()
919+
{
920+
// Given
921+
var model = new SelfAssessmentProcessViewModel { SelfAssessmentID = SelfAssessmentId };
922+
controller.ModelState.AddModelError("Test", "Error");
923+
924+
// When
925+
var result = controller.ProcessAgreed(model);
926+
927+
// Then
928+
result.Should().BeViewResult()
929+
.WithViewName("SelfAssessments/AgreeSelfAssessmentProcess")
930+
.Model.Should().Be(model);
931+
}
932+
933+
[Test]
934+
public void ProcessAgreed_should_mark_progress_and_return_description_view()
935+
{
936+
// Given
937+
var selfAssessment = SelfAssessmentTestHelper.CreateDefaultSelfAssessment();
938+
var supervisors = new List<SelfAssessmentSupervisor>();
939+
var model = new SelfAssessmentProcessViewModel { SelfAssessmentID = SelfAssessmentId };
940+
A.CallTo(() => selfAssessmentService.GetSelfAssessmentForCandidateById(DelegateUserId, SelfAssessmentId))
941+
.Returns(selfAssessment);
942+
A.CallTo(() => selfAssessmentService.GetAllSupervisorsForSelfAssessmentId(SelfAssessmentId, DelegateUserId))
943+
.Returns(supervisors);
944+
945+
// When
946+
var result = controller.ProcessAgreed(model);
947+
948+
// Then
949+
A.CallTo(() => selfAssessmentService.MarkProgressAgreed(SelfAssessmentId, DelegateUserId)).MustHaveHappened();
950+
result.Should().BeViewResult()
951+
.WithViewName("SelfAssessments/SelfAssessmentDescription")
952+
.Model.Should().BeEquivalentTo(new SelfAssessmentDescriptionViewModel(selfAssessment, supervisors));
953+
}
873954
}
874955
}

DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,55 @@ public IActionResult SelfAssessment(int selfAssessmentId)
6868
return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 });
6969
}
7070

71+
if (!selfAssessment.SelfAssessmentProcessAgreed && selfAssessment.IsSupervised)
72+
{
73+
var processmodel = new SelfAssessmentProcessViewModel()
74+
{
75+
SelfAssessmentID = selfAssessmentId,
76+
Vocabulary = selfAssessment.Vocabulary,
77+
VocabPlural = FrameworkVocabularyHelper.VocabularyPlural(selfAssessment.Vocabulary)
78+
};
79+
return View("SelfAssessments/AgreeSelfAssessmentProcess", processmodel);
80+
}
81+
7182
selfAssessmentService.IncrementLaunchCount(selfAssessmentId, delegateUserId);
7283
selfAssessmentService.UpdateLastAccessed(selfAssessmentId, delegateUserId);
7384
var supervisors = selfAssessmentService.GetAllSupervisorsForSelfAssessmentId(
7485
selfAssessmentId,
7586
delegateUserId
7687
).ToList();
7788
var model = new SelfAssessmentDescriptionViewModel(selfAssessment, supervisors);
89+
7890
return View("SelfAssessments/SelfAssessmentDescription", model);
7991
}
8092

93+
[HttpPost]
94+
public IActionResult ProcessAgreed(SelfAssessmentProcessViewModel model)
95+
{
96+
if (!ModelState.IsValid)
97+
{
98+
return View("SelfAssessments/AgreeSelfAssessmentProcess", model);
99+
}
100+
var delegateUserId = User.GetUserIdKnownNotNull();
101+
int selfAssessmentId = model.SelfAssessmentID;
102+
var selfAssessment = selfAssessmentService.GetSelfAssessmentForCandidateById(delegateUserId, selfAssessmentId);
103+
if (selfAssessment == null)
104+
{
105+
logger.LogWarning(
106+
$"Attempt to display self assessment description for user {delegateUserId} with no self assessment"
107+
);
108+
return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 });
109+
}
110+
var supervisors = selfAssessmentService.GetAllSupervisorsForSelfAssessmentId(
111+
selfAssessmentId,
112+
delegateUserId
113+
).ToList();
114+
var selfAssessmentDescriptionViewModel = new SelfAssessmentDescriptionViewModel(selfAssessment, supervisors);
115+
selfAssessmentService.MarkProgressAgreed(selfAssessmentId, delegateUserId);
116+
return View("SelfAssessments/SelfAssessmentDescription", selfAssessmentDescriptionViewModel);
117+
118+
}
119+
81120
[ServiceFilter(typeof(IsCentreAuthorizedSelfAssessment))]
82121
[Route("/LearningPortal/SelfAssessment/{selfAssessmentId:int}/{competencyNumber:int}")]
83122
public IActionResult SelfAssessmentCompetency(int selfAssessmentId, int competencyNumber)

DigitalLearningSolutions.Web/Services/SelfAssessmentService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public interface ISelfAssessmentService
3232

3333
void SetCompleteByDate(int selfAssessmentId, int delegateUserId, DateTime? completeByDate);
3434

35+
void MarkProgressAgreed(int selfAssessmentId, int delegateUserId);
3536
bool CanDelegateAccessSelfAssessment(int delegateUserId, int selfAssessmentId, int centreId);
3637

3738
// Competencies
@@ -216,6 +217,11 @@ public void SetCompleteByDate(int selfAssessmentId, int delegateUserId, DateTime
216217
selfAssessmentDataService.SetCompleteByDate(selfAssessmentId, delegateUserId, completeByDate);
217218
}
218219

220+
public void MarkProgressAgreed(int selfAssessmentId, int delegateUserId)
221+
{
222+
selfAssessmentDataService.MarkProgressAgreed(selfAssessmentId, delegateUserId);
223+
}
224+
219225
public IEnumerable<Competency> GetCandidateAssessmentResultsById(int candidateAssessmentId, int adminId, int? selfAssessmentResultId = null)
220226
{
221227
return selfAssessmentDataService.GetCandidateAssessmentResultsById(candidateAssessmentId, adminId, selfAssessmentResultId);

DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentDescriptionViewModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class SelfAssessmentDescriptionViewModel
1818
public readonly string VocabPlural;
1919
public readonly string? Vocabulary;
2020
public readonly bool NonReportable;
21+
public bool SelfAssessmentProcessAgreed { get; set; }
2122

2223
public SelfAssessmentDescriptionViewModel(
2324
CurrentSelfAssessment selfAssessment,
@@ -37,6 +38,7 @@ List<SelfAssessmentSupervisor> supervisors
3738
Vocabulary = selfAssessment.Vocabulary;
3839
VocabPlural = FrameworkVocabularyHelper.VocabularyPlural(selfAssessment.Vocabulary);
3940
NonReportable = selfAssessment.NonReportable;
41+
SelfAssessmentProcessAgreed = selfAssessment.SelfAssessmentProcessAgreed;
4042
}
4143

4244
public List<SelfAssessmentSupervisor> Supervisors { get; set; }
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace DigitalLearningSolutions.Web.ViewModels.LearningPortal.SelfAssessments
2+
{
3+
using System.Collections.Generic;
4+
using DigitalLearningSolutions.Data.Models.SelfAssessments;
5+
using DigitalLearningSolutions.Web.Attributes;
6+
using DigitalLearningSolutions.Web.Helpers;
7+
8+
public class SelfAssessmentProcessViewModel
9+
{
10+
public int SelfAssessmentID { get; set; }
11+
[BooleanMustBeTrue(ErrorMessage = "Please tick the checkbox to confirm that you understand and agree to the self-assessment process")]
12+
public bool ActionConfirmed { get; set; }
13+
14+
public string? VocabPlural { get; set; }
15+
public string? Vocabulary { get; set; }
16+
17+
}
18+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
@using DigitalLearningSolutions.Web.Extensions
2+
@using DigitalLearningSolutions.Web.ViewModels.LearningPortal.SelfAssessments;
3+
@model SelfAssessmentProcessViewModel
4+
@{
5+
var errorHasOccurred = !ViewData.ModelState.IsValid;
6+
ViewData["Title"] = (errorHasOccurred ? "Error: " : "") + "Agree To Process";
7+
ViewData["Application"] = "LearningPortal";
8+
ViewData["HeaderPathName"] = "LearningPortal";
9+
}
10+
11+
@section NavMenuItems {
12+
<partial name="Shared/_NavMenuItems" />
13+
}
14+
15+
<div class="nhsuk-grid-row">
16+
<div class="nhsuk-grid-column-full word-break">
17+
@if (errorHasOccurred)
18+
{
19+
<vc:error-summary order-of-property-names="@(new[] { nameof(Model.ActionConfirmed) })" />
20+
}
21+
<h2>How are @Model.VocabPlural?.ToLower() and frameworks assessed?</h2>
22+
23+
<p>The process is learner-driven but you must be assessed by a supervisor before completing a self-assessment.</p>
24+
25+
<p>Once enrolled in a framework with at least 1 supervisor added, you can complete a digital self-assessment for any @Model.Vocabulary?.ToLower(). However, before doing so, ensure that:</p>
26+
27+
<ol>
28+
<li>Your supervisor can sign-off this @Model.Vocabulary?.ToLower() based on their competence or department requirements.</li>
29+
<li>You have been formally assessed, this could be through observation, discussion, or another agreed method with your supervisor.</li>
30+
<li>You have reflected on the @Model.Vocabulary?.ToLower() description and feel you meet the requirements.</li>
31+
<li>If you don’t fully meet the requirements, you can still document your progress and next steps for future assessment.</li>
32+
</ol>
33+
34+
<form method="post" asp-controller="LearningPortal" asp-action="ProcessAgreed">
35+
<div class="nhsuk-checkboxes__item">
36+
<vc:single-checkbox asp-for="@nameof(Model.ActionConfirmed)"
37+
label=" I understand and agree to the self-assessment process described above."
38+
hint-text="" />
39+
</div>
40+
<button type="submit" class="nhsuk-button nhsuk-u-margin-top-4">
41+
Continue
42+
</button>
43+
<input type="hidden" asp-for="SelfAssessmentID" />
44+
<input type="hidden" asp-for="Vocabulary" />
45+
<input type="hidden" asp-for="VocabPlural" />
46+
</form>
47+
<div class="nhsuk-back-link">
48+
<a class="nhsuk-back-link__link" asp-controller="LearningPortal"
49+
asp-action="Current">
50+
<svg class="nhsuk-icon nhsuk-icon__chevron-left" focusable='false' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
51+
<path d="M13.41 12l5.3-5.29a1 1 0 1 0-1.42-1.42L12 10.59l-5.29-5.3a1 1 0 0 0-1.42 1.42l5.3 5.29-5.3 5.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l5.29-5.3 5.29 5.3a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42z"></path>
52+
</svg>
53+
Cancel
54+
</a>
55+
</div>
56+
</div>
57+
</div>

0 commit comments

Comments
 (0)