diff --git a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs index 3b39ac4361..cac172a725 100644 --- a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs +++ b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs @@ -1558,6 +1558,12 @@ ManageOptionalCompetenciesViewModel model public IActionResult RequestSignOff(int selfAssessmentId) { var delegateUserId = User.GetUserIdKnownNotNull(); + var delegateId = User.GetCandidateIdKnownNotNull(); + var recentResults = selfAssessmentService.GetMostRecentResults(selfAssessmentId, delegateId).ToList(); + var competencySummaries = CertificateHelper.CompetencySummation(recentResults); + + if (competencySummaries.QuestionsCount != competencySummaries.VerifiedCount) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 }); + var assessment = selfAssessmentService.GetSelfAssessmentForCandidateById(delegateUserId, selfAssessmentId); var supervisors = selfAssessmentService.GetSignOffSupervisorsForSelfAssessmentId(selfAssessmentId, delegateUserId); @@ -1568,6 +1574,8 @@ public IActionResult RequestSignOff(int selfAssessmentId) Supervisors = supervisors, NumberOfSelfAssessedOptionalCompetencies = optionalCompetencies.Count(x => x.IncludedInSelfAssessment) }; + if (model.NumberOfSelfAssessedOptionalCompetencies < model.SelfAssessment.MinimumOptionalCompetencies) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 }); + return View("SelfAssessments/RequestSignOff", model); } diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ActivityDelegatesController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ActivityDelegatesController.cs index eea76c00d7..caf4cdf80a 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ActivityDelegatesController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/ActivityDelegatesController.cs @@ -95,14 +95,14 @@ public IActionResult Index( sortBy ??= DefaultSortByOptions.Name.PropertyName; sortDirection ??= GenericSortingHelper.Ascending; - existingFilterString = FilteringHelper.GetFilterString( - existingFilterString, - newFilterToAdd, - clearFilters, - Request, - filterCookieName, - CourseDelegateAccountStatusFilterOptions.Active.FilterValue - ); + existingFilterString = FilteringHelper.GetFilterString( + existingFilterString, + newFilterToAdd, + clearFilters, + Request, + filterCookieName, + CourseDelegateAccountStatusFilterOptions.Active.FilterValue + ); if (isCourseDelegate) { @@ -227,7 +227,7 @@ public IActionResult Index( page = 1; offSet = 0; (selfAssessmentDelegatesData, resultCount) = selfAssessmentService.GetSelfAssessmentDelegatesPerPage(searchString ?? string.Empty, offSet, itemsPerPage ?? 0, sortBy, sortDirection, selfAssessmentId, centreId, isDelegateActive, removed, submitted, signedOff, adminCategoryId); - } + } var adminId = User.GetCustomClaimAsRequiredInt(CustomClaimTypes.UserAdminId); @@ -259,7 +259,7 @@ public IActionResult Index( : selfAssessmentService.GetSelfAssessmentNameById((int)selfAssessmentId); if (!string.IsNullOrEmpty(existingFilterString)) { - existingFilterString = FilteringHelper.GetValidFilters( existingFilterString, newFilterToAdd, availableFilters, Request, filterCookieName); + existingFilterString = FilteringHelper.GetValidFilters(existingFilterString, newFilterToAdd, availableFilters, Request, filterCookieName); } if (isCourseDelegate) { @@ -375,7 +375,9 @@ public IActionResult DownloadCurrent( fileName ); } + [Route("DownloadActivityDelegates/{selfAssessmentId:int}")] + [ServiceFilter(typeof(VerifyAdminUserCanAccessSelfAssessment))] public IActionResult DownloadActivityDelegates( int selfAssessmentId, string? searchString = null, @@ -388,7 +390,6 @@ public IActionResult DownloadActivityDelegates( sortBy ??= DefaultSortByOptions.Name.PropertyName; sortDirection ??= GenericSortingHelper.Ascending; - bool? isDelegateActive, isProgressLocked, removed, hasCompleted, submitted, signedOff; isDelegateActive = isProgressLocked = removed = hasCompleted = submitted = signedOff = null; @@ -456,6 +457,7 @@ public IActionResult DownloadActivityDelegates( fileName ); } + [Route("TrackingSystem/Delegates/ActivityDelegates/{candidateAssessmentsId}/Remove")] [HttpGet] public IActionResult RemoveDelegateSelfAssessment(int candidateAssessmentsId) @@ -466,12 +468,21 @@ public IActionResult RemoveDelegateSelfAssessment(int candidateAssessmentsId) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); } var selfAssessmentDelegate = selfAssessmentService.GetDelegateSelfAssessmentByCandidateAssessmentsId(candidateAssessmentsId); - if (selfAssessmentDelegate == null) + if (selfAssessmentDelegate != null) + { + var adminCategoryId = User.GetAdminCategoryId(); + var selfAssessmentCategoryId = selfAssessmentService.GetSelfAssessmentCategoryId(selfAssessmentDelegate.SelfAssessmentID); + if (adminCategoryId > 0 && adminCategoryId != selfAssessmentCategoryId) + { + return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 }); + } + var model = new DelegateSelfAssessmenteViewModel(selfAssessmentDelegate); + return View(model); + } + else { return new NotFoundResult(); } - var model = new DelegateSelfAssessmenteViewModel(selfAssessmentDelegate); - return View(model); } [Route("TrackingSystem/Delegates/ActivityDelegates/{candidateAssessmentsId}/Remove")] @@ -502,6 +513,7 @@ public IActionResult RemoveDelegateSelfAssessment(DelegateSelfAssessmenteViewMod [HttpGet] [ServiceFilter(typeof(IsCentreAuthorizedSelfAssessment))] + [ServiceFilter(typeof(VerifyAdminUserCanAccessSelfAssessment))] [Route("{selfAssessmentId:int}/EditCompleteByDate")] public IActionResult EditCompleteByDate( int delegateUserId, diff --git a/DigitalLearningSolutions.Web/Helpers/CertificateHelper.cs b/DigitalLearningSolutions.Web/Helpers/CertificateHelper.cs index 7d107df12f..912354764a 100644 --- a/DigitalLearningSolutions.Web/Helpers/CertificateHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/CertificateHelper.cs @@ -46,5 +46,25 @@ public static CompetencySummary CanViewCertificate(List reviewedComp }; return model; } + public static CompetencySummary CompetencySummation(List reviewedCompetencies) + { + var CompetencyGroups = reviewedCompetencies.GroupBy(competency => competency.CompetencyGroup); + var competencySummaries = CompetencyGroups.Select(g => + { + var questions = g.SelectMany(c => c.AssessmentQuestions).Where(q => q.Required); + var verifiedCount = questions.Count(q => !((q.Result == null || q.Verified == null || q.SignedOff != true) && q.Required)); + return new + { + QuestionsCount = questions.Count(), + VerifiedCount = verifiedCount + }; + }); + var model = new CompetencySummary() + { + VerifiedCount = competencySummaries.Sum(item => item.VerifiedCount), + QuestionsCount = competencySummaries.Sum(item => item.QuestionsCount), + }; + return model; + } } } diff --git a/DigitalLearningSolutions.Web/ServiceFilter/VerifyAdminUserCanAccessSelfAssessment.cs b/DigitalLearningSolutions.Web/ServiceFilter/VerifyAdminUserCanAccessSelfAssessment.cs new file mode 100644 index 0000000000..684f5974fa --- /dev/null +++ b/DigitalLearningSolutions.Web/ServiceFilter/VerifyAdminUserCanAccessSelfAssessment.cs @@ -0,0 +1,39 @@ +namespace DigitalLearningSolutions.Web.ServiceFilter +{ + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.Services; + using Microsoft.Extensions.Logging; + + public class VerifyAdminUserCanAccessSelfAssessment : IActionFilter + { + private readonly ISelfAssessmentService selfAssessmentService; + private readonly ILogger logger; + + public VerifyAdminUserCanAccessSelfAssessment(ISelfAssessmentService selfAssessmentService, + ILogger logger) + { + this.selfAssessmentService = selfAssessmentService; + this.logger = logger; + } + + public void OnActionExecuted(ActionExecutedContext context) { } + + public void OnActionExecuting(ActionExecutingContext context) + { + if (!(context.Controller is Controller controller)) + { + return; + } + var adminCategoryId = controller.User.GetAdminCategoryId(); + var selfAssessmentId = int.Parse(context.ActionArguments["selfAssessmentId"].ToString()!); + var selfAssessmentCategoryId = selfAssessmentService.GetSelfAssessmentCategoryId((selfAssessmentId)); + if (adminCategoryId > 0 && adminCategoryId != selfAssessmentCategoryId) + { + logger.LogWarning($"Attempt to access restricted self assessment {selfAssessmentId} by user {controller.User.GetUserIdKnownNotNull()}"); + context.Result = new RedirectToActionResult("StatusCode", "LearningSolutions", new { code = 403 }); + } + } + } +} diff --git a/DigitalLearningSolutions.Web/Startup.cs b/DigitalLearningSolutions.Web/Startup.cs index 193bdd1d2f..bdc34b4d79 100644 --- a/DigitalLearningSolutions.Web/Startup.cs +++ b/DigitalLearningSolutions.Web/Startup.cs @@ -51,7 +51,6 @@ namespace DigitalLearningSolutions.Web using static DigitalLearningSolutions.Web.Services.ICentreApplicationsService; using static DigitalLearningSolutions.Web.Services.ICentreSelfAssessmentsService; using System; - using IsolationLevel = System.Transactions.IsolationLevel; using Serilog; public class Startup @@ -581,6 +580,7 @@ private static void RegisterWebServiceFilters(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } public void Configure(IApplicationBuilder app, IMigrationRunner migrationRunner, IFeatureManager featureManager)