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 */
+/*