diff --git a/.gitignore b/.gitignore index 1a70475b0e..74ae0b44e1 100644 --- a/.gitignore +++ b/.gitignore @@ -512,3 +512,5 @@ DigitalLearningSolutions.Web/Views/Shared/Components/SelectList/Default.cshtml DigitalLearningSolutions.Web/Views/Shared/Components/SingleCheckbox/Default.cshtml DigitalLearningSolutions.Web/Views/Shared/Components/TextArea/Default.cshtml DigitalLearningSolutions.Web/Views/Shared/Components/TextInput/Default.cshtml +/DigitalLearningSolutions.Web/appsettings.Test.json +/DigitalLearningSolutions.Web/web.config diff --git a/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs b/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs new file mode 100644 index 0000000000..86b2026fc4 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs @@ -0,0 +1,17 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + [Migration(202506110906)] + public class AddSqlMaintenanceSolution : Migration + { + + public override void Up() + { + Execute.Sql(Properties.Resources.TD_5670_MaintenanceScripts_UP); + } + public override void Down() + { + Execute.Sql(Properties.Resources.TD_5670_MaintenanceScripts_DOWN); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/202508190845_AddSelfAssessmentProcessAgreed .cs b/DigitalLearningSolutions.Data.Migrations/202508190845_AddSelfAssessmentProcessAgreed .cs new file mode 100644 index 0000000000..97ea3eeae0 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202508190845_AddSelfAssessmentProcessAgreed .cs @@ -0,0 +1,18 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + + [Migration(202508190845)] + public class AddSelfAssessmentProcessAgreed : Migration + { + public override void Up() + { + Alter.Table("CandidateAssessments").AddColumn("SelfAssessmentProcessAgreed").AsDateTime().Nullable(); + } + + public override void Down() + { + Delete.Column("SelfAssessmentProcessAgreed").FromTable("CandidateAssessments"); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs index 4bd193dcf1..0f99da6fea 100644 --- a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs +++ b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs @@ -478,7 +478,8 @@ internal static string DLSV2_272_AlterGetLinkedFieldNameFunction_UP { ///-- Create date: 15/10/2021 ///-- Description: Reorders the CompetencyAssessmentQuestions - moving the given competency question up or down. ///-- ============================================= - ///CREATE OR ALTER PROCEDURE [dbo].[ReorderCompetencyAssessmentQuestion] /// [rest of string was truncated]";. + ///CREATE OR ALTER PROCEDURE [dbo].[ReorderCompetencyAssessmentQuestion] + /// [rest of string was truncated]";. /// internal static string DLSV2_379_ReorderCompetencyAssessmentQuestionsSP { get { @@ -1504,7 +1505,8 @@ internal static string TD_3190_SendOneMonthSelfAssessmentOverdueRemindersSP { /// @EmailProfileName nvarchar(100), /// @TestOnly bit ///AS - ///BEGIN /// [rest of string was truncated]";. + ///BEGIN + /// [rest of string was truncated]";. /// internal static string TD_3190_SendOneMonthSelfAssessmentTBCRemindersSP { get { @@ -2525,6 +2527,16 @@ internal static string TD_5514_Alter_SendExpiredTBCReminders_Up { } /// + /// Looks up a localized string similar to IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize; + ///IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute; + ///IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; + ///. + /// + internal static string TD_5670_MaintenanceScripts_DOWN { + get { + return ResourceManager.GetString("TD-5670-MaintenanceScripts_DOWN", resourceCulture); + } + } /// Looks up a localized string similar to CREATE OR ALTER PROCEDURE [dbo].[usp_GetSelfAssessmentReport] /// @SelfAssessmentID INT, /// @CentreID INT @@ -2549,6 +2561,27 @@ internal static string TD_5759_CreateOrAlterSelfAssessmentReportSPandTVF_Fix_UP } /// + /// Looks up a localized string similar to -- ============================================ + ///-- CommandLog table + ///-- ============================================ + ///IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; + ///CREATE TABLE dbo.CommandLog ( + /// ID INT IDENTITY PRIMARY KEY, + /// DatabaseName SYSNAME NULL, + /// SchemaName SYSNAME NULL, + /// ObjectName SYSNAME NULL, + /// ObjectType CHAR(2) NULL, + /// IndexName SYSNAME NULL, + /// IndexType TINYINT NULL, + /// StatisticsName SYSNAME NULL, + /// PartitionNumber INT NULL, + /// Ext [rest of string was truncated]";. + /// + internal static string TD_5670_MaintenanceScripts_UP { + get { + return ResourceManager.GetString("TD_5670_MaintenanceScripts_UP", resourceCulture); + } + } /// Looks up a localized string similar to CREATE OR ALTER FUNCTION dbo.GetOtherCentresForSelfAssessmentTVF ///( /// @UserID INT, diff --git a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx index d6a79ed9d3..55d97e6ce3 100644 --- a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx +++ b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx @@ -487,6 +487,12 @@ ..\Scripts\TD-5447-Alter_ReorderFrameworkCompetency_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + ..\Scripts\TD-5670-MaintenanceScripts_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Scripts\TD-5670-MaintenanceScripts_DOWN.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + ..\Scripts\TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql new file mode 100644 index 0000000000..14a45533ad --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql @@ -0,0 +1,4 @@ +IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize; +IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute; +IF OBJECT_ID('dbo.sp_purge_commandlog', 'P') IS NOT NULL DROP PROCEDURE dbo.sp_purge_commandlog; +IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql new file mode 100644 index 0000000000..ac8258b6fa --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql @@ -0,0 +1,204 @@ +-- ============================================ +-- Drop if exists (for clean redeploy) +-- ============================================ +IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize; +IF OBJECT_ID('dbo.DatabaseIntegrityCheck', 'P') IS NOT NULL DROP PROCEDURE dbo.DatabaseIntegrityCheck; +IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute; +IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; +GO + +-- ============================================ +-- CommandLog table +-- ============================================ +CREATE TABLE dbo.CommandLog ( + ID INT IDENTITY PRIMARY KEY, + DatabaseName SYSNAME NULL, + SchemaName SYSNAME NULL, + ObjectName SYSNAME NULL, + ObjectType CHAR(2) NULL, + IndexName SYSNAME NULL, + IndexType TINYINT NULL, + StatisticsName SYSNAME NULL, + PartitionNumber INT NULL, + ExtendedInfo XML NULL, + Command NVARCHAR(MAX) NOT NULL, + CommandType NVARCHAR(60) NOT NULL, + StartTime DATETIME NOT NULL, + EndTime DATETIME NOT NULL, + ErrorNumber INT NOT NULL, + ErrorMessage NVARCHAR(MAX) NULL +); +GO + +-- ============================================ +-- CommandExecute stored procedure +-- ============================================ +CREATE PROCEDURE dbo.CommandExecute + @Command NVARCHAR(MAX), + @CommandType NVARCHAR(60), + @DatabaseName SYSNAME = NULL, + @SchemaName SYSNAME = NULL, + @ObjectName SYSNAME = NULL, + @ObjectType CHAR(2) = NULL, + @IndexName SYSNAME = NULL, + @IndexType TINYINT = NULL, + @StatisticsName SYSNAME = NULL, + @PartitionNumber INT = NULL, + @ExtendedInfo XML = NULL +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @StartTime DATETIME = GETDATE(); + DECLARE @ErrorNumber INT = 0; + DECLARE @ErrorMessage NVARCHAR(MAX) = NULL; + + BEGIN TRY + EXEC (@Command); + END TRY + BEGIN CATCH + SET @ErrorNumber = ERROR_NUMBER(); + SET @ErrorMessage = ERROR_MESSAGE(); + END CATCH; + + INSERT INTO dbo.CommandLog ( + DatabaseName, SchemaName, ObjectName, ObjectType, IndexName, IndexType, StatisticsName, + PartitionNumber, ExtendedInfo, Command, CommandType, StartTime, EndTime, ErrorNumber, ErrorMessage + ) + VALUES ( + @DatabaseName, @SchemaName, @ObjectName, @ObjectType, @IndexName, @IndexType, @StatisticsName, + @PartitionNumber, @ExtendedInfo, @Command, @CommandType, @StartTime, GETDATE(), @ErrorNumber, @ErrorMessage + ); + + IF @ErrorNumber <> 0 + RAISERROR(@ErrorMessage, 16, 1); +END +GO + +-- ============================================ +-- IndexOptimize stored procedure +-- ============================================ +CREATE PROCEDURE dbo.IndexOptimize + @Databases NVARCHAR(MAX) = 'USER_DATABASES', + @FragmentationMedium TINYINT = 30, + @FragmentationHigh TINYINT = 70 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @db SYSNAME; + DECLARE db_cursor CURSOR FOR + SELECT name FROM sys.databases + WHERE (@Databases = 'USER_DATABASES' AND database_id > 4) + OR name = @Databases; + + OPEN db_cursor; + FETCH NEXT FROM db_cursor INTO @db; + + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @sql NVARCHAR(MAX) = N' + USE [' + @db + ']; + + DECLARE @schema SYSNAME, @table SYSNAME, @index SYSNAME; + DECLARE @index_id INT, @frag FLOAT; + + DECLARE c CURSOR FOR + SELECT s.name, t.name, i.name, i.index_id, ips.avg_fragmentation_in_percent + FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, ''LIMITED'') ips + JOIN sys.indexes i ON i.object_id = ips.object_id AND i.index_id = ips.index_id + JOIN sys.tables t ON t.object_id = ips.object_id + JOIN sys.schemas s ON s.schema_id = t.schema_id + WHERE ips.index_id > 0 AND ips.page_count > 100; + + OPEN c; + FETCH NEXT FROM c INTO @schema, @table, @index, @index_id, @frag; + + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @cmd NVARCHAR(MAX); + SET @cmd = ''ALTER INDEX ['' + @index + ''] ON ['' + @schema + ''].['' + @table + ''] ''; + + IF @frag >= ' + CAST(@FragmentationHigh AS NVARCHAR) + ' + SET @cmd += ''REBUILD''; + ELSE IF @frag >= ' + CAST(@FragmentationMedium AS NVARCHAR) + ' + SET @cmd += ''REORGANIZE''; + ELSE + SET @cmd = NULL; + + IF @cmd IS NOT NULL + EXEC dbo.CommandExecute @Command = @cmd, + @CommandType = ''ALTER INDEX'', + @DatabaseName = ''' + @db + ''', + @SchemaName = @schema, + @ObjectName = @table, + @ObjectType = ''U'', + @IndexName = @index; + + FETCH NEXT FROM c INTO @schema, @table, @index, @index_id, @frag; + END; + + CLOSE c; + DEALLOCATE c; + '; + + EXEC sp_executesql @sql; + FETCH NEXT FROM db_cursor INTO @db; + END; + + CLOSE db_cursor; + DEALLOCATE db_cursor; +END +GO + +-- ============================================ +-- DatabaseIntegrityCheck stored procedure +-- ============================================ +CREATE PROCEDURE dbo.DatabaseIntegrityCheck + @Databases NVARCHAR(MAX) = 'USER_DATABASES' +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @db SYSNAME; + DECLARE db_cursor CURSOR FOR + SELECT name FROM sys.databases + WHERE (@Databases = 'USER_DATABASES' AND database_id > 4) + OR name = @Databases; + + OPEN db_cursor; + FETCH NEXT FROM db_cursor INTO @db; + + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @cmd NVARCHAR(MAX); + SET @cmd = 'DBCC CHECKDB([' + @db + ']) WITH NO_INFOMSGS, ALL_ERRORMSGS'; + + EXEC dbo.CommandExecute + @Command = @cmd, + @CommandType = 'DBCC CHECKDB', + @DatabaseName = @db; + + FETCH NEXT FROM db_cursor INTO @db; + END + + CLOSE db_cursor; + DEALLOCATE db_cursor; +END +GO + +-- ============================================ +-- Purge command log stored procedure +-- ============================================ +CREATE OR ALTER PROCEDURE dbo.sp_purge_commandlog + @DaysToKeep INT = 30 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @DeleteBefore DATETIME = DATEADD(DAY, -@DaysToKeep, GETDATE()); + + DELETE FROM dbo.CommandLog + WHERE StartTime < @DeleteBefore; +END +GO diff --git a/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs b/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs index 2a48f0ae3d..f586fc92d6 100644 --- a/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs @@ -84,7 +84,7 @@ public void UpdateCentreDetailsForSuperAdmin( string centreName, int centreTypeId, int regionId, - string? centreEmail, + string? registrationEmail, string? ipPrefix, bool showOnMap ); @@ -180,7 +180,7 @@ ORDER BY CentreName" c.ContactSurname, c.ContactEmail, c.ContactTelephone, - c.AutoRegisterManagerEmail AS CentreEmail, + c.AutoRegisterManagerEmail AS RegistrationEmail, c.ShowOnMap, c.CMSAdministrators AS CmsAdministratorSpots, c.CMSManagers AS CmsManagerSpots, @@ -192,7 +192,8 @@ ORDER BY CentreName" c.ServerSpaceBytes, cty.CentreType, c.CandidateByteLimit, - c.ContractReviewDate + c.ContractReviewDate, + c.pwEmail as CentreEmail FROM Centres AS c INNER JOIN Regions AS r ON r.RegionID = c.RegionID INNER JOIN ContractTypes AS ct ON ct.ContractTypeID = c.ContractTypeId @@ -232,7 +233,7 @@ FROM Centres AS c c.ContactEmail, c.ContactTelephone, c.pwTelephone AS CentreTelephone, - c.AutoRegisterManagerEmail AS CentreEmail, + c.AutoRegisterManagerEmail AS RegistrationEmail, c.pwPostCode AS CentrePostcode, c.ShowOnMap, c.Long AS Longitude, @@ -253,7 +254,7 @@ FROM Centres AS c c.ServerSpaceBytes, c.CentreTypeID, ctp.CentreType, - c.pwEmail as RegistrationEmail + c.pwEmail as CentreEmail FROM Centres AS c INNER JOIN Regions AS r ON r.RegionID = c.RegionID INNER JOIN ContractTypes AS ct ON ct.ContractTypeID = c.ContractTypeId @@ -472,7 +473,7 @@ public void UpdateCentreDetailsForSuperAdmin( string centreName, int centreTypeId, int regionId, - string? centreEmail, + string? registrationEmail, string? ipPrefix, bool showOnMap ) @@ -482,7 +483,7 @@ bool showOnMap CentreName = @centreName, CentreTypeId = @centreTypeId, RegionId = @regionId, - AutoRegisterManagerEmail = @centreEmail, + AutoRegisterManagerEmail = @registrationEmail, IPPrefix = @ipPrefix, ShowOnMap = @showOnMap WHERE CentreId = @centreId", @@ -491,7 +492,7 @@ bool showOnMap centreName, centreTypeId, regionId, - centreEmail, + registrationEmail, ipPrefix, showOnMap, centreId diff --git a/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs b/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs index b6c03802f5..c5ff0af01f 100644 --- a/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs @@ -301,12 +301,13 @@ FROM FrameworkCollaborators fc AND aa3.UserID = (SELECT aa4.UserID FROM AdminAccounts aa4 WHERE aa4.ID = @adminId)) > 0 THEN 1 ELSE 0 END AS UserRole, - (SELECT fwr.ID + (SELECT TOP(1) fwr.ID FROM FrameworkCollaborators fc INNER JOIN AdminAccounts aa3 ON fc.AdminID = aa3.ID LEFT OUTER JOIN FrameworkReviews AS fwr ON fc.ID = fwr.FrameworkCollaboratorID AND fwr.Archived IS NULL AND fwr.ReviewComplete IS NULL WHERE fc.FrameworkID = fw.ID AND fc.IsDeleted = 0 - AND aa3.UserID = (SELECT aa4.UserID FROM AdminAccounts aa4 WHERE aa4.ID = @adminId)) AS FrameworkReviewID"; + AND aa3.UserID = (SELECT aa4.UserID FROM AdminAccounts aa4 WHERE aa4.ID = @adminId) + AND aa3.Active = 1 ORDER BY fwr.ID DESC) AS FrameworkReviewID"; private const string BrandedFrameworkFields = @",(SELECT BrandName @@ -863,7 +864,7 @@ public void RemoveCustomFlag(int flagId) public IEnumerable GetFrameworkCompetencyGroups(int frameworkId) { var result = connection.Query( - @"SELECT fcg.ID, fcg.CompetencyGroupID, cg.Name, fcg.Ordering, fc.ID, c.ID AS CompetencyID, c.Name, c.Description, fc.Ordering, COUNT(caq.AssessmentQuestionID) AS AssessmentQuestions + @"SELECT fcg.ID, fcg.CompetencyGroupID, cg.Name, cg.Description, fcg.Ordering, fc.ID, c.ID AS CompetencyID, c.Name, c.Description, fc.Ordering, COUNT(caq.AssessmentQuestionID) AS AssessmentQuestions ,(SELECT COUNT(*) FROM CompetencyLearningResources clr WHERE clr.CompetencyID = c.ID AND clr.RemovedDate IS NULL) AS CompetencyLearningResourcesCount FROM FrameworkCompetencyGroups AS fcg INNER JOIN CompetencyGroups AS cg ON fcg.CompetencyGroupID = cg.ID LEFT OUTER JOIN @@ -871,7 +872,7 @@ FROM FrameworkCompetencyGroups AS fcg INNER JOIN Competencies AS c ON fc.CompetencyID = c.ID LEFT OUTER JOIN CompetencyAssessmentQuestions AS caq ON c.ID = caq.CompetencyID WHERE (fcg.FrameworkID = @frameworkId) - GROUP BY fcg.ID, fcg.CompetencyGroupID, cg.Name, fcg.Ordering, fc.ID, c.ID, c.Name, c.Description, fc.Ordering + GROUP BY fcg.ID, fcg.CompetencyGroupID, cg.Name, cg.Description, fcg.Ordering, fc.ID, c.ID, c.Name, c.Description, fc.Ordering ORDER BY fcg.Ordering, fc.Ordering", (frameworkCompetencyGroup, frameworkCompetency) => { diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs index bb1dc7649f..7009b70e53 100644 --- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs @@ -193,6 +193,7 @@ CandidateAssessments AS CA LEFT OUTER JOIN SA.LinearNavigation, SA.UseDescriptionExpanders, SA.ManageOptionalCompetenciesPrompt, + CAST(CASE WHEN CA.SelfAssessmentProcessAgreed IS NOT NULL THEN 1 ELSE 0 END AS BIT) AS SelfAssessmentProcessAgreed, CAST(CASE WHEN SA.SupervisorSelfAssessmentReview = 1 OR SA.SupervisorResultsReview = 1 THEN 1 ELSE 0 END AS BIT) AS IsSupervised, CASE WHEN (SELECT COUNT(*) FROM SelfAssessmentSupervisorRoles WHERE SelfAssessmentID = @selfAssessmentId AND AllowDelegateNomination = 1) > 0 @@ -241,7 +242,7 @@ GROUP BY CA.LaunchCount, CA.SubmittedDate, SA.LinearNavigation, SA.UseDescriptionExpanders, SA.ManageOptionalCompetenciesPrompt, SA.SupervisorSelfAssessmentReview, SA.SupervisorResultsReview, SA.ReviewerCommentsLabel,SA.EnforceRoleRequirementsForSignOff, SA.ManageSupervisorsDescription,CA.NonReportable, - U.FirstName , U.LastName,SA.MinimumOptionalCompetencies", + U.FirstName , U.LastName,SA.MinimumOptionalCompetencies, CA.SelfAssessmentProcessAgreed", new { delegateUserId, selfAssessmentId } ); } @@ -325,6 +326,22 @@ public void SetCompleteByDate(int selfAssessmentId, int delegateUserId, DateTime } } + public void MarkProgressAgreed(int selfAssessmentId, int delegateUserId) + { + var numberOfAffectedRows = connection.Execute( + @"UPDATE CandidateAssessments SET SelfAssessmentProcessAgreed = GETDATE() + WHERE SelfAssessmentID = @selfAssessmentId AND DelegateUserID = @delegateUserId", + new { selfAssessmentId, delegateUserId } + ); + if (numberOfAffectedRows < 1) + { + logger.LogWarning( + "SelfAssessmentProcessAgreed not set as db update failed. " + + $"Self assessment id: {selfAssessmentId}, Delegate User id: {delegateUserId}" + ); + } + } + public void SetUpdatedFlag(int selfAssessmentId, int delegateUserId, bool status) { var numberOfAffectedRows = connection.Execute( diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs index 7312ed00c9..cdb71bb90d 100644 --- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs @@ -43,7 +43,7 @@ FROM CandidateAssessmentLearningLogItems AS calli INNER JOIN WHERE (NOT (lli.LearningResourceReferenceID IS NULL)) AND (calli.CandidateAssessmentID = ca.ID)) + (SELECT COUNT(*) AS FilteredLearning FROM FilteredLearningActivity AS fla - WHERE (CandidateId = da.ID)) AS LearningCompleted, + WHERE (CandidateId = da.ID)) AS LearningLaunched, (SELECT COUNT(*) AS LearningLaunched FROM CandidateAssessmentLearningLogItems AS calli INNER JOIN LearningLogItems AS lli ON calli.LearningLogItemID = lli.LearningLogItemID diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs index 324a1050a7..dd5dc5233c 100644 --- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs @@ -89,6 +89,7 @@ int competencyId void SetBookmark(int selfAssessmentId, int delegateUserId, string bookmark); + void MarkProgressAgreed(int selfAssessmentId, int delegateUserId); IEnumerable GetCandidateAssessments(int delegateUserId, int selfAssessmentId); // SelfAssessmentSupervisorDataService diff --git a/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs b/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs index 2cfd546720..e0218b3e55 100644 --- a/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs @@ -696,7 +696,7 @@ FROM SelfAssessmentResults AS sar2 AdminAccounts AS aa ON sd.SupervisorAdminID = aa.ID WHERE (sd.SupervisorAdminID = @adminId) AND (cas.Removed IS NULL) AND (sasv.Verified IS NULL) AND (sd.Removed IS NULL) AND (aa.CategoryID is null or sa.CategoryID = aa.CategoryID) - GROUP BY sa.ID, ca.ID, sd.ID, u.FirstName, u.LastName, sa.Name,cast(sasv.Requested as date)", new { adminId } + GROUP BY sa.ID, ca.ID, sd.ID, u.FirstName, u.LastName, sa.Name", new { adminId } ); } diff --git a/DigitalLearningSolutions.Data/Models/SelfAssessments/CurrentSelfAssessment.cs b/DigitalLearningSolutions.Data/Models/SelfAssessments/CurrentSelfAssessment.cs index ccd5af82f5..408c06a647 100644 --- a/DigitalLearningSolutions.Data/Models/SelfAssessments/CurrentSelfAssessment.cs +++ b/DigitalLearningSolutions.Data/Models/SelfAssessments/CurrentSelfAssessment.cs @@ -24,5 +24,6 @@ public class CurrentSelfAssessment : SelfAssessment public int? DelegateUserId { get; set; } public string? DelegateName { get; set; } public string? EnrolledByFullName { get; set; } + public bool SelfAssessmentProcessAgreed { get; set; } } } diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/LearningPortal/SelfAssessmentTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/LearningPortal/SelfAssessmentTests.cs index 977c4445e3..475f7f4a1f 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/LearningPortal/SelfAssessmentTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/LearningPortal/SelfAssessmentTests.cs @@ -870,5 +870,64 @@ public void SelfAssessmentOverview_Should_Return_View_With_Optional_Filter_Appli result.Should().BeViewResult().ModelAs().CompetencyGroups.ToList()[0].Count().Should().Be(1); } + + [Test] + public void SelfAssessment_should_return_description_view_when_process_agreed_or_not_supervised() + { + // Given + var selfAssessment = SelfAssessmentTestHelper.CreateDefaultSelfAssessment(); + selfAssessment.IsSupervised = false; // or set SelfAssessmentProcessAgreed = true + A.CallTo(() => selfAssessmentService.GetSelfAssessmentForCandidateById(DelegateUserId, SelfAssessmentId)) + .Returns(selfAssessment); + A.CallTo(() => selfAssessmentService.GetAllSupervisorsForSelfAssessmentId(SelfAssessmentId, DelegateUserId)) + .Returns(new List()); + var expectedModel = new SelfAssessmentDescriptionViewModel(selfAssessment, new List()); + + // When + var result = controller.SelfAssessment(SelfAssessmentId); + + // Then + result.Should().BeViewResult() + .WithViewName("SelfAssessments/SelfAssessmentDescription") + .Model.Should().BeEquivalentTo(expectedModel); + } + + [Test] + public void ProcessAgreed_should_return_agree_view_when_modelstate_invalid() + { + // Given + var model = new SelfAssessmentProcessViewModel { SelfAssessmentID = SelfAssessmentId }; + controller.ModelState.AddModelError("Test", "Error"); + + // When + var result = controller.ProcessAgreed(model); + + // Then + result.Should().BeViewResult() + .WithViewName("SelfAssessments/AgreeSelfAssessmentProcess") + .Model.Should().Be(model); + } + + [Test] + public void ProcessAgreed_should_mark_progress_and_redirect_to_self_assessment() + { + // Given + var selfAssessment = SelfAssessmentTestHelper.CreateDefaultSelfAssessment(); + var model = new SelfAssessmentProcessViewModel { SelfAssessmentID = SelfAssessmentId }; + A.CallTo(() => selfAssessmentService.GetSelfAssessmentForCandidateById(DelegateUserId, SelfAssessmentId)) + .Returns(selfAssessment); + + // When + var result = controller.ProcessAgreed(model); + + // Then + A.CallTo(() => selfAssessmentService.MarkProgressAgreed(SelfAssessmentId, DelegateUserId)) + .MustHaveHappenedOnceExactly(); + + result.Should().BeRedirectToActionResult() + .WithActionName("SelfAssessment") + .WithRouteValue("selfAssessmentId", SelfAssessmentId); + } + } } diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/SuperAdmin/CentresControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/SuperAdmin/CentresControllerTests.cs index 495ecf0cc3..18105d7dbb 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/SuperAdmin/CentresControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/SuperAdmin/CentresControllerTests.cs @@ -83,7 +83,7 @@ public void EditCentreDetails_updates_centre_and_redirects_with_successful_save( CentreTypeId = 1, CentreType = "NHS Organisation", RegionName = "National", - CentreEmail = "no.email@hee.nhs.uk", + RegistrationEmail = "no.email@hee.nhs.uk", IpPrefix = "12.33.4", ShowOnMap = true, RegionId = 13 @@ -99,7 +99,7 @@ public void EditCentreDetails_updates_centre_and_redirects_with_successful_save( model.CentreName, model.CentreTypeId, model.RegionId, - model.CentreEmail, + model.RegistrationEmail, model.IpPrefix, model.ShowOnMap)) .MustHaveHappenedOnceExactly(); diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/SupervisorController/SupervisorControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/SupervisorController/SupervisorControllerTests.cs index 7330042c4e..7dddbd0e55 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/SupervisorController/SupervisorControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/SupervisorController/SupervisorControllerTests.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; + using Microsoft.FeatureManagement; using NUnit.Framework; using System.Collections.Generic; using System.Linq; @@ -49,6 +50,7 @@ public class SupervisorControllerTests private IPdfService pdfService = null!; private SupervisorController controller = null!; private ICourseCategoriesService courseCategoriesService = null!; + private IFeatureManager featureManager = null!; [SetUp] public void Setup() @@ -73,6 +75,7 @@ public void Setup() candidateAssessmentDownloadFileService = A.Fake(); pdfService = A.Fake(); courseCategoriesService = A.Fake(); + featureManager = A.Fake(); A.CallTo(() => candidateAssessmentDownloadFileService.GetCandidateAssessmentDownloadFileForCentre(A._, A._, A._)) .Returns(new byte[] { }); @@ -108,7 +111,8 @@ public void Setup() candidateAssessmentDownloadFileService, clockUtility, pdfService, - courseCategoriesService + courseCategoriesService, + featureManager ); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = user } }; @@ -140,7 +144,8 @@ public void ExportCandidateAssessment_should_return_file_object_with_file_name_i candidateAssessmentDownloadFileService, clockUtility, pdfService, - courseCategoriesService + courseCategoriesService, + featureManager ); string expectedFileName = $"{((selfAssessmentName.Length > 30) ? selfAssessmentName.Substring(0, 30) : selfAssessmentName)} - {delegateName} - {clockUtility.UtcNow:yyyy-MM-dd}.xlsx"; diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/DelegateProgressControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/DelegateProgressControllerTests.cs index 4934bf16b3..d5f6b60186 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/DelegateProgressControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/DelegateProgressControllerTests.cs @@ -1,7 +1,5 @@ namespace DigitalLearningSolutions.Web.Tests.Controllers.TrackingSystem.Delegates { - using System; - using System.Collections.Generic; using DigitalLearningSolutions.Data.Models; using DigitalLearningSolutions.Data.Models.Courses; using DigitalLearningSolutions.Data.Models.CustomPrompts; @@ -15,8 +13,11 @@ using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Delegates.DelegateProgress; using FakeItEasy; using FluentAssertions.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using NUnit.Framework; + using System; + using System.Collections.Generic; public class DelegateProgressControllerTests { @@ -386,7 +387,11 @@ public void Delegate_removal_for_delegate_with_no_active_progress_returns_not_fo ); // Then - result.Should().BeNotFoundResult(); + result.Should() + .BeRedirectToActionResult() + .WithActionName("StatusCode") + .WithControllerName("LearningSolutions") + .WithRouteValue("code", 410); } [Test] diff --git a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs index bd8188b453..aa3153c81d 100644 --- a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs +++ b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs @@ -28,7 +28,7 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using System.IO; - + [ServiceFilter(typeof(RequireProcessAgreementFilter))] public partial class LearningPortalController { private const string CookieName = "DLSSelfAssessmentService"; @@ -75,9 +75,56 @@ public IActionResult SelfAssessment(int selfAssessmentId) delegateUserId ).ToList(); var model = new SelfAssessmentDescriptionViewModel(selfAssessment, supervisors); + return View("SelfAssessments/SelfAssessmentDescription", model); } + [Route("/LearningPortal/SelfAssessment/{selfAssessmentId:int}/AgreeProcess")] + public IActionResult AgreeSelfAssessmentProcess(int selfAssessmentId) + { + var delegateUserId = User.GetUserIdKnownNotNull(); + var selfAssessment = selfAssessmentService.GetSelfAssessmentForCandidateById(delegateUserId, selfAssessmentId); + + if (selfAssessment == null) + { + logger.LogWarning( + $"Attempt to display self assessment process for user {delegateUserId} with no self assessment" + ); + return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 }); + } + + var processmodel = new SelfAssessmentProcessViewModel() + { + SelfAssessmentID = selfAssessmentId, + Vocabulary = selfAssessment.Vocabulary, + VocabPlural = FrameworkVocabularyHelper.VocabularyPlural(selfAssessment.Vocabulary) + }; + return View("SelfAssessments/AgreeSelfAssessmentProcess", processmodel); + } + + [HttpPost("/LearningPortal/SelfAssessment/{selfAssessmentId:int}/AgreeProcess")] + public IActionResult ProcessAgreed(SelfAssessmentProcessViewModel model) + { + if (!ModelState.IsValid) + { + return View("SelfAssessments/AgreeSelfAssessmentProcess", model); + } + var delegateUserId = User.GetUserIdKnownNotNull(); + int selfAssessmentId = model.SelfAssessmentID; + var selfAssessment = selfAssessmentService.GetSelfAssessmentForCandidateById(delegateUserId, selfAssessmentId); + if (selfAssessment == null) + { + logger.LogWarning( + $"Attempt to display self assessment description for user {delegateUserId} with no self assessment" + ); + return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 }); + } + + selfAssessmentService.MarkProgressAgreed(selfAssessmentId, delegateUserId); + return RedirectToAction("SelfAssessment", new { selfAssessmentId }); + + } + [ServiceFilter(typeof(IsCentreAuthorizedSelfAssessment))] [Route("/LearningPortal/SelfAssessment/{selfAssessmentId:int}/{competencyNumber:int}")] public IActionResult SelfAssessmentCompetency(int selfAssessmentId, int competencyNumber) @@ -1532,22 +1579,6 @@ ManageOptionalCompetenciesViewModel model ); } } - var optionalCompetency = - (selfAssessmentService.GetCandidateAssessmentOptionalCompetencies(selfAssessmentId, delegateUserId)).Where(x => !x.IncludedInSelfAssessment); - if (optionalCompetency.Any()) - { - foreach (var optinal in optionalCompetency) - { - var selfAssessmentResults = selfAssessmentService.GetSelfAssessmentResultswithSupervisorVerificationsForDelegateSelfAssessmentCompetency(delegateUserId, selfAssessmentId, optinal.Id); - if (selfAssessmentResults.Any()) - { - foreach (var item in selfAssessmentResults) - { - selfAssessmentService.RemoveReviewCandidateAssessmentOptionalCompetencies(item.Id); - } - } - } - } if (model.GroupOptionalCompetenciesChecked != null) { var optionalCompetencies = @@ -1566,6 +1597,23 @@ ManageOptionalCompetenciesViewModel model } + var optionalCompetency = + (selfAssessmentService.GetCandidateAssessmentOptionalCompetencies(selfAssessmentId, delegateUserId)).Where(x => !x.IncludedInSelfAssessment); + if (optionalCompetency.Any()) + { + foreach (var optinal in optionalCompetency) + { + var selfAssessmentResults = selfAssessmentService.GetSelfAssessmentResultswithSupervisorVerificationsForDelegateSelfAssessmentCompetency(delegateUserId, selfAssessmentId, optinal.Id); + if (selfAssessmentResults.Any()) + { + foreach (var item in selfAssessmentResults) + { + selfAssessmentService.RemoveReviewCandidateAssessmentOptionalCompetencies(item.Id); + } + } + } + } + var recentResults = selfAssessmentService.GetMostRecentResults(selfAssessmentId, User.GetCandidateIdKnownNotNull()).ToList(); bool isVerificationPending = recentResults?.SelectMany(comp => comp.AssessmentQuestions).Where(quest => quest.Required) diff --git a/DigitalLearningSolutions.Web/Controllers/SuperAdmin/Centres/CentresController.cs b/DigitalLearningSolutions.Web/Controllers/SuperAdmin/Centres/CentresController.cs index 7d55d97729..8781d525bf 100644 --- a/DigitalLearningSolutions.Web/Controllers/SuperAdmin/Centres/CentresController.cs +++ b/DigitalLearningSolutions.Web/Controllers/SuperAdmin/Centres/CentresController.cs @@ -140,7 +140,7 @@ public IActionResult Index( { var baseUrl = config.GetAppRootPath(); var supportEmail = this.configService.GetConfigValue("SupportEmail"); - baseUrl = baseUrl+"/RegisterAdmin?centreId={centreId}".Replace("{centreId}", item.Centre.CentreId.ToString()); + baseUrl = baseUrl + "/RegisterAdmin?centreId={centreId}".Replace("{centreId}", item.Centre.CentreId.ToString()); Email welcomeEmail = this.passwordResetService.GenerateEmailInviteForCentreManager(centreEntity.Centre.CentreName, centreEntity.Centre.AutoRegisterManagerEmail, baseUrl, supportEmail); centreEntity.Centre.EmailInvite = "mailto:" + string.Join(",", welcomeEmail.To) + "?subject=" + welcomeEmail.Subject + "&body=" + welcomeEmail.Body.TextBody.Replace("&", "%26"); } @@ -295,7 +295,7 @@ public IActionResult EditCentreDetails(EditCentreDetailsSuperAdminViewModel mode model.CentreName.Trim(), model.CentreTypeId, model.RegionId, - model.CentreEmail, + model.RegistrationEmail, model.IpPrefix?.Trim(), model.ShowOnMap ); diff --git a/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs b/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs index 3d22fb71fb..4fac46e109 100644 --- a/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs +++ b/DigitalLearningSolutions.Web/Controllers/SupervisorController/Supervisor.cs @@ -27,7 +27,7 @@ public partial class SupervisorController { - public IActionResult Index() + public async Task IndexAsync() { var adminId = GetAdminId(); var dashboardData = supervisorService.GetDashboardDataForAdminId(adminId); @@ -35,11 +35,15 @@ public IActionResult Index() var reviewRequests = supervisorService.GetSupervisorDashboardToDoItemsForRequestedReviews(adminId); var supervisorDashboardToDoItems = Enumerable.Concat(signOffRequests, reviewRequests); var bannerText = GetBannerText(); + var tableauFlag = await featureManager.IsEnabledAsync(FeatureFlags.TableauSelfAssessmentDashboards); + var tableauQueryOverride = string.Equals(Request.Query["tableaulink"], "true", StringComparison.OrdinalIgnoreCase); + var showTableauLink = tableauFlag || tableauQueryOverride; var model = new SupervisorDashboardViewModel() { DashboardData = dashboardData, SupervisorDashboardToDoItems = supervisorDashboardToDoItems, - BannerText = bannerText + BannerText = bannerText, + ShowTableauLink = showTableauLink }; return View(model); } diff --git a/DigitalLearningSolutions.Web/Controllers/SupervisorController/SupervisorController.cs b/DigitalLearningSolutions.Web/Controllers/SupervisorController/SupervisorController.cs index 476000816f..e61cae3b7b 100644 --- a/DigitalLearningSolutions.Web/Controllers/SupervisorController/SupervisorController.cs +++ b/DigitalLearningSolutions.Web/Controllers/SupervisorController/SupervisorController.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; + using Microsoft.FeatureManagement; [Authorize(Policy = CustomPolicies.UserSupervisor)] public partial class SupervisorController : Controller @@ -28,7 +29,7 @@ public partial class SupervisorController : Controller private readonly IClockUtility clockUtility; private readonly IPdfService pdfService; private readonly ICourseCategoriesService courseCategoriesService; - + private readonly IFeatureManager featureManager; public SupervisorController( ISupervisorService supervisorService, ICommonService commonService, @@ -49,7 +50,8 @@ public SupervisorController( ICandidateAssessmentDownloadFileService candidateAssessmentDownloadFileService, IClockUtility clockUtility, IPdfService pdfService, - ICourseCategoriesService courseCategoriesService + ICourseCategoriesService courseCategoriesService, + IFeatureManager featureManager ) { this.supervisorService = supervisorService; @@ -68,6 +70,7 @@ ICourseCategoriesService courseCategoriesService this.clockUtility = clockUtility; this.pdfService = pdfService; this.courseCategoriesService = courseCategoriesService; + this.featureManager = featureManager; } private int GetCentreId() diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs index 2d4abaf2a7..5a59a12086 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/SelfAssessmentReports/SelfAssessmentReportsController.cs @@ -53,6 +53,7 @@ IFeatureManager featureManager this.selfAssessmentService = selfAssessmentService; this.featureManager = featureManager; } + [Route("/TrackingSystem/Centre/Reports/SelfAssessments")] public async Task IndexAsync() { var centreId = User.GetCentreId(); @@ -65,7 +66,7 @@ public async Task IndexAsync() return View(model); } [HttpGet] - [Route("DownloadDcsa")] + [Route("/TrackingSystem/Centre/Reports/DownloadDcsa")] public IActionResult DownloadDigitalCapabilityToExcel() { var centreId = User.GetCentreIdKnownNotNull(); @@ -78,7 +79,7 @@ public IActionResult DownloadDigitalCapabilityToExcel() ); } [HttpGet] - [Route("DownloadReport")] + [Route("/TrackingSystem/Centre/Reports/DownloadReport")] public IActionResult DownloadSelfAssessmentReport(int selfAssessmentId) { var centreId = User.GetCentreId(); @@ -92,8 +93,8 @@ public IActionResult DownloadSelfAssessmentReport(int selfAssessmentId) ); } [HttpGet] - [Route("TableauCompetencyDashboard")] - public async Task TableauCompetencyDashboardAsync() + [Route("/{source}/Reports/TableauCompetencyDashboard")] + public async Task TableauCompetencyDashboardAsync(string source = "TrackingSystem") { var userEmail = User.GetUserPrimaryEmail(); var adminId = User.GetAdminId(); @@ -101,6 +102,7 @@ public async Task TableauCompetencyDashboardAsync() var tableauFlag = await featureManager.IsEnabledAsync(FeatureFlags.TableauSelfAssessmentDashboards); var tableauQueryOverride = string.Equals(Request.Query["tableaulink"], "true", StringComparison.OrdinalIgnoreCase); var showTableauLink = tableauFlag || tableauQueryOverride; + ViewBag.Source = source; ViewBag.Email = userEmail; ViewBag.AdminId = adminId; ViewBag.SiteName = tableauSiteName; diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/DelegateProgressController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/DelegateProgressController.cs index b2fad6e72b..c6b70ef461 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/DelegateProgressController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Delegates/DelegateProgressController.cs @@ -275,10 +275,8 @@ public IActionResult ConfirmRemoveFromCourse( ) { var progress = progressService.GetDetailedCourseProgress(progressId); - if (progress == null) - { - return StatusCode((int)HttpStatusCode.Gone); - } + if (!courseService.DelegateHasCurrentProgress(progressId) || progress == null) + return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); var model = new RemoveFromCourseViewModel( progress, @@ -305,9 +303,7 @@ RemoveFromCourseViewModel model var progress = progressService.GetDetailedCourseProgress(progressId); if (!courseService.DelegateHasCurrentProgress(progressId) || progress == null) - { - return new NotFoundResult(); - } + return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 }); courseService.RemoveDelegateFromCourse( progress.DelegateId, diff --git a/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj b/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj index 866fdc7e6f..0057f8da8d 100644 --- a/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj +++ b/DigitalLearningSolutions.Web/DigitalLearningSolutions.Web.csproj @@ -68,7 +68,7 @@ - + @@ -79,7 +79,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/DigitalLearningSolutions.Web/ServiceFilter/RequireProcessAgreementFilter .cs b/DigitalLearningSolutions.Web/ServiceFilter/RequireProcessAgreementFilter .cs new file mode 100644 index 0000000000..67e11614a0 --- /dev/null +++ b/DigitalLearningSolutions.Web/ServiceFilter/RequireProcessAgreementFilter .cs @@ -0,0 +1,74 @@ +namespace DigitalLearningSolutions.Web.ServiceFilter +{ + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using DigitalLearningSolutions.Web.Services; + using DigitalLearningSolutions.Web.Helpers; + using Microsoft.Extensions.Logging; + + public class RequireProcessAgreementFilter : IActionFilter + { + private readonly ISelfAssessmentService selfAssessmentService; + private readonly ILogger logger; + + public RequireProcessAgreementFilter( + ISelfAssessmentService selfAssessmentService, + ILogger logger + ) + { + this.selfAssessmentService = selfAssessmentService; + this.logger = logger; + } + + public void OnActionExecuted(ActionExecutedContext context) { } + + public void OnActionExecuting(ActionExecutingContext context) + { + if (context.HttpContext.Request.Path.ToString().Contains("/LearningPortal/SelfAssessment/")) + { + if (!(context.Controller is Controller controller)) + { + return; + } + + if (!context.ActionArguments.ContainsKey("selfAssessmentId")) + { + return; + } + + var selfAssessmentId = int.Parse(context.ActionArguments["selfAssessmentId"].ToString()!); + var delegateUserId = controller.User.GetUserIdKnownNotNull(); + + var selfAssessment = selfAssessmentService.GetSelfAssessmentForCandidateById(delegateUserId, selfAssessmentId); + + if (selfAssessment == null) + { + logger.LogWarning( + $"Attempt to access self assessment {selfAssessmentId} by user {delegateUserId}, but no such assessment found" + ); + context.Result = new RedirectToActionResult("StatusCode", "LearningSolutions", new { code = 403 }); + return; + } + + var actionName = context.RouteData.Values["action"]?.ToString(); + if (actionName == "AgreeSelfAssessmentProcess" || actionName == "ProcessAgreed") + { + return; + } + + if (!selfAssessment.SelfAssessmentProcessAgreed && selfAssessment.IsSupervised) + { + logger.LogInformation( + $"Redirecting user {delegateUserId} to agree process page for self assessment {selfAssessmentId}" + ); + + context.Result = new RedirectToActionResult( + "AgreeSelfAssessmentProcess", + "LearningPortal", + new { selfAssessmentId } + ); + } + } + } + } +} diff --git a/DigitalLearningSolutions.Web/Services/CentresService.cs b/DigitalLearningSolutions.Web/Services/CentresService.cs index 6f367105b0..d76e112064 100644 --- a/DigitalLearningSolutions.Web/Services/CentresService.cs +++ b/DigitalLearningSolutions.Web/Services/CentresService.cs @@ -52,7 +52,7 @@ public void UpdateCentreDetailsForSuperAdmin( string centreName, int centreTypeId, int regionId, - string? centreEmail, + string? registrationEmail, string? ipPrefix, bool showOnMap ); @@ -227,9 +227,9 @@ public void UpdateCentreManagerDetails(int centreId, string firstName, string la return centresDataService.GetAllCentres(activeOnly); } - public void UpdateCentreDetailsForSuperAdmin(int centreId, string centreName, int centreTypeId, int regionId, string? centreEmail, string? ipPrefix, bool showOnMap) + public void UpdateCentreDetailsForSuperAdmin(int centreId, string centreName, int centreTypeId, int regionId, string? registrationEmail, string? ipPrefix, bool showOnMap) { - centresDataService.UpdateCentreDetailsForSuperAdmin(centreId, centreName, centreTypeId, regionId, centreEmail, ipPrefix, showOnMap); + centresDataService.UpdateCentreDetailsForSuperAdmin(centreId, centreName, centreTypeId, regionId, registrationEmail, ipPrefix, showOnMap); } public CentreSummaryForRoleLimits GetRoleLimitsForCentre(int centreId) diff --git a/DigitalLearningSolutions.Web/Services/SelfAssessmentService.cs b/DigitalLearningSolutions.Web/Services/SelfAssessmentService.cs index c9a50014fa..e0890e32ec 100644 --- a/DigitalLearningSolutions.Web/Services/SelfAssessmentService.cs +++ b/DigitalLearningSolutions.Web/Services/SelfAssessmentService.cs @@ -32,6 +32,7 @@ public interface ISelfAssessmentService void SetCompleteByDate(int selfAssessmentId, int delegateUserId, DateTime? completeByDate); + void MarkProgressAgreed(int selfAssessmentId, int delegateUserId); bool CanDelegateAccessSelfAssessment(int delegateUserId, int selfAssessmentId, int centreId); // Competencies @@ -216,6 +217,11 @@ public void SetCompleteByDate(int selfAssessmentId, int delegateUserId, DateTime selfAssessmentDataService.SetCompleteByDate(selfAssessmentId, delegateUserId, completeByDate); } + public void MarkProgressAgreed(int selfAssessmentId, int delegateUserId) + { + selfAssessmentDataService.MarkProgressAgreed(selfAssessmentId, delegateUserId); + } + public IEnumerable GetCandidateAssessmentResultsById(int candidateAssessmentId, int adminId, int? selfAssessmentResultId = null) { return selfAssessmentDataService.GetCandidateAssessmentResultsById(candidateAssessmentId, adminId, selfAssessmentResultId); diff --git a/DigitalLearningSolutions.Web/Startup.cs b/DigitalLearningSolutions.Web/Startup.cs index bf86444202..53451b05bd 100644 --- a/DigitalLearningSolutions.Web/Startup.cs +++ b/DigitalLearningSolutions.Web/Startup.cs @@ -582,6 +582,7 @@ private static void RegisterWebServiceFilters(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); } diff --git a/DigitalLearningSolutions.Web/Styles/index.scss b/DigitalLearningSolutions.Web/Styles/index.scss index 987c59e75b..c61029a83f 100644 --- a/DigitalLearningSolutions.Web/Styles/index.scss +++ b/DigitalLearningSolutions.Web/Styles/index.scss @@ -62,11 +62,7 @@ ul > li > ul > li { padding-top: 0; } -.nhsuk-button { - @include mq($until: tablet) { - margin-top: nhsuk-spacing(3); - } -} + input:invalid { border: $nhsuk-border-width-form-element-error solid $nhsuk-error-color; @@ -78,13 +74,7 @@ input[type=file] { } } -input[type=file].nhsuk-input--error { - height: 44px; - @include govuk-media-query($until: tablet) { - height: 40px; - } -} h1#page-heading { margin-bottom: 16px; @@ -98,9 +88,6 @@ h1#page-heading { display: flex; } -.nhsuk-main-wrapper { - flex: 1; -} .responsive-iframe-wrapper { height: 100%; @@ -121,10 +108,6 @@ h1#page-heading { padding-bottom: $iframe-padding-bottom * 1.5; } -.nhsuk-heading-xl.heading-margin-2 { - margin-bottom: nhsuk-spacing(2); -} - .responsive-iframe { width: 100%; height: 100%; @@ -147,25 +130,6 @@ h1#page-heading { margin-bottom: 0; } -.basic-summary-list__row:last-child .nhsuk-summary-list__key { - border: none; -} - -.basic-summary-list__row:last-child .nhsuk-summary-list__value { - border: none; - margin-bottom: 0; -} - -.nhsuk-summary-list__value { - @include govuk-media-query($until: tablet) { - word-break: break-word; - @include word-break-ie-fix; - } -} - -.nhsuk-tag { - text-align: center; -} .right-align-tag-column { padding: 0; @@ -211,10 +175,6 @@ ul.no-bullets { margin-bottom: 0; } -.nhsuk-details__text > .nhsuk-button { - margin-bottom: 0; -} - @mixin hidden-submit-ie-fix { // IE11 hack @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { @@ -335,10 +295,6 @@ ul.no-bullets { } } -.nhsuk-error-message.error-message--margin-bottom-1 { - margin-bottom: nhsuk-spacing(1); -} - .display-none { display: none; } diff --git a/DigitalLearningSolutions.Web/Styles/layout.scss b/DigitalLearningSolutions.Web/Styles/layout.scss index f815adafe4..62fe159b39 100644 --- a/DigitalLearningSolutions.Web/Styles/layout.scss +++ b/DigitalLearningSolutions.Web/Styles/layout.scss @@ -14,84 +14,14 @@ body { } #pagewrapper { - display: flex; - flex-direction: column; height: 100%; min-height: 100vh; } -#maincontentwrapper { - flex: 1 0 auto; - align-self: center; - width: 100%; - margin: 0; -} - footer { flex-shrink: 0; } -.nhsuk-header__navigation-item.selected { - a { - font-weight: bold; - } -} - -.nhsuk-header__logo { - @include mq($until: large-desktop) { - max-width: unset; - - .nhsuk-header__link--service { - align-items: center; - display: flex; - -ms-flex-align: center; - margin-bottom: 0; - width: auto; - } - - .nhsuk-header__service-name { - padding-left: nhsuk-spacing(3); - max-width: unset; - } - } -} -.nhsuk-navigation-container { - @include mq($until: tablet) { - margin-top: unset; - } -} - -body:not(.js-enabled) { - .nhsuk-header__menu { - display: none; - } - - #close-menu { - display: none; - } - - .nhsuk-header__navigation { - display: block; - } -} - -:not(.nhsuk-header--transactional) div.nhsuk-header__container { - @media (max-width: 40.0525em) { - margin: 0; - } -} - -.nhsuk-header--transactional { - .nhsuk-header__link--service { - width: auto; - height: auto; - - @include mq($from: large-desktop) { - display: flex; - } - } -} - .centre-brand-logo { float: right; max-width: 280px; @@ -102,14 +32,6 @@ body:not(.js-enabled) { } } -nav .nhsuk-width-container { - margin: 0 auto; -} - -nav, .nhsuk-header__navigation, #header-navigation { - border-bottom: 0; -} - .visual-separator { height: 8px; width: 100%; @@ -307,19 +229,7 @@ nav, .nhsuk-header__navigation, #header-navigation { clip: auto; } -.nhsuk-button--danger { - background-color: $color_nhsuk-red; - box-shadow: 0 4px 0 shade($color_nhsuk-red, 50%); - margin-bottom: 16px !important; - &:hover { - background-color: shade($color_nhsuk-red, 20%); - } - - &:active { - background-color: shade($color_nhsuk-red, 50%); - } -} .first-row td { border-top: 2px solid #d8dde0; @@ -330,12 +240,6 @@ nav, .nhsuk-header__navigation, #header-navigation { white-space: nowrap; } -.nhsuk-header__link--service { - @include mq($from: large-desktop) { - align-items: unset; - } -} - .header-beta { color: #c8e4ff; font-family: FrutigerLTW01-55Roman, Arial, sans-serif; @@ -347,15 +251,8 @@ nav, .nhsuk-header__navigation, #header-navigation { } } -.nhsuk-width-container, .nhsuk-header__navigation-list { - max-width: 1144px !important; - padding-left: $nhsuk-gutter !important; - padding-right: $nhsuk-gutter !important; -} -.nhsuk-width-container { - margin: auto !important; -} + @media only screen and (max-width: 767px) { .section-card-result { diff --git a/DigitalLearningSolutions.Web/Styles/learningPortal/selfassessmentcertificate.scss b/DigitalLearningSolutions.Web/Styles/learningPortal/selfassessmentcertificate.scss index ea0b873f76..166e37f81e 100644 --- a/DigitalLearningSolutions.Web/Styles/learningPortal/selfassessmentcertificate.scss +++ b/DigitalLearningSolutions.Web/Styles/learningPortal/selfassessmentcertificate.scss @@ -99,13 +99,12 @@ width: 100%; } -.pg-2 { +.certificate .pg-2 { background-color: white; width: 100%; position: relative; display: flex; flex-direction: column; - width: 100%; max-width: 210mm; min-height: 297mm; margin: 0; @@ -113,8 +112,9 @@ background: white; } -.pg-2 .body { +.certificate .pg-2 .body { padding: 50px; + flex: 1; } .activity { @@ -126,6 +126,7 @@ .activity p, ul { width: 100%; + word-break: break-word; } .activity ul { @@ -157,12 +158,6 @@ .certificate .pg-1 { width: 100%; } - - - - .pg-2 { - width: 100%; - } } .certificate h1 { @@ -378,18 +373,21 @@ .certificate .nhsuk-u-margin-bottom-2 { margin-bottom: 8px !important; } - } +} + @media screen and (max-width: 767px) { .certificate { - width: 95%; + width: 100%; justify-content: space-around; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; -webkit-print-color-adjust: exact; } + .certificate .pg-1 { - width: 95%; + width: 100%; } + .pg-2 { - width: 95%; + width: 100%; } } diff --git a/DigitalLearningSolutions.Web/Styles/nhsuk.scss b/DigitalLearningSolutions.Web/Styles/nhsuk.scss index c2b4c7239b..51b728e407 100644 --- a/DigitalLearningSolutions.Web/Styles/nhsuk.scss +++ b/DigitalLearningSolutions.Web/Styles/nhsuk.scss @@ -7,3 +7,151 @@ .nhsuk-hint { white-space: initial; } + + +@mixin word-break-ie-fix { + // IE11 hack + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + word-break: break-all; + } +} + +/* Items below moved from layout.scss or index.scss and deemed to be part of NHSE frontend */ + +body:not(.js-enabled) { + .nhsuk-header__menu { + display: none; + } + + #close-menu { + display: none; + } + + .nhsuk-header__navigation { + display: block; + } +} + +/* nhsuk-header__navigation-item paired with menu li items that become bold when you're on that page */ +/*
  • */ +.nhsuk-header__navigation-item.selected { + a { + font-weight: bold; + } +} + +/* stops blue menu overlapping white header, noticable because it's white, not as noticable in blue headers */ +.nhsuk-header--white .nhsuk-navigation-container { + @include mq($until: tablet) { + margin-top: unset; + } +} + +/* .nhsuk-header--transactional brought from live design-system, not in current implentation */ +.nhsuk-header--transactional { + + .nhsuk-header__link { + display: block; + height: 32px; + width: 80px + } + + .nhsuk-logo { + height: 32px; + width: 80px + } +} + +.nhsuk-main-wrapper { + flex: 1; +} + +.feedback-bar .nhsuk-width-container { + margin-bottom: 0; +} + +nav, .nhsuk-header__navigation, #header-navigation { + border-bottom: 0; +} + +.nhsuk-heading-xl.heading-margin-2 { + margin-bottom: nhsuk-spacing(2); +} + +.nhsuk-button { + @include mq($until: tablet) { + margin-top: nhsuk-spacing(3); + } +} + +.nhsuk-button--danger { + background-color: $color_nhsuk-red; + box-shadow: 0 4px 0 shade($color_nhsuk-red, 50%); + margin-bottom: 16px !important; + + &:hover { + background-color: shade($color_nhsuk-red, 20%); + } + + &:active { + background-color: shade($color_nhsuk-red, 50%); + } +} + +.nhsuk-details__text > .nhsuk-button { + margin-bottom: 0; +} + +input[type=file].nhsuk-input--error { + height: 44px; + + @include govuk-media-query($until: tablet) { + height: 40px; + } +} + +.nhsuk-error-message.error-message--margin-bottom-1 { + margin-bottom: nhsuk-spacing(1); +} + +.basic-summary-list__row:last-child .nhsuk-summary-list__key { + border: none; +} + +.basic-summary-list__row:last-child .nhsuk-summary-list__value { + border: none; + margin-bottom: 0; +} + +.nhsuk-summary-list__value { + @include govuk-media-query($until: tablet) { + word-break: break-word; + @include word-break-ie-fix; + } +} + +.nhsuk-tag { + text-align: center; +} + +.nhsuk-header__logo { + display: flex; + justify-content: space-between; +} + +@media (max-width: 600px) { + .nhsuk-header__transactional--logo { + flex-direction: column; + align-items: flex-start; + gap: 4px; + } +} + +/* + styles/index.scss has the following nhsuk dependancies - FGC 2/6/25 + .nhsuk-u-font-weight-normal; + .nhsuk-u-font-weight-bold; + .nhsuk-u-font-size-16; + .nhsuk-u-font-size-19; +*/ + diff --git a/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentDescriptionViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentDescriptionViewModel.cs index eb11408029..e4f7c1d11b 100644 --- a/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentDescriptionViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentDescriptionViewModel.cs @@ -18,6 +18,7 @@ public class SelfAssessmentDescriptionViewModel public readonly string VocabPlural; public readonly string? Vocabulary; public readonly bool NonReportable; + public bool SelfAssessmentProcessAgreed { get; set; } public SelfAssessmentDescriptionViewModel( CurrentSelfAssessment selfAssessment, @@ -37,6 +38,7 @@ List supervisors Vocabulary = selfAssessment.Vocabulary; VocabPlural = FrameworkVocabularyHelper.VocabularyPlural(selfAssessment.Vocabulary); NonReportable = selfAssessment.NonReportable; + SelfAssessmentProcessAgreed = selfAssessment.SelfAssessmentProcessAgreed; } public List Supervisors { get; set; } diff --git a/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentProcessViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentProcessViewModel.cs new file mode 100644 index 0000000000..0a63eda050 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/LearningPortal/SelfAssessments/SelfAssessmentProcessViewModel.cs @@ -0,0 +1,18 @@ +namespace DigitalLearningSolutions.Web.ViewModels.LearningPortal.SelfAssessments +{ + using System.Collections.Generic; + using DigitalLearningSolutions.Data.Models.SelfAssessments; + using DigitalLearningSolutions.Web.Attributes; + using DigitalLearningSolutions.Web.Helpers; + + public class SelfAssessmentProcessViewModel + { + public int SelfAssessmentID { get; set; } + [BooleanMustBeTrue(ErrorMessage = "Please tick the checkbox to confirm that you understand and agree to the self-assessment process")] + public bool ActionConfirmed { get; set; } + + public string? VocabPlural { get; set; } + public string? Vocabulary { get; set; } + + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Centres/EditCentreDetailsSuperAdminViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Centres/EditCentreDetailsSuperAdminViewModel.cs index 8218658e96..9000f85285 100644 --- a/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Centres/EditCentreDetailsSuperAdminViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/SuperAdmin/Centres/EditCentreDetailsSuperAdminViewModel.cs @@ -19,6 +19,7 @@ public EditCentreDetailsSuperAdminViewModel(Centre centre) IpPrefix = centre.IpPrefix?.Trim(); ShowOnMap = centre.ShowOnMap; RegionId = centre.RegionId; + RegistrationEmail = centre.RegistrationEmail; } public int CentreId { get; set; } @@ -37,5 +38,10 @@ public EditCentreDetailsSuperAdminViewModel(Centre centre) [RegularExpression(@"^[\d.,\s]+$", ErrorMessage = "IP Prefix can contain only digits, stops, commas and spaces")] public string? IpPrefix { get; set; } public bool ShowOnMap { get; set; } + + [MaxLength(250, ErrorMessage = "Email must be 250 characters or fewer")] + [EmailAddress(ErrorMessage = "Enter an email in the correct format, like name@example.com")] + [NoWhitespace(ErrorMessage = "Email must not contain any whitespace characters")] + public string? RegistrationEmail { get; set; } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/Supervisor/SupervisorDashboardViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Supervisor/SupervisorDashboardViewModel.cs index 301e96fe77..067803fd78 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Supervisor/SupervisorDashboardViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Supervisor/SupervisorDashboardViewModel.cs @@ -8,5 +8,6 @@ public class SupervisorDashboardViewModel public string? BannerText; public DashboardData DashboardData { get; set; } public IEnumerable SupervisorDashboardToDoItems { get; set; } + public bool ShowTableauLink { get; set; } = false; } } diff --git a/DigitalLearningSolutions.Web/Views/Frameworks/Developer/FrameworkPrintLayout.cshtml b/DigitalLearningSolutions.Web/Views/Frameworks/Developer/FrameworkPrintLayout.cshtml index 7d69325447..6b22a8fe8d 100644 --- a/DigitalLearningSolutions.Web/Views/Frameworks/Developer/FrameworkPrintLayout.cshtml +++ b/DigitalLearningSolutions.Web/Views/Frameworks/Developer/FrameworkPrintLayout.cshtml @@ -57,7 +57,7 @@ if (frameworkCompetencyGroup.Description != null) {

    - @frameworkCompetencyGroup.Description + @Html.Raw(frameworkCompetencyGroup.Description)

    } diff --git a/DigitalLearningSolutions.Web/Views/Frameworks/Shared/_CompetencyGroupCard.cshtml b/DigitalLearningSolutions.Web/Views/Frameworks/Shared/_CompetencyGroupCard.cshtml index f8c35d8898..a94b6562ff 100644 --- a/DigitalLearningSolutions.Web/Views/Frameworks/Shared/_CompetencyGroupCard.cshtml +++ b/DigitalLearningSolutions.Web/Views/Frameworks/Shared/_CompetencyGroupCard.cshtml @@ -3,8 +3,12 @@
    - - @Model.FrameworkCompetencyGroup.Name (@Model.FrameworkCompetencyGroup.FrameworkCompetencies.Count() @ViewData["VocabPlural"].ToString().ToLower()) + @Model.FrameworkCompetencyGroup.Name (@Model.FrameworkCompetencyGroup.FrameworkCompetencies.Count() @ViewData["VocabPlural"].ToString().ToLower()) + +
    +
    + + @Html.Raw(@Model.FrameworkCompetencyGroup.Description) @if (Model.CanModify) { diff --git a/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/AgreeSelfAssessmentProcess.cshtml b/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/AgreeSelfAssessmentProcess.cshtml new file mode 100644 index 0000000000..b3f460f4e1 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/AgreeSelfAssessmentProcess.cshtml @@ -0,0 +1,57 @@ +@using DigitalLearningSolutions.Web.Extensions +@using DigitalLearningSolutions.Web.ViewModels.LearningPortal.SelfAssessments; +@model SelfAssessmentProcessViewModel +@{ + var errorHasOccurred = !ViewData.ModelState.IsValid; + ViewData["Title"] = (errorHasOccurred ? "Error: " : "") + "Agree To Process"; + ViewData["Application"] = "LearningPortal"; + ViewData["HeaderPathName"] = "LearningPortal"; +} + +@section NavMenuItems { + +} + +
    +
    + @if (errorHasOccurred) + { + + } +

    How are @Model.VocabPlural?.ToLower() and frameworks assessed?

    + +

    The process is learner-driven but you must be assessed by a supervisor before completing a self-assessment.

    + +

    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:

    + +
      +
    1. Your supervisor can sign-off this @Model.Vocabulary?.ToLower() based on their competence or department requirements.
    2. +
    3. You have been formally assessed, this could be through observation, discussion, or another agreed method with your supervisor.
    4. +
    5. You have reflected on the @Model.Vocabulary?.ToLower() description and feel you meet the requirements.
    6. +
    7. If you don’t fully meet the requirements, you can still document your progress and next steps for future assessment.
    8. +
    + +
    +
    + +
    + + + + +
    + +
    +
    diff --git a/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/CompetencySelfAssessmentCertificate.cshtml b/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/CompetencySelfAssessmentCertificate.cshtml index 9c3c3152c0..67fde6c257 100644 --- a/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/CompetencySelfAssessmentCertificate.cshtml +++ b/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/CompetencySelfAssessmentCertificate.cshtml @@ -11,7 +11,7 @@ padding: 0 0; } - .nhsuk-width-container { + .nhsuk-grid-row { margin: 0; } @@ -49,8 +49,8 @@ {
  • + asp-action="SelfAssessmentOverview" asp-route-vocabulary="@Model.VocabPlural" + asp-route-selfAssessmentId="@Model.CompetencySelfAssessmentCertificates.SelfAssessmentID"> < Back
  • @@ -59,9 +59,9 @@ {
  • + asp-action="ReviewDelegateSelfAssessment" + asp-route-supervisorDelegateId="@Model.LoggedInSupervisorDelegateId" + asp-route-candidateAssessmentId="@Model.CompetencySelfAssessmentCertificates.CandidateAssessmentID"> < Back
  • @@ -76,15 +76,15 @@

    Certificate

    @Model.CompetencySelfAssessmentCertificates.SelfAssessment - @Model.CompetencySelfAssessmentCertificates.LearnerName

    - @if (Model.Vocabulary == "Proficiencies") + @if (Model.Vocabulary == "Proficiencies") { - Download certificate - + asp-controller="LearningPortal" + asp-route-candidateAssessmentId="@Model.CompetencySelfAssessmentCertificates.CandidateAssessmentID" + asp-action="DownloadCertificate" + role="button"> + Download certificate + } @if (Model.Vocabulary == "ProfileAssessment") { @@ -99,9 +99,9 @@
    -

    +
    - +
    diff --git a/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/_Layout.cshtml b/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/_Layout.cshtml index a92ae4ce53..da2b85dc86 100644 --- a/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/_Layout.cshtml +++ b/DigitalLearningSolutions.Web/Views/LearningPortal/SelfAssessments/_Layout.cshtml @@ -51,24 +51,26 @@ { @await Html.PartialAsync("~/Views/Shared/_GoogleTagManagerBodyTagJs.cshtml") } + + Skip to main content
    - - - Skip to main content - -
    -
    - -
    - @ViewData["SelfAssessmentTitle"] - Beta +
  • + +@if (Model.ShowTableauLink) +{ +

    Progress reports

    + +} diff --git a/DigitalLearningSolutions.Web/Views/Supervisor/ReviewSelfAssessment.cshtml b/DigitalLearningSolutions.Web/Views/Supervisor/ReviewSelfAssessment.cshtml index ad7d7bdb49..b1362017bd 100644 --- a/DigitalLearningSolutions.Web/Views/Supervisor/ReviewSelfAssessment.cshtml +++ b/DigitalLearningSolutions.Web/Views/Supervisor/ReviewSelfAssessment.cshtml @@ -177,12 +177,10 @@ @if (!string.IsNullOrWhiteSpace(competency.Description) && !competency.AlwaysShowDescription) {
    - -

    + @competency.Name -

    @(Html.Raw(@competency.Description)) @@ -191,9 +189,9 @@ } else { -

    +

    @competency.Name -

    +

    @if (!string.IsNullOrWhiteSpace(competency.Description)) {

    diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/Index.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/Index.cshtml index 67792bf87d..fa25c11a8a 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/Index.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/Index.cshtml @@ -4,7 +4,7 @@ @model SelfAssessmentReportsViewModel @{ ViewData["Title"] = "Self assessment reports"; - var routeData = new Dictionary(); + var routeData = (new Dictionary() { { "source", "TrackingSystem" } }); if (string.Equals(Context.Request.Query["tableaulink"], "true", StringComparison.OrdinalIgnoreCase)) { diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/TableauCompetencyDashboard.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/TableauCompetencyDashboard.cshtml index a5743efeea..97fd535837 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/TableauCompetencyDashboard.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/SelfAssessmentReports/TableauCompetencyDashboard.cshtml @@ -11,7 +11,7 @@ ViewData["Title"] = "Supervised self assessments dashboard"; }