Skip to content

Commit 827693c

Browse files
authored
Merge pull request #2767 from TechnologyEnhancedLearning/Develop/Fix/TD-4445-Prevent-course-enrolment
TD-4445-Course validation added for self enrolment.
2 parents 515f0ce + 93ee220 commit 827693c

File tree

8 files changed

+216
-4
lines changed

8 files changed

+216
-4
lines changed

DigitalLearningSolutions.Data/DataServices/CourseDataService.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ int EnrolOnActivitySelfAssessment(int selfAssessmentId, int candidateId, int sup
137137
public IEnumerable<CourseStatistics> GetDelegateCourseStatisticsAtCentre(string searchString, int centreId, int? categoryId, bool allCentreCourses, bool? hideInLearnerPortal, string isActive, string categoryName, string courseTopic, string hasAdminFields);
138138

139139
public IEnumerable<DelegateAssessmentStatistics> GetDelegateAssessmentStatisticsAtCentre(string searchString, int centreId, string categoryName, string isActive);
140+
bool IsSelfEnrollmentAllowed(int customisationId);
141+
Customisation? GetCourse(int customisationId);
140142
}
141143

142144
public class CourseDataService : ICourseDataService
@@ -1984,5 +1986,38 @@ AND sa.[Name] LIKE '%' + @searchString + '%'
19841986
new { searchString, centreId, categoryName, isActive }, commandTimeout: 3000);
19851987
return delegateAssessmentStatistics;
19861988
}
1989+
1990+
public bool IsSelfEnrollmentAllowed(int customisationId)
1991+
{
1992+
int selfRegister = connection.QueryFirstOrDefault<int>(
1993+
@"SELECT COUNT(CustomisationID) FROM Customisations
1994+
WHERE CustomisationID = @customisationID AND SelfRegister = 1 AND Active = 1",
1995+
new { customisationId });
1996+
1997+
return selfRegister > 0;
1998+
}
1999+
2000+
public Customisation? GetCourse(int customisationId)
2001+
{
2002+
return connection.Query<Customisation>(
2003+
@"SELECT CustomisationID
2004+
,Active
2005+
,CentreID
2006+
,ApplicationID
2007+
,CustomisationName
2008+
,IsAssessed
2009+
,Password
2010+
,SelfRegister
2011+
,TutCompletionThreshold
2012+
,DiagCompletionThreshold
2013+
,DiagObjSelect
2014+
,HideInLearnerPortal
2015+
,NotificationEmails
2016+
FROM Customisations
2017+
WHERE CustomisationID = @customisationID ",
2018+
new { customisationId }).FirstOrDefault();
2019+
2020+
2021+
}
19872022
}
19882023
}

DigitalLearningSolutions.Data/Models/Courses/Customisation.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{
33
public class Customisation
44
{
5+
public Customisation() { }
56
public Customisation(
67
int centreId,
78
int applicationId,
@@ -40,5 +41,8 @@ public Customisation(
4041
public bool DiagObjSelect { get; set; }
4142
public bool HideInLearnerPortal { get; set; }
4243
public string? NotificationEmails { get; set; }
44+
public int CustomisationId { get; set; }
45+
public bool Active { get; set; }
46+
4347
}
4448
}

DigitalLearningSolutions.Web.Tests/Controllers/LearningMenu/IndexTests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
namespace DigitalLearningSolutions.Web.Tests.Controllers.LearningMenu
22
{
3+
using DigitalLearningSolutions.Data.Models.Progress;
34
using DigitalLearningSolutions.Web.Tests.TestHelpers;
45
using DigitalLearningSolutions.Web.ViewModels.LearningMenu;
56
using FakeItEasy;
67
using FluentAssertions;
78
using FluentAssertions.AspNetCore.Mvc;
89
using Microsoft.AspNetCore.Http;
910
using NUnit.Framework;
11+
using System.Collections.Generic;
1012

1113
public partial class LearningMenuControllerTests
1214
{
@@ -15,6 +17,11 @@ public void Index_should_render_view()
1517
{
1618
// Given
1719
var expectedCourseContent = CourseContentHelper.CreateDefaultCourseContent(CustomisationId);
20+
var course = CourseContentHelper.CreateDefaultCourse();
21+
A.CallTo(() => courseService.GetCourse(CustomisationId)).Returns(course);
22+
A.CallTo(() => progressService.GetDelegateProgressForCourse(CandidateId, CustomisationId)).Returns(
23+
new List<Progress> { new Progress { ProgressId = 1, Completed = null, RemovedDate = null } }
24+
);
1825
A.CallTo(() => courseContentService.GetCourseContent(CandidateId, CustomisationId))
1926
.Returns(expectedCourseContent);
2027
A.CallTo(() => courseContentService.GetOrCreateProgressId(CandidateId, CustomisationId, CentreId)).Returns(10);
@@ -38,7 +45,13 @@ public void Index_should_redirect_to_section_page_if_one_section_in_course()
3845
var section = CourseContentHelper.CreateDefaultCourseSection(id: sectionId);
3946
var expectedCourseContent = CourseContentHelper.CreateDefaultCourseContent(customisationId);
4047
expectedCourseContent.Sections.Add(section);
48+
var course = CourseContentHelper.CreateDefaultCourse();
49+
course.CustomisationId = customisationId;
4150

51+
A.CallTo(() => courseService.GetCourse(customisationId)).Returns(course);
52+
A.CallTo(() => progressService.GetDelegateProgressForCourse(CandidateId, customisationId)).Returns(
53+
new List<Progress> { new Progress { ProgressId = 1, Completed = null, RemovedDate = null } }
54+
);
4255
A.CallTo(() => courseContentService.GetCourseContent(CandidateId, customisationId))
4356
.Returns(expectedCourseContent);
4457
A.CallTo(() => courseContentService.GetOrCreateProgressId(CandidateId, customisationId, CentreId)).Returns(10);
@@ -67,6 +80,13 @@ public void Index_should_not_redirect_to_section_page_if_more_than_one_section_i
6780

6881
var expectedCourseContent = CourseContentHelper.CreateDefaultCourseContent(customisationId);
6982
expectedCourseContent.Sections.AddRange(new[] { section1, section2, section3 });
83+
var course = CourseContentHelper.CreateDefaultCourse();
84+
course.CustomisationId = customisationId;
85+
86+
A.CallTo(() => courseService.GetCourse(customisationId)).Returns(course);
87+
A.CallTo(() => progressService.GetDelegateProgressForCourse(CandidateId, customisationId)).Returns(
88+
new List<Progress> { new Progress { ProgressId = 1, Completed = null, RemovedDate = null } }
89+
);
7090

7191
A.CallTo(() => courseContentService.GetCourseContent(CandidateId, customisationId))
7292
.Returns(expectedCourseContent);
@@ -86,6 +106,12 @@ public void Index_should_not_redirect_to_section_page_if_more_than_one_section_i
86106
public void Index_should_return_404_if_unknown_course()
87107
{
88108
// Given
109+
var course = CourseContentHelper.CreateDefaultCourse();
110+
111+
A.CallTo(() => courseService.GetCourse(CustomisationId)).Returns(course);
112+
A.CallTo(() => progressService.GetDelegateProgressForCourse(CandidateId, CustomisationId)).Returns(
113+
new List<Progress> { new Progress { ProgressId = 1, Completed = null, RemovedDate = null } }
114+
);
89115
A.CallTo(() => courseContentService.GetCourseContent(CandidateId, CustomisationId)).Returns(null);
90116
A.CallTo(() => courseContentService.GetOrCreateProgressId(CandidateId, CustomisationId, CentreId))
91117
.Returns(3);
@@ -106,6 +132,12 @@ public void Index_should_return_404_if_unable_to_enrol()
106132
{
107133
// Given
108134
var defaultCourseContent = CourseContentHelper.CreateDefaultCourseContent(CustomisationId);
135+
var course = CourseContentHelper.CreateDefaultCourse();
136+
137+
A.CallTo(() => courseService.GetCourse(CustomisationId)).Returns(course);
138+
A.CallTo(() => progressService.GetDelegateProgressForCourse(CandidateId, CustomisationId)).Returns(
139+
new List<Progress> { new Progress { ProgressId = 1, Completed = null, RemovedDate = null } }
140+
);
109141
A.CallTo(() => courseContentService.GetCourseContent(CandidateId, CustomisationId))
110142
.Returns(defaultCourseContent);
111143
A.CallTo(() => courseContentService.GetOrCreateProgressId(CandidateId, CustomisationId, CentreId))
@@ -127,6 +159,13 @@ public void Index_always_calls_get_course_content()
127159
{
128160
// Given
129161
const int customisationId = 1;
162+
var course = CourseContentHelper.CreateDefaultCourse();
163+
164+
A.CallTo(() => courseService.GetCourse(customisationId)).Returns(course);
165+
A.CallTo(() => progressService.GetDelegateProgressForCourse(CandidateId, customisationId)).Returns(
166+
new List<Progress> { new Progress { ProgressId = 1, Completed = null, RemovedDate = null } }
167+
);
168+
130169

131170
// When
132171
controller.Index(1,2);
@@ -141,6 +180,12 @@ public void Index_valid_customisation_id_should_update_login_and_duration()
141180
// Given
142181
const int progressId = 13;
143182
var defaultCourseContent = CourseContentHelper.CreateDefaultCourseContent(CustomisationId);
183+
var course = CourseContentHelper.CreateDefaultCourse();
184+
185+
A.CallTo(() => courseService.GetCourse(CustomisationId)).Returns(course);
186+
A.CallTo(() => progressService.GetDelegateProgressForCourse(CandidateId, CustomisationId)).Returns(
187+
new List<Progress> { new Progress { ProgressId = 1, Completed = null, RemovedDate = null } }
188+
);
144189
A.CallTo(() => courseContentService.GetCourseContent(CandidateId, CustomisationId))
145190
.Returns(defaultCourseContent);
146191
A.CallTo(() => courseContentService.GetOrCreateProgressId(CandidateId, CustomisationId, CentreId)).Returns(progressId);
@@ -197,6 +242,12 @@ public void Index_valid_customisationId_should_StartOrUpdate_course_sessions()
197242
{
198243
// Given
199244
var defaultCourseContent = CourseContentHelper.CreateDefaultCourseContent(CustomisationId);
245+
var course = CourseContentHelper.CreateDefaultCourse();
246+
247+
A.CallTo(() => courseService.GetCourse(CustomisationId)).Returns(course);
248+
A.CallTo(() => progressService.GetDelegateProgressForCourse(CandidateId, CustomisationId)).Returns(
249+
new List<Progress> { new Progress { ProgressId = 1, Completed = null, RemovedDate = null } }
250+
);
200251
A.CallTo(() => courseContentService.GetCourseContent(CandidateId, CustomisationId))
201252
.Returns(defaultCourseContent);
202253
A.CallTo(() => courseContentService.GetOrCreateProgressId(CandidateId, CustomisationId, CentreId)).Returns(1);
@@ -291,6 +342,13 @@ public void Index_not_detects_id_manipulation_self_register_true()
291342
{
292343
// Given
293344
var expectedCourseContent = CourseContentHelper.CreateDefaultCourseContent(CustomisationId);
345+
346+
var course = CourseContentHelper.CreateDefaultCourse();
347+
348+
A.CallTo(() => courseService.GetCourse(CustomisationId)).Returns(course);
349+
A.CallTo(() => progressService.GetDelegateProgressForCourse(CandidateId, CustomisationId)).Returns(
350+
new List<Progress> { new Progress { ProgressId = 1, Completed = null, RemovedDate = null } }
351+
);
294352
A.CallTo(() => courseContentService.GetCourseContent(CandidateId, CustomisationId))
295353
.Returns(expectedCourseContent);
296354
A.CallTo(() => courseContentService.GetOrCreateProgressId(CandidateId, CustomisationId, CentreId)).Returns(10);

DigitalLearningSolutions.Web.Tests/Controllers/LearningMenu/LearningMenuControllerTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public partial class LearningMenuControllerTests
3434
private ISessionService sessionService = null!;
3535
private ITutorialContentService tutorialContentService = null!;
3636
private ICourseService courseService = null!;
37+
private IProgressService progressService = null!;
38+
private IUserService userService = null!;
3739

3840
[SetUp]
3941
public void SetUp()
@@ -49,6 +51,8 @@ public void SetUp()
4951
postLearningAssessmentService = A.Fake<IPostLearningAssessmentService>();
5052
courseCompletionService = A.Fake<ICourseCompletionService>();
5153
courseService = A.Fake<ICourseService>();
54+
progressService = A.Fake<IProgressService>();
55+
userService = A.Fake<IUserService>();
5256
clockUtility = A.Fake<IClockUtility>();
5357

5458
controller = new LearningMenuController(
@@ -62,6 +66,8 @@ public void SetUp()
6266
sessionService,
6367
courseCompletionService,
6468
courseService,
69+
progressService,
70+
userService,
6571
clockUtility
6672
).WithDefaultContext()
6773
.WithMockHttpContextSession()

DigitalLearningSolutions.Web.Tests/TestHelpers/CourseContentHelper.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{
33
using System;
44
using DigitalLearningSolutions.Data.Models.CourseContent;
5+
using DigitalLearningSolutions.Data.Models.Courses;
56

67
internal class CourseContentHelper
78
{
@@ -62,5 +63,25 @@ public static CourseSection CreateDefaultCourseSection(
6263
postLearningAssessmentsPassed
6364
);
6465
}
66+
67+
public static Customisation CreateDefaultCourse()
68+
{
69+
Customisation customisation = new Customisation();
70+
customisation.CentreId = 1;
71+
customisation.ApplicationId = 1;
72+
customisation.CustomisationName = "Customisation";
73+
customisation.Password = null;
74+
customisation.SelfRegister = true;
75+
customisation.TutCompletionThreshold = 0;
76+
customisation.IsAssessed = true;
77+
customisation.DiagCompletionThreshold = 100;
78+
customisation.DiagObjSelect = true;
79+
customisation.HideInLearnerPortal = false;
80+
customisation.NotificationEmails = null;
81+
customisation.CustomisationId = 1;
82+
customisation.Active = true;
83+
84+
return customisation;
85+
}
6586
}
6687
}

DigitalLearningSolutions.Web/Controllers/LearningMenuController/LearningMenuController.cs

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public class LearningMenuController : Controller
2828
private readonly IPostLearningAssessmentService postLearningAssessmentService;
2929
private readonly ICourseCompletionService courseCompletionService;
3030
private readonly ICourseService courseService;
31+
private readonly IProgressService progressService;
32+
private readonly IUserService userService;
3133
private readonly IClockUtility clockUtility;
3234

3335
public LearningMenuController(
@@ -41,6 +43,8 @@ public LearningMenuController(
4143
ISessionService sessionService,
4244
ICourseCompletionService courseCompletionService,
4345
ICourseService courseService,
46+
IProgressService progressService,
47+
IUserService userService,
4448
IClockUtility clockUtility
4549
)
4650
{
@@ -55,20 +59,43 @@ IClockUtility clockUtility
5559
this.courseCompletionService = courseCompletionService;
5660
this.clockUtility = clockUtility;
5761
this.courseService = courseService;
62+
this.progressService = progressService;
63+
this.userService = userService;
5864
}
5965

6066
[Route("/LearningMenu/{customisationId:int}")]
6167
public IActionResult Index(int customisationId, int progressID)
6268
{
6369
var centreId = User.GetCentreIdKnownNotNull();
6470
var candidateId = User.GetCandidateIdKnownNotNull();
71+
72+
string courseValidationErrorMessage = "Redirecting to 403 as course/centre id was not available for self enrolment. " +
73+
$"Candidate id: {candidateId}, customisation id: {customisationId}, " +
74+
$"centre id: {centreId.ToString() ?? "null"}";
75+
76+
string courseErrorMessage = "Redirecting to 404 as course/centre id was not found. " +
77+
$"Candidate id: {candidateId}, customisation id: {customisationId}, " +
78+
$"centre id: {centreId.ToString() ?? "null"}";
79+
80+
var course = courseService.GetCourse(customisationId);
81+
82+
if (course == null)
83+
{
84+
logger.LogError(courseErrorMessage);
85+
return RedirectToAction("StatusCode", "LearningSolutions", new { code = 404 });
86+
}
87+
88+
if (course.CustomisationName == "ESR" || !course.Active ||
89+
!ValidateCourse(candidateId, customisationId))
90+
{
91+
logger.LogError(courseValidationErrorMessage);
92+
return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 });
93+
}
94+
6595
var courseContent = courseContentService.GetCourseContent(candidateId, customisationId);
6696
if (courseContent == null)
6797
{
68-
logger.LogError(
69-
"Redirecting to 404 as course/centre id was not found. " +
70-
$"Candidate id: {candidateId}, customisation id: {customisationId}, " +
71-
$"centre id: {centreId.ToString() ?? "null"}");
98+
logger.LogError(courseErrorMessage);
7299
return RedirectToAction("StatusCode", "LearningSolutions", new { code = 404 });
73100
}
74101
if (!String.IsNullOrEmpty(courseContent.Password) && !courseContent.PasswordSubmitted)
@@ -627,7 +654,48 @@ private bool UniqueIdManipulationDetected(int candidateId, int customisationId)
627654
{
628655
return false;
629656
}
657+
return true;
658+
}
659+
660+
private bool ValidateCourse(int candidateId, int customisationId)
661+
{
662+
var progress = progressService.GetDelegateProgressForCourse(candidateId, customisationId);
663+
664+
if (progress.Any())
665+
{
666+
if (!progress.Where(p => p.RemovedDate == null).Any())
667+
{
668+
if (!IsValidCourseForEnrloment(customisationId))
669+
{
670+
return false;
671+
}
672+
}
673+
}
674+
else
675+
{
676+
if (!IsValidCourseForEnrloment(customisationId))
677+
{
678+
return false;
679+
}
680+
}
681+
return true;
682+
}
683+
684+
private bool IsValidCourseForEnrloment(int customisationId)
685+
{
686+
if (!courseService.IsSelfEnrollmentAllowed(customisationId))
687+
{
688+
var centreId = User.GetCentreIdKnownNotNull();
689+
var userId = User.GetUserIdKnownNotNull();
690+
var userEntity = userService.GetUserById(userId);
691+
692+
var adminAccount = userEntity!.GetCentreAccountSet(centreId)?.AdminAccount;
630693

694+
if (adminAccount == null)
695+
{
696+
return false;
697+
}
698+
}
631699
return true;
632700
}
633701
}

0 commit comments

Comments
 (0)