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/202503111500_AddLastAccessedColumn.cs b/DigitalLearningSolutions.Data.Migrations/202503111500_AddLastAccessedColumn.cs
new file mode 100644
index 0000000000..95a0eb6020
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202503111500_AddLastAccessedColumn.cs
@@ -0,0 +1,26 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+
+ [Migration(202503111500)]
+ public class AddLastAccessedColumn : Migration
+ {
+ public override void Up()
+ {
+ Alter.Table("Users").AddColumn("LastAccessed").AsDateTime().Nullable();
+ Alter.Table("DelegateAccounts").AddColumn("LastAccessed").AsDateTime().Nullable();
+ Alter.Table("AdminAccounts").AddColumn("LastAccessed").AsDateTime().Nullable();
+
+ Execute.Sql("UPDATE u SET LastAccessed = (SELECT MAX(s.LoginTime) FROM DelegateAccounts da JOIN Sessions s ON da.ID = s.CandidateId WHERE da.UserID = u.ID) FROM users u;");
+ Execute.Sql("UPDATE da SET LastAccessed = (SELECT MAX(s.LoginTime) FROM Sessions s WHERE s.CandidateId = da.ID) FROM DelegateAccounts da;");
+ Execute.Sql("UPDATE da SET LastAccessed = (SELECT ca.LastAccessed FROM CandidateAssessments ca WHERE ca.ID = da.ID) FROM DelegateAccounts da where da.LastAccessed IS NULL;");
+ Execute.Sql("UPDATE AA SET LastAccessed = (SELECT MAX(AdS.LoginTime) FROM AdminSessions AdS WHERE AdS.AdminID = AA.ID) FROM AdminAccounts AA;");
+ }
+ public override void Down()
+ {
+ Delete.Column("LastAccessed").FromTable("Users");
+ Delete.Column("LastAccessed").FromTable("DelegateAccounts");
+ Delete.Column("LastAccessed").FromTable("AdminAccounts");
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/202503121538_Alter_SendExpiredTBCReminders.cs b/DigitalLearningSolutions.Data.Migrations/202503121538_Alter_SendExpiredTBCReminders.cs
new file mode 100644
index 0000000000..315960fb4f
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202503121538_Alter_SendExpiredTBCReminders.cs
@@ -0,0 +1,19 @@
+
+
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+
+ [Migration(202503121538)]
+ public class Alter_SendExpiredTBCReminders : Migration
+ {
+ public override void Up()
+ {
+ Execute.Sql(Properties.Resources.TD_5412_Alter_SendExpiredTBCReminders_Up);
+ }
+ public override void Down()
+ {
+ Execute.Sql(Properties.Resources.TD_5412_Alter_SendExpiredTBCReminders_Down);
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/2025041161520_Alter_SendExpiredTBCReminders_AppendCourseName.cs b/DigitalLearningSolutions.Data.Migrations/2025041161520_Alter_SendExpiredTBCReminders_AppendCourseName.cs
new file mode 100644
index 0000000000..02bcb19b10
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/2025041161520_Alter_SendExpiredTBCReminders_AppendCourseName.cs
@@ -0,0 +1,19 @@
+
+
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+
+ [Migration(2025041161520)]
+ public class Alter_SendExpiredTBCReminders_AppendCourseName : Migration
+ {
+ public override void Up()
+ {
+ Execute.Sql(Properties.Resources.TD_5514_Alter_SendExpiredTBCReminders_Up);
+ }
+ public override void Down()
+ {
+ Execute.Sql(Properties.Resources.TD_5514_Alter_SendExpiredTBCReminders_Down);
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/202504241517_AddFieldIsPrimaryToSelfAssessmentFrameworksTable.cs b/DigitalLearningSolutions.Data.Migrations/202504241517_AddFieldIsPrimaryToSelfAssessmentFrameworksTable.cs
new file mode 100644
index 0000000000..fe1e096135
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202504241517_AddFieldIsPrimaryToSelfAssessmentFrameworksTable.cs
@@ -0,0 +1,12 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+ [Migration(202504241517)]
+ public class AddFieldIsPrimaryToSelfAssessmentFrameworksTable : AutoReversingMigration
+ {
+ public override void Up()
+ {
+ Alter.Table("SelfAssessmentFrameworks").AddColumn("IsPrimary").AsBoolean().NotNullable().WithDefaultValue(1);
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/202504281045_Alter_ReorderFrameworkCompetency.cs b/DigitalLearningSolutions.Data.Migrations/202504281045_Alter_ReorderFrameworkCompetency.cs
new file mode 100644
index 0000000000..2759e056ac
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202504281045_Alter_ReorderFrameworkCompetency.cs
@@ -0,0 +1,19 @@
+
+
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+
+ [Migration(202504281045)]
+ public class Alter_ReorderFrameworkCompetency : Migration
+ {
+ public override void Up()
+ {
+ Execute.Sql(Properties.Resources.TD_5447_Alter_ReorderFrameworkCompetency_Up);
+ }
+ public override void Down()
+ {
+ Execute.Sql(Properties.Resources.TD_5447_Alter_ReorderFrameworkCompetency_Down);
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/202504281115_UpdateFrameworkCompetenciesOrdering.cs b/DigitalLearningSolutions.Data.Migrations/202504281115_UpdateFrameworkCompetenciesOrdering.cs
new file mode 100644
index 0000000000..e64cdd3f6f
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202504281115_UpdateFrameworkCompetenciesOrdering.cs
@@ -0,0 +1,21 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+ [Migration(202504281115)]
+ public class UpdateFrameworkCompetenciesOrdering : ForwardOnlyMigration
+ {
+ public override void Up()
+ {
+ Execute.Sql(@"WITH Ranked AS (
+ SELECT ID,
+ ROW_NUMBER() OVER (PARTITION BY FrameworkID ORDER BY SysStartTime) AS NewOrder
+ FROM FrameworkCompetencies
+ Where FrameworkCompetencyGroupID is null
+ )
+ UPDATE fc
+ SET fc.Ordering = r.NewOrder
+ FROM FrameworkCompetencies fc
+ JOIN Ranked r ON fc.ID = r.ID;");
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/202504290900_UpdateNotifications.cs b/DigitalLearningSolutions.Data.Migrations/202504290900_UpdateNotifications.cs
new file mode 100644
index 0000000000..a662824279
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202504290900_UpdateNotifications.cs
@@ -0,0 +1,14 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+
+ [Migration(202504290900)]
+ public class UpdateNotifications : ForwardOnlyMigration
+ {
+ public override void Up()
+ {
+ Execute.Sql(@$"UPDATE Notifications SET NotificationName = 'Completed course follow-up feedback requests' where NotificationID = 13");
+ }
+
+ }
+}
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/202507030918_CreateOrAlterGetSelfAssessmentReport.cs b/DigitalLearningSolutions.Data.Migrations/202507030918_CreateOrAlterGetSelfAssessmentReport.cs
new file mode 100644
index 0000000000..daa85c4f15
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202507030918_CreateOrAlterGetSelfAssessmentReport.cs
@@ -0,0 +1,17 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+ [Migration(20250703091)]
+ public class CreateOrAlterGetSelfAssessmentReport : Migration
+ {
+ public override void Up()
+ {
+ Execute.Sql(Properties.Resources.TD_5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP);
+ }
+ public override void Down()
+ {
+ Execute.Sql("DROP PROCEDURE IF EXISTS [dbo].[usp_GetSelfAssessmentReport]");
+ Execute.Sql("DROP FUNCTION IF EXISTS [dbo].[GetOtherCentresForSelfAssessmentTVF]");
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/202507040803_FixCreateOrAlterGetSelfAssessmentReport.cs b/DigitalLearningSolutions.Data.Migrations/202507040803_FixCreateOrAlterGetSelfAssessmentReport.cs
new file mode 100644
index 0000000000..73aab1d8a0
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202507040803_FixCreateOrAlterGetSelfAssessmentReport.cs
@@ -0,0 +1,12 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+ [Migration(202507040803)]
+ public class FixCreateOrAlterGetSelfAssessmentReport : ForwardOnlyMigration
+ {
+ public override void Up()
+ {
+ Execute.Sql(Properties.Resources.TD_5759_CreateOrAlterSelfAssessmentReportSPandTVF_Fix_UP);
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/202507111455_UpdatePrivacyNoticeAcceptableUse.cs b/DigitalLearningSolutions.Data.Migrations/202507111455_UpdatePrivacyNoticeAcceptableUse.cs
new file mode 100644
index 0000000000..b9dc84ab7e
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202507111455_UpdatePrivacyNoticeAcceptableUse.cs
@@ -0,0 +1,430 @@
+using FluentMigrator;
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ [Migration(202507111455)]
+ public class UpdatePrivacyNoticeAcceptableUse : Migration
+ {
+ public override void Up()
+ {
+ var PrivacyPolicyNew = @"
PRIVACY NOTICE
This page explains our privacy policy and how we will use and protect any information about you that you give to us or that we collate when you visit this website, or undertake employment with NHS England (NHSE or we/us/our), or participate in any NHSE sponsored training, education and development including via any of our training platform websites (Training).
This privacy notice is intended to provide transparency regarding what personal data NHSE may hold about you, how it will be processed and stored, how long it will be retained and who may have access to your data.
Personal data is any information relating to an identified or identifiable living person (known as the data subject). An identifiable person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number or factors specific to the physical, genetic or mental identity of that person, for example.
1 OUR ROLE IN THE NHS
We are here to improve the quality of healthcare for the people and patients of England through education, training and lifelong development of staff and appropriate planning of the workforce required to deliver healthcare services in England.
We aim to enable high quality, effective, compassionate care and to identify the right people with the right skills and the right values. All the information we collect is to support these objectives.
2 IMPORTANT INFORMATION
NHSE is the data controller in respect of any personal data it holds concerning trainees in Training, individuals employed by NHSE and individuals accessing NHSE’s website.
We have appointed a data protection officer (DPO) who is responsible for overseeing questions in relation to this privacy policy. If you have any questions about this privacy policy or our privacy practices, want to know more about how your information will be used, or make a request to exercise your legal rights, please contact our DPO in the following ways:
Postal address: NHS England of Skipton House, 80 London Road, London SE1 6LH
3 WHAT THIS PRIVACY STATEMENT COVERS
This privacy statement only covers the processing of personal data by NHSE that NHSE has obtained from data subjects accessing any of NHSE’s websites and from its provision of services and exercise of functions. It does not cover the processing of data by any sites that can be linked to or from NHSE’s websites, so you should always be aware when you are moving to another site and read the privacy statement on that website.
When providing NHSE with any of your personal data for the first time, for example, when you take up an appointment with NHSE or when you register for any Training, you will be asked to confirm that you have read and accepted the terms of this privacy statement. A copy of your acknowledgement will be retained for reference.
If our privacy policy changes in any way, we will place an updated version on this page. Regularly reviewing the page ensures you are always aware of what information we collect, how we use it and under what circumstances, if any, we will share it with other parties.
4 WHY NHSE COLLECTS YOUR PERSONAL DATA
Personal data may be collected from you via the recruitment process, when you register and/or create an account for any Training, during your Annual Review of Competence Progression or via NHSE’s appraisal process. Personal data may also be obtained from Local Education Providers or employing organisations in connection with the functions of NHSE.
Your personal data is collected and processed for the purposes of and in connection with the functions that NHSE performs with regard to Training and planning. The collection and processing of such data is necessary for the purposes of those functions.
A full copy of our notification to the Information Commissioner’s Office (ICO) (the UK regulator for data protection issues), can be found on the ICO website here: www.ico.org.uk by searching NHSE’s ICO registration number, which is Z2950066.
In connection with Training, NHSE collects and uses your personal information for the following purposes:
to manage your Training and programme, including allowing you to access your own learning history;
to quality assure Training programmes and ensure that standards are maintained, including gathering feedback or input on the service, content, or layout of the Training and customising the content and/or layout of the Training;
to identify workforce planning targets;
to maintain patient safety through the management of performance concerns;
to comply with legal and regulatory responsibilities including revalidation;
to contact you about Training updates, opportunities, events, surveys and information that may be of interest to you;
transferring your Training activity records for programmes to other organisations involved in medical training in the healthcare sector. These organisations include professional bodies that you may be a member of, such as a medical royal college or foundation school; or employing organisations, such as trusts;
making your Training activity records visible to specific named individuals, such as tutors, to allow tutors to view their trainees’ activity. We would seek your explicit consent before authorising anyone else to view your records;
providing anonymous, summarised data to partner organisations, such as professional bodies; or local organisations, such as strategic health authorities or trusts;
for NHSE internal review;
to provide HR related support services and Training to you, for clinical professional learner recruitment;
to promote our services;
to monitor our own accounts and records;
to monitor our work, to report on progress made; and
to let us fulfil our statutory obligations and statutory returns as set by the Department of Health and the law (for example complying with NHSE’s legal obligations and regulatory responsibilities under employment law).
Further information about our use of your personal data in connection with Training can be found in ’A Reference Guide for Postgraduate Foundation and Specialty Training in the UK’, published by the Conference of Postgraduate Medical Deans of the United Kingdom and known as the ‘Gold Guide’, which can be found here: https://www.copmed.org.uk/gold-guide.
5 TYPES OF PERSONAL DATA COLLECTED BY NHSE
The personal data that NHSE collects when you register for Training enables the creation of an accurate user profile/account, which is necessary for reporting purposes and to offer Training that is relevant to your needs.
The personal data that is stored by NHSE is limited to information relating to your work, such as your job role, place of work, and membership number for a professional body (e.g. your General Medical Council number). NHSE will never ask for your home address or any other domestic information.
When accessing Training, you will be asked to set up some security questions, which may contain personal information. These questions enable you to log in if you forget your password and will never be used for any other purpose. The answers that you submit when setting up these security questions are encrypted in the database so no one can view what has been entered, not even NHSE administrators.
NHSE also store a record of some Training activity, including upload and download of Training content, posts on forums or other communication media, and all enquires to the service desks that support the Training.
If you do not provide personal data that we need from you when requested, we may not be able to provide services (such as Training) to you. In this case, we may have to cancel such service, but we will notify you at the time if this happens.
6 COOKIES
When you access NHSE’s website and Training, we want to make them easy, useful and reliable. This sometimes involves placing small amounts of limited information on your device (such as your computer or mobile phone). These small files are known as cookies, and we ask you to agree to their usage in accordance with ICO guidance.
These cookies are used to improve the services (including the Training) we provide you through, for example:
enabling a service to recognise your device, so you do not have to give the same information several times during one task (e.g. we use a cookie to remember your username if you check the ’Remember Me’ box on a log in page);
recognising that you may already have given a username and password, so you do not need to do it for every web page requested;
measuring how many people are using services, so they can be made easier to use and there is enough capacity to ensure they are fast; and
analysing anonymised data to help us understand how people interact with services so we can make them better.
We use a series of cookies to monitor website speed and usage, as well as to ensure that any preferences you have selected previously are the same when you return to our website. Please visit our cookie policies page to understand the cookies that we use: https://www.dls.nhs.uk/v2/CookieConsent/CookiePolicy
Most cookies applied when accessing Training are used to keep track of your input when filling in online forms, known as session-ID cookies, which are exempt from needing consent as they are deemed essential for using the website or Training they apply to. Some cookies, like those used to measure how you use the Training, are not needed for our website to work. These cookies can help us improve the Training, but we’ll only use them if you say it’s OK. We’ll use a cookie to save your settings.
On a number of pages on our website or Training, we use ’plug-ins’ or embedded media. For example, we might embed YouTube videos. Where we have used this type of content, the suppliers of these services may also set cookies on your device when you visit their pages. These are known as ’third-party’ cookies. To opt-out of third-parties collecting any data regarding your interaction on our website, please refer to their websites for further information.
We will not use cookies to collect personal data about you. However, if you wish to restrict or block the cookies which are set by our websites or Training, or indeed any other website, you can do this through your browser settings. The ’Help’ function within your browser should tell you how. Alternatively, you may wish to visit www.aboutcookies.org which contains comprehensive information on how to do this on a wide variety of browsers. You will also find details on how to delete cookies from your machine as well as more general information about cookies. Please be aware that restricting cookies may impact on the functionality of our website.
7 LEGAL BASIS FOR PROCESSING
The retained EU law version of the General Data Protection Regulation ((EU) 2016/679) (UK GDPR) requires that data controllers and organisations that process personal data demonstrate compliance with its provisions. This involves publishing our basis for lawful processing of personal data.
As personal data is processed for the purposes of NHSE’s statutory functions, NHSE’s legal bases for the processing of personal data as listed in Article 6 of the UK GDPR are as follows:
6(1)(a) – Consent of the data subject
6(1)(b) – Processing is necessary for the performance of a contract to which the data subject is party or in order to take steps at the request of the data subject prior to entering into a contract
6(1)(c) – Processing is necessary for compliance with a legal obligation
6(1)(e) – Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority vested in the controller
Where NHSE processes special categories of personal data, its additional legal bases for processing such data as listed in Article 9 of the UK GDPR are as follows:
9(2)(a) – Explicit consent of the data subject
9(2)(b) – Processing is necessary for the purposes of carrying out the obligations and exercising specific rights of the controller or of the data subject in the field of employment and social security and social protection law
9(2)(f) – Processing is necessary for the establishment, exercise or defence of legal claims or whenever courts are acting in their judicial capacity
9(2)(g) – Processing is necessary for reasons of substantial public interest
9(2)(h) – Processing is necessary for the purposes of occupational medicine, for the assessment of the working capacity of the employee, or the management of health and social care systems and services
9(2)(j) – Processing is necessary for archiving purposes in the public interest, scientific or historical research purposes or statistical purposes
Special categories of personal data include data relating to racial or ethnic origin, political opinions, religious beliefs, sexual orientation and data concerning health.
Please note that not all of the above legal bases will apply for each type of processing activity that NHSE may undertake. However, when processing any personal data for any particular purpose, one or more of the above legal bases will apply.
We may seek your consent for some processing activities, for example for sending out invitations to you to Training events and sending out material from other government agencies. If you do not give consent for us to use your data for these purposes, we will not use your data for these purposes, but your data may still be retained by us and used by us for other processing activities based on the above lawful conditions for processing set out above.
8 INFORMATION THAT WE MAY NEED TO SEND YOU
We may occasionally have to send you information from NHSE, the Department of Health, other public authorities and government agencies about matters of policy where those policy issues impact on Training, workforce planning, or other matters related to NHSE. This is because NHSE is required by statute to exercise functions of the Secretary of State in respect of Training and workforce planning. If you prefer, you can opt out of receiving information about general matters of policy impacting on Training and workforce planning by contacting your Local Office recruitment lead or tel@hee.nhs.uk. The relevant Local Office or a representative from the relevant training platform website will provide you with further advice and guidance regarding any consequences of your request.
NHSE will not send you generic information from other public authorities and government agencies on issues of government policy.
9 TRANSFERS ABROAD
The UK GDPR imposes restrictions on the transfer of personal data outside the European Union, to third countries or international organisations, in order to ensure that the level of protection of individuals afforded by the UK GDPR is not undermined.
Your data may only be transferred abroad where NHSE is assured that a third country, a territory or one or more specific sectors in the third country, or an international organisation ensures an adequate level of protection.
10 HOW WE PROTECT YOUR PERSONAL DATA
Our processing of all personal data complies with the UK GDPR principles. We have put in place appropriate security measures to prevent your personal data from being accidentally lost, used or accessed in an unauthorised way, altered or disclosed. The security of the data is assured through the implementation of NHSE’s policies on information governance management.
The personal data we hold may be held as an electronic record on data systems managed by NHSE, or as a paper record. These records are only accessed, seen and used in the following circumstances:
if required and/or permitted by law; or
by NHSE staff who need access to them so they can do their jobs and who are subject to a duty of confidentiality; or
by other partner organisations, including our suppliers, who have been asked to sign appropriate non-disclosure or data sharing agreements and will never be allowed to use the information for commercial purposes.
We make every effort to keep your personal information accurate and up to date, but in some cases we are reliant on you as the data subject to notify us of any necessary changes to your personal data. If you tell us of any changes in your circumstances, we can update the records with personal data you choose to share with us.
Information collected by NHSE will never be sold for financial gain or shared with other organisations for commercial purposes.
We have put in place procedures to deal with any suspected personal data breach and will notify you and any applicable regulator of a breach where we are legally required to do so.
11 SHARING PERSONAL DATA
So we can provide the right services at the right level, we may share your personal data within services across NHSE and with other third party organisations such as the Department of Health, higher education institutions, clinical placement providers, colleges, faculties, other NHSE Local Offices, the General Medical Council, NHS Trusts/Health Boards/Health and Social Care Trusts, approved academic researchers and other NHS and government agencies where necessary, to provide the best possible Training and to ensure that we discharge NHSEs responsibilities for employment and workforce planning for the NHS. This will be on a legitimate need to know basis only.
We may also share information, where necessary, to prevent, detect or assist in the investigation of fraud or criminal activity, to assist in the administration of justice, for the purposes of seeking legal advice or exercising or defending legal rights or as otherwise required by the law.
Where the data is used for analysis and publication by a recipient or third party, any publication will be on an anonymous basis, and will not make it possible to identify any individual. This will mean that the data ceases to become personal data.
12 HOW LONG WE RETAIN YOUR PERSONAL DATA
We will keep personal data for no longer than necessary to fulfil the purposes we collected it for, in accordance with our records management policy and the NHS records retention schedule within the NHS Records Management Code of Practice at: https://www.england.nhs.uk/contact-us/privacy-notice/nhs-england-as-a-data-controller (as may be amended from time to time).
In some circumstances you can ask us to delete your data. Please see the “Your rights” section below for further information.
In some circumstances we will anonymise your personal data (so that it can no longer be associated with you) for research or statistical purposes, in which case we may use this information indefinitely without further notice to you.
13 OPEN DATA
Open data is data that is published by central government, local authorities and public bodies to help you build products and services. NHSE policy is to observe the Cabinet Office transparency and accountability commitments towards more open use of public data in accordance with relevant and applicable UK legislation.
NHSE would never share personal data through the open data facility. To this end, NHSE will implement information governance protocols that reflect the ICO’s recommended best practice for record anonymisation, and Office of National Statistics guidance on publication of statistical information.
14 YOUR RIGHTS
14.1 Right to rectification and erasure
Under the UK GDPR you have the right to rectification of inaccurate personal data and the right to request the erasure of your personal data. However, the right to erasure is not an absolute right and it may be that it is necessary for NHSE to continue to process your personal data for a number of lawful and legitimate reasons.
14.2 Right to object and withdraw your consent
You have the right in certain circumstances to ask NHSE to stop processing your personal data in relation to any NHSE service. As set out above, you can decide that you do not wish to receive information from NHSE about matters of policy affecting Training and workforce. However, the right to object is not an absolute right and it may be that it is necessary in certain circumstances for NHSE to continue to process your personal data for a number of lawful and legitimate reasons.
If you object to the way in which NHSE is processing your personal information or if you wish to ask NHSE to stop processing your personal data, please contact your relevant Local Office.
Please note, if we do stop processing personal data about you, this may prevent NHSE from providing the best possible service to you. Withdrawing your consent will result in your Training account being anonymised and access to the Training removed.
14.3 Right to request access
You can access a copy of the information NHSE holds about you by writing to NHSE’s Public and Parliamentary Accountability Team. This information is generally available to you free of charge subject to the receipt of appropriate identification. More information about subject access requests can be found here: https://www.hee.nhs.uk/about/contact-us/subject-access-request.
14.4 Right to request a transfer
The UK GDPR sets out the right for a data subject to have their personal data ported from one controller to another on request in certain circumstances. You should discuss any request for this with your Local Office. This right only applies to automated information which you initially provided consent for us to use or where we used the information to perform a contract with you.
14.5 Right to restrict processing
You can ask us to suspend the processing of your personal data if you want us to establish the data’s accuracy, where our use of the data is unlawful but you do not want us to erase it, where you need us to hold the data even if we no longer require it as you need it to establish, exercise or defend legal claims or where you have objected to our use of your data but we need to verify whether we have overriding legitimate grounds to use it.
14.6 Complaints
You have the right to make a complaint at any time to the ICO. We would, however, appreciate the chance to deal with your concerns before you approach the ICO so please contact your Local Office or the DPO in the first instance, using the contact details above.
You can contact the ICO at the following address:
The Office of the Information Commissioner Wycliffe House Water Lane Wilmslow Cheshire SK9 5AF
14.7 Your responsibilities
It is important that you work with us to ensure that the information we hold about you is accurate and up to date so please inform NHSE if any of your personal data needs to be updated or corrected.
All communications from NHSE will normally be by email. It is therefore essential for you to maintain an effective and secure email address, or you may not receive information or other important news and information about your employment or Training.
";
+
+ Execute.Sql(@"UPDATE Config SET UpdatedDate = GETDATE() ,ConfigText =N'" + PrivacyPolicyNew + "'" +
+ "where ConfigName='PrivacyPolicy' AND IsHtml = 1");
+
+ var AcceptableUseNew = @"
ACCEPTABLE USE POLICY
+
+
+ General
+
+
This Acceptable Use Policy sets out how we permit you to use any of our Platforms. Your compliance with this Acceptable Use Policy is a condition of your use of the Platform.
You are permitted to use the Platform as set out in the Terms and for the purpose of personal study.
+
You must not use any part of the Content on the Platform for commercial purposes without obtaining a licence to do so from us or our licensors.
+
If you print off, copy, download, share or repost any part of the Platform in breach of this Acceptable Use Policy, your right to use the Platform will cease immediately and you must, at our option, return or destroy any copies of the materials you have made.
+
Our status (and that of any identified contributors) as the authors of Content on the Platform must always be acknowledged (except in respect of Third-Party Content).
+
+
+
+ Prohibited uses
+
+
+ You may not use the Platform:
+
+
in any way that breaches any applicable local, national or international law or regulation;
+
in any way that is unlawful or fraudulent or has any unlawful or fraudulent purpose or effect;
+
in any way that infringes the rights of, or restricts or inhibits the use and enjoyment of this site by any third party;
+
for the purpose of harming or attempting to harm minors in any way;
+
to bully, insult, intimidate or humiliate any person;
+
to send, knowingly receive, upload, download, use or re-use any material which does not comply with our Content Standards as set out in paragraph 4;
+
to transmit, or procure the sending of, any unsolicited or unauthorised advertising or promotional material or any other form of similar solicitation (spam), or any unwanted or repetitive content that may cause disruption to the Platform or diminish the user experience, of the Platform’s usefulness or relevant to others;
+
to do any act or thing with the intention of disrupting the Platform in any way, including uploading any malware or links to malware, or introduce any virus, trojan, worm, logic bomb or other material that is malicious or technologically harmful or other potentially damaging items into the Platform;
+
to knowingly transmit any data, send or upload any material that contains viruses, Trojan horses, worms, time-bombs, keystroke loggers, spyware, adware or any other harmful programs or similar computer code designed to adversely affect the operation of any computer software or hardware; or
+
to upload terrorist content.
+
+
+
+ You also agree:
+
+
to follow any reasonable instructions given to you by us in connection with your use of the Platform;
+
to respect the rights and dignity of others, in order to maintain the ethos and good reputation of the NHS, the public good generally and the spirit of cooperation between those studying and working within the health and care sector. In particular, you must act in a professional manner with regard to all other users of the Platform at all times;
+
+ not to modify or attempt to modify any of the Content, save:
+
+
in respect of Contributions;
+
where you are the editor of a catalogue within the Learning Hub, you may alter Content within that catalogue;
+
+
+
not to download or copy any of the Content to electronic or photographic media;
+
not to reproduce any part of the Content by any means or under any format other than as a reasonable aid to your personal study;
+
not to reproduce, duplicate, copy or re-sell any Content in contravention of the provisions of this Acceptable Use Policy; and
+
not to use tools that automatically perform actions on your behalf;
+
not to upload any content that infringes the intellectual property rights, privacy rights or any other rights of any person or organisation; and
+
not to attempt to disguise your identity or that of your organisation;
+
+ not to access without authority, interfere with, damage or disrupt:
+
+
any part of the Platform;
+
any equipment or network on which the Platform is stored;
+
any software used in the provision of the Platform;
+
the server on which the Platform is stored;
+
any computer or database connected to the Platform; or
+
any equipment or network or software owned or used by any third party.
+
+
+
not to attack the Platform via a denial-of-service attack or a distributed denial-of-service attack.
+
+
+
+
+
+ Content standards
+
+
The content standards set out in this paragraph 4 (Content Standards) apply to any and all Contributions.
+
The Content Standards must be complied with in spirit as well as to the letter. The Content Standards apply to each part of any Contribution as well as to its whole.
+
We will determine, in our discretion, whether a Contribution breaches the Content Standards.
+
+ A Contribution must:
+
+
be accurate (where it states facts);
+
be genuinely held (where it states opinions); and
+
comply with the law applicable in England and Wales and in any country from which it is posted.
+
+
+
+ A Contribution must not:
+
+
+ contain misinformation that is likely to harm users, patients/service users, health and care workers, or the general public’s wellbeing, safety, trust and reputation, including the reputation of the NHS or any part of it. This could include false and misleading information relating to disease prevention and treatment, conspiracy theories, content that encourages discrimination, harassment or physical violence, content originating from misinformation campaigns, and content edited or manipulated in such a way as to constitute misinformation;
+
+
+ contain any content or link to any content:
+
+
which is created for advertising, promotional or other commercial purposes, including links, logos and business names;
+
which requires a subscription or payment to gain access to such content;
+
in which the user has a commercial interest;
+
which promotes a business name and/or logo;
+
which contains a link to an app via iOS or Google Play; or
+
which has as its purpose or effect the collection and sharing of personal data;
+
+
+
+ be irrelevant to the purpose or aims of the Platform or while addressing relevant subject matter, contain an irrelevant, unsuitable or inappropriate slant (for example relating to potentially controversial opinions or beliefs of any kind intended to influence others);
+
+
be defamatory of any person;
+
be obscene, offensive, hateful or inflammatory, or contain any profanity;
+
bully, insult, intimidate or humiliate;
+
+ encourage suicide, substance abuse, eating disorders or other acts of self-harm.Content related to self - harm for the purposes of therapy, education and the promotion of general wellbeing may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally;
+
+
+ feature sexual imagery purely intended to stimulate sexual arousal. Non - pornographic content relating to sexual health and related issues, surgical procedures and the results of surgical procedures, breastfeeding, therapy, education and the promotion of general wellbeing may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally;
+
+
+ include child sexual abuse material. Content relating to safeguarding which addresses the subject of child sexual abuse may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally;
+
+
+ incite or glorify violence including content designed principally for the purposes of causing reactions of shock or disgust;
+
+
+ promote discrimination or discriminate in respect of the protected characteristics set out in the Equality Act 2010, being age, disability, gender reassignment, marriage and civil partnership, pregnancy and maternity, race, nationality, religion or belief, sex, and sexual orientation;
+
+
infringe any copyright, database right or trade mark of any other person;
+
be likely to deceive any person;
+
breach any legal duty owed to a third party, such as a contractual duty or a duty of confidence;
+
+ promote any illegal content or activity, including but not limited to the encouragement, promotion, justification, praise or provision of aid to dangerous persons or organisations, including extremists, terrorists and terrorist organisations and those engaged in any form of criminal activity;
+
+
be in contempt of court;
+
+ be threatening, abuse or invade another''s privacy, or cause annoyance, inconvenience or needless anxiety;
+
+
be likely to harass, bully, shame, degrade, upset, embarrass, alarm or annoy any other person;
+
impersonate any person or misrepresent your identity or affiliation with any person;
+
+ advocate, promote, incite any party to commit, or assist any unlawful or criminal act such as (by way of example only) copyright infringement or computer misuse;
+
+
+ contain a statement which you know or believe, or have reasonable grounds for believing, that members of the public to whom the statement is, or is to be, published are likely to understand as a direct or indirect encouragement or other inducement to the commission, preparation or instigation of acts of terrorism;
+
+
contain harmful material;
+
give the impression that the Contribution emanates from us, if this is not the case; or
+
disclose any third party’s confidential information, identity, personally identifiable information or personal data (including data concerning health).
+
+
+
You acknowledge and accept that, when using the Platform and accessing the Content, some Content that has been uploaded by third parties may be factually inaccurate, or the topics addressed by the Content may be offensive, indecent, or objectionable in nature. We are not responsible (legally or otherwise) for any claim you may have in relation to the Content.
+
When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources of evidence are valid (for example, by peer review).
+
+
+
+ Metadata
+
When making any Contribution, you must where prompted include a sufficient description of the Content so that other users can understand the description, source, and age of the Content. For example, if Content has been quality assured, then the relevant information should be posted in the appropriate field. All metadata fields on the Platform must be completed appropriately before initiating upload. Including the correct information is important in order to help other users locate the Content (otherwise the Content may not appear in search results for others to select).
+
+
+ Updates
+
You must update each Contribution at least once every 3 (three) years, or update or remove it should it cease to be relevant or become outdated or revealed or generally perceived to be unsafe or otherwise unsuitable for inclusion on the Platform.
The Platform must not be framed on any other site.
+
You may directly link to any Content that is hosted on the Platform, however, please be aware that not all links will continue to be available indefinitely. We will use our best efforts to ensure that all links are valid at the time of creating the related Content but cannot be held responsible for any subsequent changes to the link address or related Content.
+
+
+
+ No text or data mining, or web scraping
+
+
+ You shall not conduct, facilitate, authorize or permit any text or data mining or web scraping in relation to the Platform or any services provided via, or in relation to, the Platform. This includes using (or permitting, authorizing or attempting the use of):
+
+
any ""robot"", ""bot"", ""spider"", ""scraper"" or other automated device, program, tool, algorithm, code, process or methodology to access, obtain, copy, monitor or republish any portion of the Platform or any data, Content, information or services accessed via the same; and/or
+
any automated analytical technique aimed at analyzing text and data in digital form to generate information which includes but is not limited to patterns, trends, and correlations.
+
+
+
The provisions in this paragraph should be treated as an express reservation of our rights in this regard, including for the purposes of Article 4(3) of Digital Copyright Directive ((EU) 2019/790).
+
This paragraph shall not apply insofar as (but only to the extent that) we are unable to exclude or limit text or data mining or web scraping activity by contract under the laws which are applicable to us.
+
+
+
+ Breach of this Acceptable Use Policy
+
Failure to comply with this Acceptable Use Policy constitutes a material breach of this Acceptable Use Policy upon which you are permitted to use the Platform and may result in our taking all or any of the following actions:
+
+
Immediate, temporary, or permanent withdrawal of your right to use the Platform;
+
Immediate, temporary, or permanent removal of any Contribution uploaded by you to the Platform;
+
Issue of a warning to you;
+
Legal proceedings against you for reimbursement of all costs...
+
Disclosure of such information to law enforcement authorities...
+
Any other action we reasonably deem appropriate.
+
+
+ ";
+
+ Execute.Sql(@"UPDATE Config SET UpdatedDate = GETDATE() ,ConfigText =N'" + AcceptableUseNew + "'" +
+ "where ConfigName='AcceptableUse' AND IsHtml = 1");
+ }
+ public override void Down()
+ {
+ var PrivacyPolicyOld = @"
PRIVACY NOTICE
This page explains our privacy policy and how we will use and protect any information about you that you give to us or that we collate when you visit this website, or undertake employment with NHS England (NHSE or we/us/our), or participate in any NHSE sponsored training, education and development including via any of our training platform websites (Training). This privacy notice is intended to provide transparency regarding what personal data NHSE may hold about you, how it will be processed and stored, how long it will be retained and who may have access to your data. Personal data is any information relating to an identified or identifiable living person (known as the data subject). An identifiable person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number or factors specific to the physical, genetic or mental identity of that person, for example.
1 OUR ROLE IN THE NHS
We are here to improve the quality of healthcare for the people and patients of England through education, training and lifelong development of staff and appropriate planning of the workforce required to deliver healthcare services in England. We aim to enable high quality, effective, compassionate care and to identify the right people with the right skills and the right values. All the information we collect is to support these objectives.
2 IMPORTANT INFORMATION
NHSE is the data controller in respect of any personal data it holds concerning trainees in Training, individuals employed by NHSE and individuals accessing NHSE’s website. We have appointed a data protection officer (DPO) who is responsible for overseeing questions in relation to this privacy policy. If you have any questions about this privacy policy or our privacy practices, want to know more about how your information will be used, or make a request to exercise your legal rights, please contact our DPO in the following ways: Name: Andrew Todd Email address: gdpr@hee.nhs.ukPostal address: NHS England of Skipton House, 80 London Road, London SE1 6LH
3 WHAT THIS PRIVACY STATEMENT COVERS
This privacy statement only covers the processing of personal data by NHSE that NHSE has obtained from data subjects accessing any of NHSE’s websites and from its provision of services and exercise of functions. It does not cover the processing of data by any sites that can be linked to or from NHSE’s websites, so you should always be aware when you are moving to another site and read the privacy statement on that website. When providing NHSE with any of your personal data for the first time, for example, when you take up an appointment with NHSE or when you register for any Training, you will be asked to confirm that you have read and accepted the terms of this privacy statement. A copy of your acknowledgement will be retained for reference. If our privacy policy changes in any way, we will place an updated version on this page. Regularly reviewing the page ensures you are always aware of what information we collect, how we use it and under what circumstances, if any, we will share it with other parties.
4 WHY NHSE COLLECTS YOUR PERSONAL DATA
Personal data may be collected from you via the recruitment process, when you register and/or create an account for any Training, during your Annual Review of Competence Progression or via NHSE’s appraisal process. Personal data may also be obtained from Local Education Providers or employing organisations in connection with the functions of NHSE. Your personal data is collected and processed for the purposes of and in connection with the functions that NHSE performs with regard to Training and planning. The collection and processing of such data is necessary for the purposes of those functions. A full copy of our notification to the Information Commissioner’s Office (ICO) (the UK regulator for data protection issues), can be found on the ICO website here: www.ico.org.uk by searching NHSE’s ICO registration number, which is Z2950066. In connection with Training, NHSE collects and uses your personal information for the following purposes:
to manage your Training and programme, including allowing you to access your own learning history;
to quality assure Training programmes and ensure that standards are maintained, including gathering feedback or input on the service, content, or layout of the Training and customising the content and/or layout of the Training;
to identify workforce planning targets;
to maintain patient safety through the management of performance concerns;
to comply with legal and regulatory responsibilities including revalidation;
to contact you about Training updates, opportunities, events, surveys and information that may be of interest to you;
transferring your Training activity records for programmes to other organisations involved in medical training in the healthcare sector. These organisations include professional bodies that you may be a member of, such as a medical royal college or foundation school; or employing organisations, such as trusts;
making your Training activity records visible to specific named individuals, such as tutors, to allow tutors to view their trainees’ activity. We would seek your explicit consent before authorising anyone else to view your records;
providing anonymous, summarised data to partner organisations, such as professional bodies; or local organisations, such as strategic health authorities or trusts;
for NHSE internal review;
to provide HR related support services and Training to you, for clinical professional learner recruitment;
to promote our services;
to monitor our own accounts and records;
to monitor our work, to report on progress made; and
to let us fulfil our statutory obligations and statutory returns as set by the Department of Health and the law (for example complying with NHSE’s legal obligations and regulatory responsibilities under employment law).
Further information about our use of your personal data in connection with Training can be found in ’A Reference Guide for Postgraduate Foundation and Specialty Training in the UK’, published by the Conference of Postgraduate Medical Deans of the United Kingdom and known as the ‘Gold Guide’, which can be found here: https://www.copmed.org.uk/gold-guide.
5 TYPES OF PERSONAL DATA COLLECTED BY NHSE
The personal data that NHSE collects when you register for Training enables the creation of an accurate user profile/account, which is necessary for reporting purposes and to offer Training that is relevant to your needs. The personal data that is stored by NHSE is limited to information relating to your work, such as your job role, place of work, and membership number for a professional body (e.g. your General Medical Council number). NHSE will never ask for your home address or any other domestic information. When accessing Training, you will be asked to set up some security questions, which may contain personal information. These questions enable you to log in if you forget your password and will never be used for any other purpose. The answers that you submit when setting up these security questions are encrypted in the database so no one can view what has been entered, not even NHSE administrators. NHSE also store a record of some Training activity, including upload and download of Training content, posts on forums or other communication media, and all enquires to the service desks that support the Training. If you do not provide personal data that we need from you when requested, we may not be able to provide services (such as Training) to you. In this case, we may have to cancel such service, but we will notify you at the time if this happens.
6 COOKIES
When you access NHSE’s website and Training, we want to make them easy, useful and reliable. This sometimes involves placing small amounts of limited information on your device (such as your computer or mobile phone). These small files are known as cookies, and we ask you to agree to their usage in accordance with ICO guidance. These cookies are used to improve the services (including the Training) we provide you through, for example:
enabling a service to recognise your device, so you do not have to give the same information several times during one task (e.g. we use a cookie to remember your username if you check the ’Remember Me’ box on a log in page);
recognising that you may already have given a username and password, so you do not need to do it for every web page requested;
measuring how many people are using services, so they can be made easier to use and there is enough capacity to ensure they are fast; and
analysing anonymised data to help us understand how people interact with services so we can make them better.
We use a series of cookies to monitor website speed and usage, as well as to ensure that any preferences you have selected previously are the same when you return to our website. Please visit our cookie policies page to understand the cookies that we use: https://www.dls.nhs.uk/v2/CookieConsent/CookiePolicy Most cookies applied when accessing Training are used to keep track of your input when filling in online forms, known as session-ID cookies, which are exempt from needing consent as they are deemed essential for using the website or Training they apply to. Some cookies, like those used to measure how you use the Training, are not needed for our website to work. These cookies can help us improve the Training, but we’ll only use them if you say it’s OK. We’ll use a cookie to save your settings. On a number of pages on our website or Training, we use ’plug-ins’ or embedded media. For example, we might embed YouTube videos. Where we have used this type of content, the suppliers of these services may also set cookies on your device when you visit their pages. These are known as ’third-party’ cookies. To opt-out of third-parties collecting any data regarding your interaction on our website, please refer to their websites for further information. We will not use cookies to collect personal data about you. However, if you wish to restrict or block the cookies which are set by our websites or Training, or indeed any other website, you can do this through your browser settings. The ’Help’ function within your browser should tell you how. Alternatively, you may wish to visit www.aboutcookies.org which contains comprehensive information on how to do this on a wide variety of browsers. You will also find details on how to delete cookies from your machine as well as more general information about cookies. Please be aware that restricting cookies may impact on the functionality of our website.
7 LEGAL BASIS FOR PROCESSING
The retained EU law version of the General Data Protection Regulation ((EU) 2016/679) (UK GDPR) requires that data controllers and organisations that process personal data demonstrate compliance with its provisions. This involves publishing our basis for lawful processing of personal data. As personal data is processed for the purposes of NHSE’s statutory functions, NHSE’s legal bases for the processing of personal data as listed in Article 6 of the UK GDPR are as follows:
6(1)(a) – Consent of the data subject
6(1)(b) – Processing is necessary for the performance of a contract to which the data subject is party or in order to take steps at the request of the data subject prior to entering into a contract
6(1)(c) – Processing is necessary for compliance with a legal obligation
6(1)(e) – Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority vested in the controller
Where NHSE processes special categories of personal data, its additional legal bases for processing such data as listed in Article 9 of the UK GDPR are as follows:
9(2)(a) – Explicit consent of the data subject
9(2)(b) – Processing is necessary for the purposes of carrying out the obligations and exercising specific rights of the controller or of the data subject in the field of employment and social security and social protection law
9(2)(f) – Processing is necessary for the establishment, exercise or defence of legal claims or whenever courts are acting in their judicial capacity
9(2)(g) – Processing is necessary for reasons of substantial public interest
9(2)(h) – Processing is necessary for the purposes of occupational medicine, for the assessment of the working capacity of the employee, or the management of health and social care systems and services
9(2)(j) – Processing is necessary for archiving purposes in the public interest, scientific or historical research purposes or statistical purposes
Special categories of personal data include data relating to racial or ethnic origin, political opinions, religious beliefs, sexual orientation and data concerning health. Please note that not all of the above legal bases will apply for each type of processing activity that NHSE may undertake. However, when processing any personal data for any particular purpose, one or more of the above legal bases will apply. We may seek your consent for some processing activities, for example for sending out invitations to you to Training events and sending out material from other government agencies. If you do not give consent for us to use your data for these purposes, we will not use your data for these purposes, but your data may still be retained by us and used by us for other processing activities based on the above lawful conditions for processing set out above.
8 INFORMATION THAT WE MAY NEED TO SEND YOU
We may occasionally have to send you information from NHSE, the Department of Health, other public authorities and government agencies about matters of policy where those policy issues impact on Training, workforce planning, or other matters related to NHSE. This is because NHSE is required by statute to exercise functions of the Secretary of State in respect of Training and workforce planning. If you prefer, you can opt out of receiving information about general matters of policy impacting on Training and workforce planning by contacting your Local Office recruitment lead or tel@hee.nhs.uk. The relevant Local Office or a representative from the relevant training platform website will provide you with further advice and guidance regarding any consequences of your request. NHSE will not send you generic information from other public authorities and government agencies on issues of government policy.
9 TRANSFERS ABROAD
The UK GDPR imposes restrictions on the transfer of personal data outside the European Union, to third countries or international organisations, in order to ensure that the level of protection of individuals afforded by the UK GDPR is not undermined. Your data may only be transferred abroad where NHSE is assured that a third country, a territory or one or more specific sectors in the third country, or an international organisation ensures an adequate level of protection.
10 HOW WE PROTECT YOUR PERSONAL DATA
Our processing of all personal data complies with the UK GDPR principles. We have put in place appropriate security measures to prevent your personal data from being accidentally lost, used or accessed in an unauthorised way, altered or disclosed. The security of the data is assured through the implementation of NHSE’s policies on information governance management. The personal data we hold may be held as an electronic record on data systems managed by NHSE, or as a paper record. These records are only accessed, seen and used in the following circumstances:
if required and/or permitted by law; or
by NHSE staff who need access to them so they can do their jobs and who are subject to a duty of confidentiality; or
by other partner organisations, including our suppliers, who have been asked to sign appropriate non-disclosure or data sharing agreements and will never be allowed to use the information for commercial purposes.
We make every effort to keep your personal information accurate and up to date, but in some cases we are reliant on you as the data subject to notify us of any necessary changes to your personal data. If you tell us of any changes in your circumstances, we can update the records with personal data you choose to share with us. Information collected by NHSE will never be sold for financial gain or shared with other organisations for commercial purposes. We have put in place procedures to deal with any suspected personal data breach and will notify you and any applicable regulator of a breach where we are legally required to do so.
11 SHARING PERSONAL DATA
So we can provide the right services at the right level, we may share your personal data within services across NHSE and with other third party organisations such as the Department of Health, higher education institutions, clinical placement providers, colleges, faculties, other NHSE Local Offices, the General Medical Council, NHS Trusts/Health Boards/Health and Social Care Trusts, approved academic researchers and other NHS and government agencies where necessary, to provide the best possible Training and to ensure that we discharge NHSEs responsibilities for employment and workforce planning for the NHS. This will be on a legitimate need to know basis only. We may also share information, where necessary, to prevent, detect or assist in the investigation of fraud or criminal activity, to assist in the administration of justice, for the purposes of seeking legal advice or exercising or defending legal rights or as otherwise required by the law. Where the data is used for analysis and publication by a recipient or third party, any publication will be on an anonymous basis, and will not make it possible to identify any individual. This will mean that the data ceases to become personal data.
12 HOW LONG WE RETAIN YOUR PERSONAL DATA
We will keep personal data for no longer than necessary to fulfil the purposes we collected it for, in accordance with our records management policy and the NHS records retention schedule within the NHS Records Management Code of Practice at: https://www.england.nhs.uk/contact-us/privacy-notice/nhs-england-as-a-data-controller (as may be amended from time to time). In some circumstances you can ask us to delete your data. Please see the “Your rights” section below for further information. In some circumstances we will anonymise your personal data (so that it can no longer be associated with you) for research or statistical purposes, in which case we may use this information indefinitely without further notice to you.
13 OPEN DATA
Open data is data that is published by central government, local authorities and public bodies to help you build products and services. NHSE policy is to observe the Cabinet Office transparency and accountability commitments towards more open use of public data in accordance with relevant and applicable UK legislation. NHSE would never share personal data through the open data facility. To this end, NHSE will implement information governance protocols that reflect the ICO’s recommended best practice for record anonymisation, and Office of National Statistics guidance on publication of statistical information.
14 YOUR RIGHTS
14.1 Right to rectification and erasure
Under the UK GDPR you have the right to rectification of inaccurate personal data and the right to request the erasure of your personal data. However, the right to erasure is not an absolute right and it may be that it is necessary for NHSE to continue to process your personal data for a number of lawful and legitimate reasons.
14.2 Right to object and withdraw your consent
You have the right in certain circumstances to ask NHSE to stop processing your personal data in relation to any NHSE service. As set out above, you can decide that you do not wish to receive information from NHSE about matters of policy affecting Training and workforce. However, the right to object is not an absolute right and it may be that it is necessary in certain circumstances for NHSE to continue to process your personal data for a number of lawful and legitimate reasons. If you object to the way in which NHSE is processing your personal information or if you wish to ask NHSE to stop processing your personal data, please contact your relevant Local Office. Please note, if we do stop processing personal data about you, this may prevent NHSE from providing the best possible service to you. Withdrawing your consent will result in your Training account being anonymised and access to the Training removed.
14.3 Right to request access
You can access a copy of the information NHSE holds about you by writing to NHSE’s Public and Parliamentary Accountability Team. This information is generally available to you free of charge subject to the receipt of appropriate identification. More information about subject access requests can be found here: https://www.hee.nhs.uk/about/contact-us/subject-access-request.
14.4 Right to request a transfer
The UK GDPR sets out the right for a data subject to have their personal data ported from one controller to another on request in certain circumstances. You should discuss any request for this with your Local Office. This right only applies to automated information which you initially provided consent for us to use or where we used the information to perform a contract with you.
14.5 Right to restrict processing
You can ask us to suspend the processing of your personal data if you want us to establish the data’s accuracy, where our use of the data is unlawful but you do not want us to erase it, where you need us to hold the data even if we no longer require it as you need it to establish, exercise or defend legal claims or where you have objected to our use of your data but we need to verify whether we have overriding legitimate grounds to use it.
14.6 Complaints
You have the right to make a complaint at any time to the ICO. We would, however, appreciate the chance to deal with your concerns before you approach the ICO so please contact your Local Office or the DPO in the first instance, using the contact details above. You can contact the ICO at the following address:
The Office of the Information Commissioner Wycliffe House Water Lane Wilmslow Cheshire SK9 5AF
14.7 Your responsibilities
It is important that you work with us to ensure that the information we hold about you is accurate and up to date so please inform NHSE if any of your personal data needs to be updated or corrected. All communications from NHSE will normally be by email. It is therefore essential for you to maintain an effective and secure email address, or you may not receive information or other important news and information about your employment or Training. ";
+
+ Execute.Sql(@"UPDATE Config SET UpdatedDate = GETDATE() ,ConfigText =N'" + PrivacyPolicyOld + "'" +
+ "where ConfigName='PrivacyPolicy' AND IsHtml = 1");
+
+ var AcceptableUseOld = @"
ACCEPTABLE USE POLICY
+
+
+ General
+
+
This Acceptable Use Policy sets out how we permit you to use any of our Platforms. Your compliance with this Acceptable Use Policy is a condition of your use of the Platform.
You are permitted to use the Platform as set out in the Terms and for the purpose of personal study.
+
You must not use any part of the Content on the Platform for commercial purposes without obtaining a licence to do so from us or our licensors.
+
If you print off, copy, download, share or repost any part of the Platform in breach of this Acceptable Use Policy, your right to use the Platform will cease immediately and you must, at our option, return or destroy any copies of the materials you have made.
+
Our status (and that of any identified contributors) as the authors of Content on the Platform must always be acknowledged (except in respect of Third-Party Content).
+
+
+
+ Prohibited uses
+
+
+ You may not use the Platform:
+
+
in any way that breaches any applicable local, national or international law or regulation;
+
in any way that is unlawful or fraudulent or has any unlawful or fraudulent purpose or effect;
+
in any way that infringes the rights of, or restricts or inhibits the use and enjoyment of this site by any third party;
+
for the purpose of harming or attempting to harm minors in any way;
+
to bully, insult, intimidate or humiliate any person;
+
to send, knowingly receive, upload, download, use or re-use any material which does not comply with our Content Standards as set out in paragraph 4;
+
to transmit, or procure the sending of, any unsolicited or unauthorised advertising or promotional material or any other form of similar solicitation (spam), or any unwanted or repetitive content that may cause disruption to the Platform or diminish the user experience, of the Platform’s usefulness or relevant to others;
+
to do any act or thing with the intention of disrupting the Platform in any way, including uploading any malware or links to malware, or introduce any virus, trojan, worm, logic bomb or other material that is malicious or technologically harmful or other potentially damaging items into the Platform;
+
to knowingly transmit any data, send or upload any material that contains viruses, Trojan horses, worms, time-bombs, keystroke loggers, spyware, adware or any other harmful programs or similar computer code designed to adversely affect the operation of any computer software or hardware; or
+
to upload terrorist content.
+
+
+
+ You also agree:
+
+
to follow any reasonable instructions given to you by us in connection with your use of the Platform;
+
to respect the rights and dignity of others, in order to maintain the ethos and good reputation of the NHS, the public good generally and the spirit of cooperation between those studying and working within the health and care sector. In particular, you must act in a professional manner with regard to all other users of the Platform at all times;
+
+ not to modify or attempt to modify any of the Content, save:
+
+
in respect of Contributions;
+
where you are the editor of a catalogue within the Learning Hub, you may alter Content within that catalogue;
+
+
+
not to download or copy any of the Content to electronic or photographic media;
+
not to reproduce any part of the Content by any means or under any format other than as a reasonable aid to your personal study;
+
not to reproduce, duplicate, copy or re-sell any Content in contravention of the provisions of this Acceptable Use Policy; and
+
not to use tools that automatically perform actions on your behalf;
+
not to upload any content that infringes the intellectual property rights, privacy rights or any other rights of any person or organisation; and
+
not to attempt to disguise your identity or that of your organisation;
+
+ not to access without authority, interfere with, damage or disrupt:
+
+
any part of the Platform;
+
any equipment or network on which the Platform is stored;
+
any software used in the provision of the Platform;
+
the server on which the Platform is stored;
+
any computer or database connected to the Platform; or
+
any equipment or network or software owned or used by any third party.
+
+
+
not to attack the Platform via a denial-of-service attack or a distributed denial-of-service attack.
+
+
+
+
+
+ Content standards
+
+
The content standards set out in this paragraph 4 (Content Standards) apply to any and all Contributions.
+
The Content Standards must be complied with in spirit as well as to the letter. The Content Standards apply to each part of any Contribution as well as to its whole.
+
We will determine, in our discretion, whether a Contribution breaches the Content Standards.
+
+ A Contribution must:
+
+
be accurate (where it states facts);
+
be genuinely held (where it states opinions); and
+
comply with the law applicable in England and Wales and in any country from which it is posted.
+
+
+
+ A Contribution must not:
+
+
+ contain misinformation that is likely to harm users, patients/service users, health and care workers, or the general public’s wellbeing, safety, trust and reputation, including the reputation of the NHS or any part of it. This could include false and misleading information relating to disease prevention and treatment, conspiracy theories, content that encourages discrimination, harassment or physical violence, content originating from misinformation campaigns, and content edited or manipulated in such a way as to constitute misinformation;
+
+
+ contain any content or link to any content:
+
+
which is created for advertising, promotional or other commercial purposes, including links, logos and business names;
+
which requires a subscription or payment to gain access to such content;
+
in which the user has a commercial interest;
+
which promotes a business name and/or logo;
+
which contains a link to an app via iOS or Google Play; or
+
which has as its purpose or effect the collection and sharing of personal data;
+
+
+
+ be irrelevant to the purpose or aims of the Platform or while addressing relevant subject matter, contain an irrelevant, unsuitable or inappropriate slant (for example relating to potentially controversial opinions or beliefs of any kind intended to influence others);
+
+
be defamatory of any person;
+
be obscene, offensive, hateful or inflammatory, or contain any profanity;
+
bully, insult, intimidate or humiliate;
+
+ encourage suicide, substance abuse, eating disorders or other acts of self-harm.Content related to self - harm for the purposes of therapy, education and the promotion of general wellbeing may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally;
+
+
+ feature sexual imagery purely intended to stimulate sexual arousal. Non - pornographic content relating to sexual health and related issues, surgical procedures and the results of surgical procedures, breastfeeding, therapy, education and the promotion of general wellbeing may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally;
+
+
+ include child sexual abuse material. Content relating to safeguarding which addresses the subject of child sexual abuse may be uploaded, but we reserve the right to make changes to the way in which it is accessed in order that users do not view it accidentally;
+
+
+ incite or glorify violence including content designed principally for the purposes of causing reactions of shock or disgust;
+
+
+ promote discrimination or discriminate in respect of the protected characteristics set out in the Equality Act 2010, being age, disability, gender reassignment, marriage and civil partnership, pregnancy and maternity, race, nationality, religion or belief, sex, and sexual orientation;
+
+
infringe any copyright, database right or trade mark of any other person;
+
be likely to deceive any person;
+
breach any legal duty owed to a third party, such as a contractual duty or a duty of confidence;
+
+ promote any illegal content or activity, including but not limited to the encouragement, promotion, justification, praise or provision of aid to dangerous persons or organisations, including extremists, terrorists and terrorist organisations and those engaged in any form of criminal activity;
+
+
be in contempt of court;
+
+ be threatening, abuse or invade another''s privacy, or cause annoyance, inconvenience or needless anxiety;
+
+
be likely to harass, bully, shame, degrade, upset, embarrass, alarm or annoy any other person;
+
impersonate any person or misrepresent your identity or affiliation with any person;
+
+ advocate, promote, incite any party to commit, or assist any unlawful or criminal act such as (by way of example only) copyright infringement or computer misuse;
+
+
+ contain a statement which you know or believe, or have reasonable grounds for believing, that members of the public to whom the statement is, or is to be, published are likely to understand as a direct or indirect encouragement or other inducement to the commission, preparation or instigation of acts of terrorism;
+
+
contain harmful material;
+
give the impression that the Contribution emanates from us, if this is not the case; or
+
disclose any third party’s confidential information, identity, personally identifiable information or personal data (including data concerning health).
+
+
+
You acknowledge and accept that, when using the Platform and accessing the Content, some Content that has been uploaded by third parties may be factually inaccurate, or the topics addressed by the Content may be offensive, indecent, or objectionable in nature. We are not responsible (legally or otherwise) for any claim you may have in relation to the Content.
+
When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources of evidence are valid (for example, by peer review).
+
+
+
+ Metadata
+
+ When making any Contribution, you must where prompted include a sufficient description of the Content so that other users can understand the description, source, and age of the Content. For example, if Content has been quality assured, then the relevant information should be posted in the appropriate field. All metadata fields on the Platform must be completed appropriately before initiating upload. Including the correct information is important in order to help other users locate the Content (otherwise the Content may not appear in search results for others to select).
+
+
+
+ Updates
+
+ You must update each Contribution at least once every 3 (three) years, or update or remove it should it cease to be relevant or become outdated or revealed or generally perceived to be unsafe or otherwise unsuitable for inclusion on the Platform.
+
+
The Platform must not be framed on any other site.
+
You may directly link to any Content that is hosted on the Platform, however, please be aware that not all links will continue to be available indefinitely. We will use our best efforts to ensure that all links are valid at the time of creating the related Content but cannot be held responsible for any subsequent changes to the link address or related Content.
+
+
+
+ No text or data mining, or web scraping
+
+
+ You shall not conduct, facilitate, authorize or permit any text or data mining or web scraping in relation to the Platform or any services provided via, or in relation to, the Platform. This includes using (or permitting, authorizing or attempting the use of):
+
+
any ""robot"", ""bot"", ""spider"", ""scraper"" or other automated device, program, tool, algorithm, code, process or methodology to access, obtain, copy, monitor or republish any portion of the Platform or any data, Content, information or services accessed via the same; and/or
+
any automated analytical technique aimed at analyzing text and data in digital form to generate information which includes but is not limited to patterns, trends, and correlations.
+
+
+
The provisions in this paragraph should be treated as an express reservation of our rights in this regard, including for the purposes of Article 4(3) of Digital Copyright Directive ((EU) 2019/790).
+
This paragraph shall not apply insofar as (but only to the extent that) we are unable to exclude or limit text or data mining or web scraping activity by contract under the laws which are applicable to us.
+
+
+
+ Breach of this Acceptable Use Policy
+
+ Failure to comply with this Acceptable Use Policy constitutes a material breach of this Acceptable Use Policy upon which you are permitted to use the Platform and may result in our taking all or any of the following actions:
+
immediate, temporary, or permanent withdrawal of your right to use the Platform;
+
immediate, temporary, or permanent removal of any Contribution uploaded by you to the Platform;
+
issue of a warning to you;
+
legal proceedings against you for reimbursement of all costs on an indemnity basis (including, but not limited to, reasonable administrative and legal costs) resulting from the breach, and/or further legal action against you;
+
disclosure of such information to law enforcement authorities as we reasonably feel is necessary or as required by law; and/or
+
any other action we reasonably deem appropriate.
+
+
+ ";
+
+ Execute.Sql(@"UPDATE Config SET UpdatedDate = GETDATE() ,ConfigText =N'" + AcceptableUseOld + "'" +
+ "where ConfigName='AcceptableUse' AND IsHtml = 1");
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/202507221424_AddNewColumnsInSelfAssessments.cs b/DigitalLearningSolutions.Data.Migrations/202507221424_AddNewColumnsInSelfAssessments.cs
new file mode 100644
index 0000000000..7b6c381573
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202507221424_AddNewColumnsInSelfAssessments.cs
@@ -0,0 +1,22 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+ [Migration(202507221424)]
+ public class AddNewColumnsInSelfAssessments : Migration
+ {
+ public override void Up()
+ {
+ Alter.Table("SelfAssessments")
+ .AddColumn("RetirementDate").AsDateTime().Nullable()
+ .AddColumn("EnrolmentCutoffDate").AsDateTime().Nullable()
+ .AddColumn("RetirementReason").AsString(2000).Nullable();
+ }
+
+ public override void Down()
+ {
+ Delete.Column("RetirementDate").FromTable("SelfAssessments");
+ Delete.Column("EnrolmentCutoffDate").FromTable("SelfAssessments");
+ Delete.Column("RetirementReason").FromTable("SelfAssessments");
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/202507240953_Alter_GetActivitiesForDelegateEnrolment_Retired_SA.cs b/DigitalLearningSolutions.Data.Migrations/202507240953_Alter_GetActivitiesForDelegateEnrolment_Retired_SA.cs
new file mode 100644
index 0000000000..abf874dd9b
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202507240953_Alter_GetActivitiesForDelegateEnrolment_Retired_SA.cs
@@ -0,0 +1,18 @@
+using FluentMigrator;
+
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ [Migration(202507240953)]
+ public class Alter_GetActivitiesForDelegateEnrolment_Retired_SA : Migration
+ {
+ public override void Up()
+ {
+ Execute.Sql(Properties.Resources.TD_5535_Alter_GetActivitiesForDelegateEnrolment_Up);
+ }
+ public override void Down()
+ {
+ Execute.Sql(Properties.Resources.TD_5535_Alter_GetActivitiesForDelegateEnrolment_Down);
+ }
+ }
+}
+
diff --git a/DigitalLearningSolutions.Data.Migrations/202508181154_CreateOrAlterMoveCompetenciesAndGroups.cs b/DigitalLearningSolutions.Data.Migrations/202508181154_CreateOrAlterMoveCompetenciesAndGroups.cs
new file mode 100644
index 0000000000..60ac1b3cfb
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202508181154_CreateOrAlterMoveCompetenciesAndGroups.cs
@@ -0,0 +1,19 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+ [Migration(202508181154)]
+ public class CreateOrAlterMoveCompetenciesAndGroups : Migration
+ {
+ public override void Up()
+ {
+ Execute.Sql(Properties.Resources.TD_483_uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP);
+ Execute.Sql(Properties.Resources.TD_483_uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP);
+ }
+ public override void Down()
+ {
+ Execute.Sql("DROP PROCEDURE IF EXISTS [dbo].[usp_MoveCompetencyGroupInSelfAssessment]");
+ Execute.Sql("DROP PROCEDURE IF EXISTS [dbo].[usp_MoveCompetencyInSelfAssessment]");
+ Execute.Sql("DROP PROCEDURE IF EXISTS [dbo].[usp_RenumberSelfAssessmentStructure]");
+ }
+ }
+}
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/202508191440_CreateSendRetiringSelfAssessmentNotificationSP.cs b/DigitalLearningSolutions.Data.Migrations/202508191440_CreateSendRetiringSelfAssessmentNotificationSP.cs
new file mode 100644
index 0000000000..e657f3d384
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202508191440_CreateSendRetiringSelfAssessmentNotificationSP.cs
@@ -0,0 +1,31 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+ [Migration(202508191440)]
+ public class CreateSendRetiringSelfAssessmentNotificationSP : Migration
+ {
+ public override void Up()
+ {
+ string generalTELTeam =
+ @"[
+ { ""FirstName"":"""", ""LastName"":"""", ""Email"":""Support@dls.nhs.uk"" },
+ { ""FirstName"":""Anna"", ""LastName"":""Athanasopoulou"", ""Email"":""anna.athanasopoulou@nhs.net"" },
+ { ""FirstName"":""Benjamin"", ""LastName"":""Witton"", ""Email"":""Benjamin.witton1@nhs.net"" }
+ ]";
+
+ Execute.Sql(@$"IF NOT EXISTS (SELECT ConfigID FROM Config WHERE ConfigName = 'GeneralTELTeam')
+ BEGIN
+ INSERT INTO Config VALUES ('GeneralTELTeam', '{generalTELTeam}', 0,GETDATE(), GETDATE())
+ END"
+ );
+
+ Execute.Sql(Properties.Resources.TD_5552_SendRetiringNotification);
+ }
+ public override void Down()
+ {
+ Execute.Sql(@"DELETE FROM Config WHERE ConfigName = N'GeneralTELTeam'");
+
+ Execute.Sql("DROP PROCEDURE [dbo].[SendRetiringSelfAssessmentNotification]");
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/202509180915_Alter_SendRetiringSelfAssessmentNotification.cs b/DigitalLearningSolutions.Data.Migrations/202509180915_Alter_SendRetiringSelfAssessmentNotification.cs
new file mode 100644
index 0000000000..8e870cd933
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202509180915_Alter_SendRetiringSelfAssessmentNotification.cs
@@ -0,0 +1,19 @@
+
+
+namespace DigitalLearningSolutions.Data.Migrations
+{
+ using FluentMigrator;
+
+ [Migration(202509180915)]
+ public class Alter_SendRetiringSelfAssessmentNotification : Migration
+ {
+ public override void Up()
+ {
+ Execute.Sql(Properties.Resources.TD_5552_Alter_SendRetiringSelfAssessmentNotification_Up);
+ }
+ public override void Down()
+ {
+ Execute.Sql(Properties.Resources.TD_5552_Alter_SendRetiringSelfAssessmentNotification_Down);
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data.Migrations/DigitalLearningSolutions.Data.Migrations.csproj b/DigitalLearningSolutions.Data.Migrations/DigitalLearningSolutions.Data.Migrations.csproj
index aad3eb5051..ef4e4a02b1 100644
--- a/DigitalLearningSolutions.Data.Migrations/DigitalLearningSolutions.Data.Migrations.csproj
+++ b/DigitalLearningSolutions.Data.Migrations/DigitalLearningSolutions.Data.Migrations.csproj
@@ -19,7 +19,7 @@
-
+
diff --git a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs
index 5b5d5f74dd..79fe3eb560 100644
--- a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs
+++ b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs
@@ -2237,6 +2237,58 @@ internal static string TD_4634_Alter_GetCompletedCoursesForCandidate_UP {
}
}
+ ///
+ /// Looks up a localized string similar to
+ ///CREATE OR ALTER PROCEDURE usp_RenumberSelfAssessmentStructure
+ /// @SelfAssessmentID INT
+ ///AS
+ ///BEGIN
+ /// SET NOCOUNT ON;
+ ///
+ /// /*
+ /// Step 1: Build an ordered list of groups
+ /// - Each group is ranked by its current Min(Ordering)
+ /// - Ungrouped competencies (NULL CompetencyGroupID) are treated as their own "pseudo group"
+ /// */
+ /// ;WITH GroupRanks AS (
+ /// SELECT
+ /// CompetencyGroupID,
+ /// ROW_NUMBER() OVER (ORDER BY MIN(Ordering)) AS GroupRank
+ /// FROM Sel [rest of string was truncated]";.
+ ///
+ internal static string TD_483_uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP {
+ get {
+ return ResourceManager.GetString("TD-483-uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to CREATE OR ALTER PROCEDURE usp_MoveCompetencyInSelfAssessment
+ /// @SelfAssessmentID INT,
+ /// @CompetencyID INT,
+ /// @Direction NVARCHAR(10)
+ ///AS
+ ///BEGIN
+ /// SET NOCOUNT ON;
+ ///
+ /// DECLARE @GroupID INT, @CurrentOrder INT;
+ ///
+ /// SELECT
+ /// @GroupID = CompetencyGroupID,
+ /// @CurrentOrder = Ordering
+ /// FROM SelfAssessmentStructure
+ /// WHERE SelfAssessmentID = @SelfAssessmentID AND CompetencyID = @CompetencyID;
+ ///
+ /// IF @GroupID IS NULL
+ /// BEGIN
+ /// -- Can't reorder ungrouped competencies [rest of string was truncated]";.
+ ///
+ internal static string TD_483_uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP {
+ get {
+ return ResourceManager.GetString("TD-483-uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[GetActivitiesForDelegateEnrolment] Script Date: 22/10/2024 16:55:08 ******/
///SET ANSI_NULLS ON
@@ -2392,6 +2444,331 @@ internal static string TD_4950_dboGetOtherCentresForSelfAssessmentCreateOrAlter
}
}
+ ///
+ /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendExpiredTBCReminders] Script Date: 11/03/2025 13:13:15 ******/
+ ///SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///-- =============================================
+ ///-- Author: Kevin Whittaker
+ ///-- Create date: 17/08/2018
+ ///-- Description: Uses DB mail to send reminders to delegates on courses with a TBC date within 1 month.
+ ///-- =============================================
+ ///ALTER PROCEDURE [dbo].[SendExpiredTBCReminders]
+ /// -- Add the parameters for the stor [rest of string was truncated]";.
+ ///
+ internal static string TD_5412_Alter_SendExpiredTBCReminders_Down {
+ get {
+ return ResourceManager.GetString("TD_5412_Alter_SendExpiredTBCReminders_Down", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendExpiredTBCReminders] Script Date: 11/03/2025 13:13:15 ******/
+ ///SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///-- =============================================
+ ///-- Author: Kevin Whittaker
+ ///-- Create date: 17/08/2018
+ ///-- Description: Uses DB mail to send reminders to delegates on courses with a TBC date within 1 month.
+ ///-- =============================================
+ ///ALTER PROCEDURE [dbo].[SendExpiredTBCReminders]
+ /// -- Add the parameters for the stor [rest of string was truncated]";.
+ ///
+ internal static string TD_5412_Alter_SendExpiredTBCReminders_Up {
+ get {
+ return ResourceManager.GetString("TD_5412_Alter_SendExpiredTBCReminders_Up", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[ReorderFrameworkCompetency] Script Date: 24/04/2025 09:23:17 ******/
+ ///SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///-- =============================================
+ ///-- Author: Kevin Whittaker
+ ///-- Create date: 04/01/2021
+ ///-- Description: Reorders the FrameworkCompetencies in a given FrameworkCompetencyGroup - moving the given competency up or down.
+ ///-- =============================================
+ ///ALTER PROCEDURE [dbo].[ReorderFrameworkCompetency]
+ /// [rest of string was truncated]";.
+ ///
+ internal static string TD_5447_Alter_ReorderFrameworkCompetency_Down {
+ get {
+ return ResourceManager.GetString("TD_5447_Alter_ReorderFrameworkCompetency_Down", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[ReorderFrameworkCompetency] Script Date: 24/04/2025 09:23:17 ******/
+ ///SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///-- =============================================
+ ///-- Author: Kevin Whittaker
+ ///-- Create date: 04/01/2021
+ ///-- Description: Reorders the FrameworkCompetencies in a given FrameworkCompetencyGroup - moving the given competency up or down.
+ ///-- =============================================
+ ///ALTER PROCEDURE [dbo].[ReorderFrameworkCompetency]
+ /// [rest of string was truncated]";.
+ ///
+ internal static string TD_5447_Alter_ReorderFrameworkCompetency_Up {
+ get {
+ return ResourceManager.GetString("TD_5447_Alter_ReorderFrameworkCompetency_Up", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendExpiredTBCReminders] Script Date: 16/04/2025 10:50:12 ******/
+ ///SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///-- =============================================
+ ///-- Author: Kevin Whittaker
+ ///-- Create date: 17/08/2018
+ ///-- Description: Uses DB mail to send reminders to delegates on courses with a TBC date within 1 month.
+ ///-- =============================================
+ ///ALTER PROCEDURE [dbo].[SendExpiredTBCReminders]
+ /// -- Add the parameters for the stor [rest of string was truncated]";.
+ ///
+ internal static string TD_5514_Alter_SendExpiredTBCReminders_Down {
+ get {
+ return ResourceManager.GetString("TD_5514_Alter_SendExpiredTBCReminders_Down", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendExpiredTBCReminders] Script Date: 16/04/2025 10:50:12 ******/
+ ///SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///-- =============================================
+ ///-- Author: Kevin Whittaker
+ ///-- Create date: 17/08/2018
+ ///-- Description: Uses DB mail to send reminders to delegates on courses with a TBC date within 1 month.
+ ///-- =============================================
+ ///ALTER PROCEDURE [dbo].[SendExpiredTBCReminders]
+ /// -- Add the parameters for the stor [rest of string was truncated]";.
+ ///
+ internal static string TD_5514_Alter_SendExpiredTBCReminders_Up {
+ get {
+ return ResourceManager.GetString("TD_5514_Alter_SendExpiredTBCReminders_Up", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[GetActivitiesForDelegateEnrolment] Script Date: 24/07/2025 02:06:43 ******/
+ ///SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///-- =============================================
+ ///-- Author: Kevin Whittaker
+ ///-- Create date: 24/01/2023
+ ///-- Description: Returns active available for delegate enrolment based on original GetActiveAvailableCustomisationsForCentreFiltered_V6 sproc but adjusted for user account refactor and filters properly for category.
+ ///-- ========= [rest of string was truncated]";.
+ ///
+ internal static string TD_5535_Alter_GetActivitiesForDelegateEnrolment_Down {
+ get {
+ return ResourceManager.GetString("TD_5535_Alter_GetActivitiesForDelegateEnrolment_Down", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[GetActivitiesForDelegateEnrolment] Script Date: 24/07/2025 02:06:43 ******/
+ ///SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///-- =============================================
+ ///-- Author: Kevin Whittaker
+ ///-- Create date: 24/01/2023
+ ///-- Description: Returns active available for delegate enrolment based on original GetActiveAvailableCustomisationsForCentreFiltered_V6 sproc but adjusted for user account refactor and filters properly for category.
+ ///-- ========= [rest of string was truncated]";.
+ ///
+ internal static string TD_5535_Alter_GetActivitiesForDelegateEnrolment_Up {
+ get {
+ return ResourceManager.GetString("TD_5535_Alter_GetActivitiesForDelegateEnrolment_Up", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendRetiringSelfAssessmentNotification] Script Date: 18/09/2025 09:03:21 ******/
+ ///SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///-- =============================================
+ ///-- Author: Auldrin Possa
+ ///-- Create date: 04/08/2015
+ ///-- Description: Uses DB mail to send notification to delegates on retiring self assessment.
+ ///-- =============================================
+ ///ALTER PROCEDURE [dbo].[SendRetiringSelfAssessmentNotification]
+ /// @SelfAssessmentId [rest of string was truncated]";.
+ ///
+ internal static string TD_5552_Alter_SendRetiringSelfAssessmentNotification_Down {
+ get {
+ return ResourceManager.GetString("TD_5552_Alter_SendRetiringSelfAssessmentNotification_Down", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[SendRetiringSelfAssessmentNotification] Script Date: 18/09/2025 09:03:21 ******/
+ ///SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///-- =============================================
+ ///-- Author: Auldrin Possa
+ ///-- Create date: 04/08/2015
+ ///-- Description: Uses DB mail to send notification to delegates on retiring self assessment.
+ ///-- =============================================
+ ///ALTER PROCEDURE [dbo].[SendRetiringSelfAssessmentNotification]
+ /// @SelfAssessmentId [rest of string was truncated]";.
+ ///
+ internal static string TD_5552_Alter_SendRetiringSelfAssessmentNotification_Up {
+ get {
+ return ResourceManager.GetString("TD_5552_Alter_SendRetiringSelfAssessmentNotification_Up", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to SET ANSI_NULLS ON
+ ///GO
+ ///
+ ///SET QUOTED_IDENTIFIER ON
+ ///GO
+ ///
+ ///
+ ///
+ ///-- =============================================
+ ///-- Author: Auldrin Possa
+ ///-- Create date: 04/08/2015
+ ///-- Description: Uses DB mail to send notification to delegates on retiring self assessment.
+ ///-- =============================================
+ ///CREATE PROCEDURE [dbo].[SendRetiringSelfAssessmentNotification]
+ /// @SelfAssessmentId int,
+ /// @TestOnly bit
+ ///AS
+ ///BEGIN
+ /// -- SET NOCOUNT ON added to prevent extra result sets from
+ /// -- interfering with SELEC [rest of string was truncated]";.
+ ///
+ internal static string TD_5552_SendRetiringNotification {
+ get {
+ return ResourceManager.GetString("TD_5552_SendRetiringNotification", resourceCulture);
+ }
+ }
+
+ ///
+ /// 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.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;
+ ///.
+ ///
+ internal static string TD_5670_MaintenanceScripts_DOWN {
+ get {
+ return ResourceManager.GetString("TD-5670-MaintenanceScripts_DOWN", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to -- ============================================
+ ///-- 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
+ ///
+ ///-- =========== [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 PROCEDURE [dbo].[usp_GetSelfAssessmentReport]
+ /// @SelfAssessmentID INT,
+ /// @CentreID INT
+ ///AS
+ ///BEGIN
+ /// SET NOCOUNT ON;
+ ///
+ /// -- Step 1: Materialize the LatestAssessmentResults into a temp table
+ /// IF OBJECT_ID('tempdb..#LatestAssessmentResults') IS NOT NULL
+ /// DROP TABLE #LatestAssessmentResults;
+ ///
+ /// SELECT
+ /// s.DelegateUserID,
+ /// CASE WHEN COALESCE(rr.LevelRAG, 0) = 3 THEN s.ID ELSE NULL END AS SelfAssessed,
+ /// CASE
+ /// WHEN sv.Verified IS NOT N [rest of string was truncated]";.
+ ///
+ internal static string TD_5759_CreateOrAlterSelfAssessmentReportSPandTVF_Fix_UP {
+ get {
+ return ResourceManager.GetString("TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF-Fix_UP", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to CREATE OR ALTER FUNCTION dbo.GetOtherCentresForSelfAssessmentTVF
+ ///(
+ /// @UserID INT,
+ /// @SelfAssessmentID INT,
+ /// @ExcludeCentreID INT
+ ///)
+ ///RETURNS TABLE
+ ///AS
+ ///RETURN
+ ///(
+ /// SELECT
+ /// STUFF((
+ /// SELECT DISTINCT
+ /// ', ' + c.CentreName
+ /// FROM Users AS u
+ /// INNER JOIN DelegateAccounts AS da ON u.ID = da.UserID
+ /// INNER JOIN Centres AS c ON da.CentreID = c.CentreID
+ /// INNER JOIN CentreSelfAssessments AS csa ON c.CentreID = csa.CentreID [rest of string was truncated]";.
+ ///
+ internal static string TD_5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP {
+ get {
+ return ResourceManager.GetString("TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[GetActiveAvailableCustomisationsForCentreFiltered_V6] Script Date: 29/09/2022 19:11:04 ******/
///SET ANSI_NULLS ON
diff --git a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx
index c2c733076c..c1c2673d2f 100644
--- a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx
+++ b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx
@@ -469,13 +469,55 @@
..\Scripts\TD_4950_Alter_GetAssessmentResultsByDelegate_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
-
- ..\Resources\TD-4950-dboGetOtherCentresForSelfAssessmentCreateOrAlter.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
+
+ ..\Scripts\TD-5412-Alter_SendExpiredTBCReminders_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
-
- ..\Scripts\TD-4878-Alter_GetActivitiesForDelegateEnrolment_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
+
+ ..\Scripts\TD-5412-Alter_SendExpiredTBCReminders_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
-
- ..\Scripts\TD-4878-Alter_GetActivitiesForDelegateEnrolment_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
+
+ ..\Scripts\TD-5514-Alter_SendExpiredTBCReminders_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
+
+
+ ..\Scripts\TD-5514-Alter_SendExpiredTBCReminders_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
+
+
+ ..\Scripts\TD-5447-Alter_ReorderFrameworkCompetency_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
+
+
+ ..\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
+
+
+ ..\Scripts\TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF-Fix_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
+
+
+ ..\Scripts\TD-483-uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
+
+
+ ..\Scripts\TD-483-uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
+
+
+ ..\Scripts\TD-5535-Alter_GetActivitiesForDelegateEnrolment_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
+
+
+ ..\Scripts\TD-5535-Alter_GetActivitiesForDelegateEnrolment_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
+
+
+ ..\Scripts\TD-5552-SendRetiringNotification.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
+
+
+ ..\Scripts\TD-5552-Alter_SendRetiringSelfAssessmentNotification_Down.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
+
+
+ ..\Scripts\TD-5552-Alter_SendRetiringSelfAssessmentNotification_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16
\ No newline at end of file
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP.sql
new file mode 100644
index 0000000000..f42d8894c7
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyGroupInSelfAssessmentCreateOrAlter_UP.sql
@@ -0,0 +1,164 @@
+
+CREATE OR ALTER PROCEDURE usp_RenumberSelfAssessmentStructure
+ @SelfAssessmentID INT
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ /*
+ Step 1: Build an ordered list of groups
+ - Each group is ranked by its current Min(Ordering)
+ - Ungrouped competencies (NULL CompetencyGroupID) are treated as their own "pseudo group"
+ */
+ ;WITH GroupRanks AS (
+ SELECT
+ CompetencyGroupID,
+ ROW_NUMBER() OVER (ORDER BY MIN(Ordering)) AS GroupRank
+ FROM SelfAssessmentStructure
+ WHERE SelfAssessmentID = @SelfAssessmentID
+ GROUP BY CompetencyGroupID
+ )
+ /*
+ Step 2: Renumber groups and competencies
+ - Groups get ranked (1,2,3)
+ - Within each group, competencies are ordered by their current Ordering and renumbered (1,2,3)
+ */
+ UPDATE sas
+ SET sas.Ordering = rn.NewOrdering
+ FROM SelfAssessmentStructure sas
+ INNER JOIN (
+ SELECT
+ s.ID,
+ -- new group position * 1000 + row within group
+ -- gives room between groups and avoids clashes
+ (g.GroupRank * 1000) +
+ ROW_NUMBER() OVER (PARTITION BY s.CompetencyGroupID, g.GroupRank ORDER BY s.Ordering, s.ID) AS NewOrdering
+ FROM SelfAssessmentStructure s
+ INNER JOIN GroupRanks g
+ ON g.CompetencyGroupID = s.CompetencyGroupID
+ WHERE s.SelfAssessmentID = @SelfAssessmentID
+ ) rn ON sas.ID = rn.ID;
+
+END
+
+GO
+
+CREATE OR ALTER PROCEDURE usp_MoveCompetencyGroupInSelfAssessment
+ @SelfAssessmentID INT,
+ @GroupID INT,
+ @Direction NVARCHAR(10) -- 'up' or 'down'
+AS
+BEGIN
+ SET NOCOUNT ON;
+ SET XACT_ABORT ON;
+
+ BEGIN TRY
+ BEGIN TRAN;
+
+ /* 1) Rank groups by current Min(Ordering) (NULL groups excluded here; include if desired). */
+ ;WITH GroupRanks AS (
+ SELECT
+ CompetencyGroupID,
+ MIN(Ordering) AS MinOrder,
+ ROW_NUMBER() OVER (
+ ORDER BY MIN(Ordering), MIN(CompetencyGroupID)
+ ) AS RankPos
+ FROM SelfAssessmentStructure
+ WHERE SelfAssessmentID = @SelfAssessmentID
+ AND CompetencyGroupID IS NOT NULL
+ GROUP BY CompetencyGroupID
+ )
+ SELECT *
+ INTO #Groups
+ FROM GroupRanks;
+
+ DECLARE @CurRank INT, @SwapRank INT, @SwapGroupID INT;
+
+ SELECT @CurRank = RankPos
+ FROM #Groups
+ WHERE CompetencyGroupID = @GroupID;
+
+ IF @CurRank IS NULL
+ BEGIN
+ DROP TABLE #Groups;
+ COMMIT TRAN; RETURN; -- nothing to do
+ END
+
+ IF LOWER(@Direction) = 'up'
+ SET @SwapRank = @CurRank - 1;
+ ELSE IF LOWER(@Direction) = 'down'
+ SET @SwapRank = @CurRank + 1;
+ ELSE
+ BEGIN
+ DROP TABLE #Groups;
+ ROLLBACK TRAN; THROW 50000, 'Direction must be ''up'' or ''down''.', 1;
+ END
+
+ SELECT @SwapGroupID = CompetencyGroupID
+ FROM #Groups
+ WHERE RankPos = @SwapRank;
+
+ IF @SwapGroupID IS NULL
+ BEGIN
+ DROP TABLE #Groups;
+ COMMIT TRAN; RETURN; -- already at top/bottom
+ END
+
+ /* 2) Build a mapping where ONLY the two groups swap ranks; others keep theirs. */
+ SELECT
+ g.CompetencyGroupID,
+ CASE
+ WHEN g.CompetencyGroupID = @GroupID THEN @SwapRank
+ WHEN g.CompetencyGroupID = @SwapGroupID THEN @CurRank
+ ELSE g.RankPos
+ END AS NewRank
+ INTO #RankMap
+ FROM #Groups g;
+
+ /* 3) Choose a block size big enough to keep groups separated when we recompute.
+ Using count(rows in this self assessment) + 10 is a safe dynamic choice. */
+ DECLARE @Block INT =
+ (SELECT COUNT(*) FROM SelfAssessmentStructure WHERE SelfAssessmentID = @SelfAssessmentID) + 10;
+
+ /* 4) Recompute EVERY rows Ordering from the rank map (this sets the new global order).
+ We preserve within-group relative order using the existing Ordering (and ID as tiebreak). */
+ ;WITH NewOrders AS (
+ SELECT
+ s.ID,
+ (m.NewRank * @Block)
+ + ROW_NUMBER() OVER (
+ PARTITION BY s.CompetencyGroupID
+ ORDER BY s.Ordering, s.ID
+ ) AS NewOrdering
+ FROM SelfAssessmentStructure s
+ JOIN #RankMap m
+ ON m.CompetencyGroupID = s.CompetencyGroupID
+ WHERE s.SelfAssessmentID = @SelfAssessmentID
+ )
+ UPDATE s
+ SET s.Ordering = n.NewOrdering
+ FROM SelfAssessmentStructure s
+ JOIN NewOrders n ON n.ID = s.ID;
+
+ /* 5) (Optional) Compress to 1..N while keeping the just-established relative order. */
+ ;WITH Ordered AS (
+ SELECT ID,
+ ROW_NUMBER() OVER (ORDER BY Ordering, ID) AS Seq
+ FROM SelfAssessmentStructure
+ WHERE SelfAssessmentID = @SelfAssessmentID
+ )
+ UPDATE s
+ SET s.Ordering = o.Seq
+ FROM SelfAssessmentStructure s
+ JOIN Ordered o ON o.ID = s.ID;
+
+ DROP TABLE #Groups;
+ DROP TABLE #RankMap;
+
+ COMMIT TRAN;
+ END TRY
+ BEGIN CATCH
+ IF @@TRANCOUNT > 0 ROLLBACK TRAN;
+ THROW;
+ END CATCH
+END;
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP.sql
new file mode 100644
index 0000000000..176422b549
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-483-uspMoveCompetencyInSelfAssessmentCreateOrAlter_UP.sql
@@ -0,0 +1,59 @@
+CREATE OR ALTER PROCEDURE usp_MoveCompetencyInSelfAssessment
+ @SelfAssessmentID INT,
+ @CompetencyID INT,
+ @Direction NVARCHAR(10)
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ DECLARE @GroupID INT, @CurrentOrder INT;
+
+ SELECT
+ @GroupID = CompetencyGroupID,
+ @CurrentOrder = Ordering
+ FROM SelfAssessmentStructure
+ WHERE SelfAssessmentID = @SelfAssessmentID AND CompetencyID = @CompetencyID;
+
+ IF @GroupID IS NULL
+ BEGIN
+ -- Can't reorder ungrouped competencies via group-based logic
+ RETURN;
+ END
+
+ DECLARE @TargetCompetencyID INT, @TargetOrder INT;
+
+ IF @Direction = 'up'
+ BEGIN
+ SELECT TOP 1
+ @TargetCompetencyID = CompetencyID,
+ @TargetOrder = Ordering
+ FROM SelfAssessmentStructure
+ WHERE SelfAssessmentID = @SelfAssessmentID
+ AND CompetencyGroupID = @GroupID
+ AND Ordering < @CurrentOrder
+ ORDER BY Ordering DESC;
+ END
+ ELSE IF @Direction = 'down'
+ BEGIN
+ SELECT TOP 1
+ @TargetCompetencyID = CompetencyID,
+ @TargetOrder = Ordering
+ FROM SelfAssessmentStructure
+ WHERE SelfAssessmentID = @SelfAssessmentID
+ AND CompetencyGroupID = @GroupID
+ AND Ordering > @CurrentOrder
+ ORDER BY Ordering ASC;
+ END
+
+ IF @TargetCompetencyID IS NOT NULL
+ BEGIN
+ -- Swap the orderings
+ UPDATE SelfAssessmentStructure
+ SET Ordering = @TargetOrder
+ WHERE SelfAssessmentID = @SelfAssessmentID AND CompetencyID = @CompetencyID;
+
+ UPDATE SelfAssessmentStructure
+ SET Ordering = @CurrentOrder
+ WHERE SelfAssessmentID = @SelfAssessmentID AND CompetencyID = @TargetCompetencyID;
+ END
+END
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Down.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Down.sql
new file mode 100644
index 0000000000..7ca4a74075
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Down.sql differ
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Up.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Up.sql
new file mode 100644
index 0000000000..3ff6d79f99
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5412-Alter_SendExpiredTBCReminders_Up.sql differ
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Down.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Down.sql
new file mode 100644
index 0000000000..56bac72b67
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Down.sql differ
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Up.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Up.sql
new file mode 100644
index 0000000000..bd170f6b9e
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5447-Alter_ReorderFrameworkCompetency_Up.sql differ
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Down.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Down.sql
new file mode 100644
index 0000000000..f80def01d5
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Down.sql differ
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Up.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Up.sql
new file mode 100644
index 0000000000..448ba42946
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5514-Alter_SendExpiredTBCReminders_Up.sql differ
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Down.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Down.sql
new file mode 100644
index 0000000000..d2f8374a09
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Down.sql differ
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Up.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Up.sql
new file mode 100644
index 0000000000..0e8c76138f
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5535-Alter_GetActivitiesForDelegateEnrolment_Up.sql differ
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Down.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Down.sql
new file mode 100644
index 0000000000..c416e54e50
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Down.sql differ
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Up.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Up.sql
new file mode 100644
index 0000000000..6ee6c7c21c
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-Alter_SendRetiringSelfAssessmentNotification_Up.sql differ
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-SendRetiringNotification.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-SendRetiringNotification.sql
new file mode 100644
index 0000000000..a5c5437375
Binary files /dev/null and b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5552-SendRetiringNotification.sql differ
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.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF-Fix_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF-Fix_UP.sql
new file mode 100644
index 0000000000..21922679ff
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF-Fix_UP.sql
@@ -0,0 +1,117 @@
+CREATE OR ALTER PROCEDURE [dbo].[usp_GetSelfAssessmentReport]
+ @SelfAssessmentID INT,
+ @CentreID INT
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ -- Step 1: Materialize the LatestAssessmentResults into a temp table
+ IF OBJECT_ID('tempdb..#LatestAssessmentResults') IS NOT NULL
+ DROP TABLE #LatestAssessmentResults;
+
+ SELECT
+ s.DelegateUserID,
+ CASE WHEN COALESCE(rr.LevelRAG, 0) = 3 THEN s.ID ELSE NULL END AS SelfAssessed,
+ CASE
+ WHEN sv.Verified IS NOT NULL AND sv.SignedOff = 1 AND COALESCE(rr.LevelRAG, 0) = 3
+ THEN s.ID ELSE NULL
+ END AS Confirmed,
+ CASE WHEN sas.Optional = 1 THEN s.CompetencyID ELSE NULL END AS Optional
+ INTO #LatestAssessmentResults
+ FROM SelfAssessmentResults AS s
+ LEFT JOIN SelfAssessmentStructure AS sas
+ ON sas.SelfAssessmentID = @SelfAssessmentID
+ AND s.CompetencyID = sas.CompetencyID
+ LEFT JOIN SelfAssessmentResultSupervisorVerifications AS sv
+ ON s.ID = sv.SelfAssessmentResultId
+ AND sv.Superceded = 0
+ LEFT JOIN CompetencyAssessmentQuestionRoleRequirements AS rr
+ ON s.CompetencyID = rr.CompetencyID
+ AND s.AssessmentQuestionID = rr.AssessmentQuestionID
+ AND sas.SelfAssessmentID = rr.SelfAssessmentID
+ AND s.Result = rr.LevelValue
+ WHERE sas.SelfAssessmentID = @SelfAssessmentID;
+
+ CREATE NONCLUSTERED INDEX IX_LAR_DelegateUserID ON #LatestAssessmentResults(DelegateUserID);
+
+ -- Step 2: Run the main query
+ SELECT
+ sa.Name AS SelfAssessment,
+ u.LastName + ', ' + u.FirstName AS Learner,
+ da.Active AS LearnerActive,
+ u.ProfessionalRegistrationNumber AS PRN,
+ jg.JobGroupName AS JobGroup,
+ da.Answer1 AS RegistrationAnswer1,
+ da.Answer2 AS RegistrationAnswer2,
+ da.Answer3 AS RegistrationAnswer3,
+ da.Answer4 AS RegistrationAnswer4,
+ da.Answer5 AS RegistrationAnswer5,
+ da.Answer6 AS RegistrationAnswer6,
+ oc.OtherCentres,
+ CASE
+ WHEN aa.ID IS NULL THEN 'Learner'
+ WHEN aa.IsCentreManager = 1 THEN 'Centre Manager'
+ WHEN aa.IsCentreAdmin = 1 AND aa.IsCentreManager = 0 THEN 'Centre Admin'
+ WHEN aa.IsSupervisor = 1 THEN 'Supervisor'
+ WHEN aa.IsNominatedSupervisor = 1 THEN 'Nominated supervisor'
+ END AS DLSRole,
+ da.DateRegistered AS Registered,
+ ca.StartedDate AS [Started],
+ ca.LastAccessed,
+ COUNT(DISTINCT LAR.Optional) AS [OptionalProficienciesAssessed],
+ COUNT(DISTINCT LAR.SelfAssessed) AS [SelfAssessedAchieved],
+ COUNT(DISTINCT LAR.Confirmed) AS [ConfirmedResults],
+ MAX(casv.Requested) AS SignOffRequested,
+ MAX(1 * casv.SignedOff) AS SignOffAchieved,
+ MIN(casv.Verified) AS ReviewedDate
+ FROM CandidateAssessments AS ca
+ INNER JOIN DelegateAccounts AS da
+ ON ca.DelegateUserID = da.UserID
+ AND da.CentreID = @CentreID
+ INNER JOIN Users AS u
+ ON u.ID = da.UserID
+ INNER JOIN SelfAssessments AS sa
+ ON ca.SelfAssessmentID = sa.ID
+ INNER JOIN CentreSelfAssessments AS csa
+ ON sa.ID = csa.SelfAssessmentID
+ INNER JOIN Centres AS c
+ ON csa.CentreID = c.CentreID
+ AND da.CentreID = c.CentreID
+ INNER JOIN JobGroups AS jg
+ ON u.JobGroupID = jg.JobGroupID
+ LEFT JOIN AdminAccounts AS aa
+ ON da.UserID = aa.UserID
+ AND aa.CentreID = da.CentreID
+ AND aa.Active = 1
+ LEFT JOIN CandidateAssessmentSupervisors AS cas
+ ON ca.ID = cas.CandidateAssessmentID
+ LEFT JOIN CandidateAssessmentSupervisorVerifications AS casv
+ ON casv.CandidateAssessmentSupervisorID = cas.ID
+ LEFT JOIN SupervisorDelegates AS sd
+ ON cas.SupervisorDelegateId = sd.ID
+ LEFT JOIN #LatestAssessmentResults AS LAR
+ ON LAR.DelegateUserID = ca.DelegateUserID
+ OUTER APPLY dbo.GetOtherCentresForSelfAssessmentTVF(da.UserID, @SelfAssessmentID, c.CentreID) AS oc
+ WHERE
+ sa.ID = @SelfAssessmentID
+ AND sa.ArchivedDate IS NULL
+ AND c.Active = 1
+ AND ca.RemovedDate IS NULL
+ AND ca.NonReportable = 0
+ GROUP BY
+ sa.Name,
+ u.LastName + ', ' + u.FirstName,
+ da.Active,
+ u.ProfessionalRegistrationNumber,
+ jg.JobGroupName,
+ da.Answer1, da.Answer2, da.Answer3, da.Answer4, da.Answer5, da.Answer6,
+ da.DateRegistered,
+ ca.StartedDate,
+ ca.LastAccessed,
+ oc.OtherCentres,
+ aa.ID, aa.IsCentreManager, aa.IsCentreAdmin, aa.IsSupervisor, aa.IsNominatedSupervisor
+ ORDER BY
+ sa.Name, u.LastName + ', ' + u.FirstName;
+
+END;
+GO
\ No newline at end of file
diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP.sql
new file mode 100644
index 0000000000..eb99526d7a
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5759_CreateOrAlterSelfAssessmentReportSPandTVF_UP.sql
@@ -0,0 +1,145 @@
+CREATE OR ALTER FUNCTION dbo.GetOtherCentresForSelfAssessmentTVF
+(
+ @UserID INT,
+ @SelfAssessmentID INT,
+ @ExcludeCentreID INT
+)
+RETURNS TABLE
+AS
+RETURN
+(
+ SELECT
+ STUFF((
+ SELECT DISTINCT
+ ', ' + c.CentreName
+ FROM Users AS u
+ INNER JOIN DelegateAccounts AS da ON u.ID = da.UserID
+ INNER JOIN Centres AS c ON da.CentreID = c.CentreID
+ INNER JOIN CentreSelfAssessments AS csa ON c.CentreID = csa.CentreID
+ WHERE u.ID = @UserID
+ AND da.Active = 1
+ AND da.Approved = 1
+ AND csa.SelfAssessmentID = @SelfAssessmentID
+ AND c.CentreID <> @ExcludeCentreID
+ FOR XML PATH(''), TYPE
+ ).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS OtherCentres
+);
+GO
+
+CREATE OR ALTER PROCEDURE [dbo].[usp_GetSelfAssessmentReport]
+ @SelfAssessmentID INT,
+ @CentreID INT
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ -- Step 1: Materialize the LatestAssessmentResults into a temp table
+ IF OBJECT_ID('tempdb..#LatestAssessmentResults') IS NOT NULL
+ DROP TABLE #LatestAssessmentResults;
+
+ SELECT
+ s.DelegateUserID,
+ CASE WHEN COALESCE(rr.LevelRAG, 0) = 3 THEN s.ID ELSE NULL END AS SelfAssessed,
+ CASE
+ WHEN sv.Verified IS NOT NULL AND sv.SignedOff = 1 AND COALESCE(rr.LevelRAG, 0) = 3
+ THEN s.ID ELSE NULL
+ END AS Confirmed,
+ CASE WHEN sas.Optional = 1 THEN s.CompetencyID ELSE NULL END AS Optional
+ INTO #LatestAssessmentResults
+ FROM SelfAssessmentResults AS s
+ LEFT JOIN SelfAssessmentStructure AS sas
+ ON sas.SelfAssessmentID = @SelfAssessmentID
+ AND s.CompetencyID = sas.CompetencyID
+ LEFT JOIN SelfAssessmentResultSupervisorVerifications AS sv
+ ON s.ID = sv.SelfAssessmentResultId
+ AND sv.Superceded = 0
+ LEFT JOIN CompetencyAssessmentQuestionRoleRequirements AS rr
+ ON s.CompetencyID = rr.CompetencyID
+ AND s.AssessmentQuestionID = rr.AssessmentQuestionID
+ AND sas.SelfAssessmentID = rr.SelfAssessmentID
+ AND s.Result = rr.LevelValue
+ WHERE sas.SelfAssessmentID = @SelfAssessmentID;
+
+ CREATE NONCLUSTERED INDEX IX_LAR_DelegateUserID ON #LatestAssessmentResults(DelegateUserID);
+
+ -- Step 2: Run the main query
+ SELECT
+ sa.Name AS SelfAssessment,
+ u.LastName + ', ' + u.FirstName AS Learner,
+ da.Active AS LearnerActive,
+ u.ProfessionalRegistrationNumber AS PRN,
+ jg.JobGroupName AS JobGroup,
+ da.Answer1,
+ da.Answer2,
+ da.Answer3,
+ da.Answer4,
+ da.Answer5,
+ da.Answer6,
+ oc.OtherCentres,
+ CASE
+ WHEN aa.ID IS NULL THEN 'Learner'
+ WHEN aa.IsCentreManager = 1 THEN 'Centre Manager'
+ WHEN aa.IsCentreAdmin = 1 AND aa.IsCentreManager = 0 THEN 'Centre Admin'
+ WHEN aa.IsSupervisor = 1 THEN 'Supervisor'
+ WHEN aa.IsNominatedSupervisor = 1 THEN 'Nominated supervisor'
+ END AS DLSRole,
+ da.DateRegistered AS Registered,
+ ca.StartedDate,
+ ca.LastAccessed,
+ COUNT(DISTINCT LAR.Optional) AS [OptionalProficienciesAssessed],
+ COUNT(DISTINCT LAR.SelfAssessed) AS [SelfAssessedAchieved],
+ COUNT(DISTINCT LAR.Confirmed) AS [ConfirmedResults],
+ MAX(casv.Requested) AS SignOffRequested,
+ MAX(1 * casv.SignedOff) AS SignOffAchieved,
+ MIN(casv.Verified) AS ReviewedDate
+ FROM CandidateAssessments AS ca
+ INNER JOIN DelegateAccounts AS da
+ ON ca.DelegateUserID = da.UserID
+ AND da.CentreID = @CentreID
+ INNER JOIN Users AS u
+ ON u.ID = da.UserID
+ INNER JOIN SelfAssessments AS sa
+ ON ca.SelfAssessmentID = sa.ID
+ INNER JOIN CentreSelfAssessments AS csa
+ ON sa.ID = csa.SelfAssessmentID
+ INNER JOIN Centres AS c
+ ON csa.CentreID = c.CentreID
+ AND da.CentreID = c.CentreID
+ INNER JOIN JobGroups AS jg
+ ON u.JobGroupID = jg.JobGroupID
+ LEFT JOIN AdminAccounts AS aa
+ ON da.UserID = aa.UserID
+ AND aa.CentreID = da.CentreID
+ AND aa.Active = 1
+ LEFT JOIN CandidateAssessmentSupervisors AS cas
+ ON ca.ID = cas.CandidateAssessmentID
+ LEFT JOIN CandidateAssessmentSupervisorVerifications AS casv
+ ON casv.CandidateAssessmentSupervisorID = cas.ID
+ LEFT JOIN SupervisorDelegates AS sd
+ ON cas.SupervisorDelegateId = sd.ID
+ LEFT JOIN #LatestAssessmentResults AS LAR
+ ON LAR.DelegateUserID = ca.DelegateUserID
+ OUTER APPLY dbo.GetOtherCentresForSelfAssessmentTVF(da.UserID, @SelfAssessmentID, c.CentreID) AS oc
+ WHERE
+ sa.ID = @SelfAssessmentID
+ AND sa.ArchivedDate IS NULL
+ AND c.Active = 1
+ AND ca.RemovedDate IS NULL
+ AND ca.NonReportable = 0
+ GROUP BY
+ sa.Name,
+ u.LastName + ', ' + u.FirstName,
+ da.Active,
+ u.ProfessionalRegistrationNumber,
+ jg.JobGroupName,
+ da.Answer1, da.Answer2, da.Answer3, da.Answer4, da.Answer5, da.Answer6,
+ da.DateRegistered,
+ ca.StartedDate,
+ ca.LastAccessed,
+ oc.OtherCentres,
+ aa.ID, aa.IsCentreManager, aa.IsCentreAdmin, aa.IsSupervisor, aa.IsNominatedSupervisor
+ ORDER BY
+ sa.Name, u.LastName + ', ' + u.FirstName;
+
+END;
+GO
\ No newline at end of file
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/CompetencyAssessmentDataService.cs b/DigitalLearningSolutions.Data/DataServices/CompetencyAssessmentDataService.cs
index 983bdba073..3c2cc31532 100644
--- a/DigitalLearningSolutions.Data/DataServices/CompetencyAssessmentDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/CompetencyAssessmentDataService.cs
@@ -1,16 +1,12 @@
namespace DigitalLearningSolutions.Data.DataServices
{
+ using Dapper;
+ using DigitalLearningSolutions.Data.Models.CompetencyAssessments;
+ using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
- using Dapper;
- using DigitalLearningSolutions.Data.Models.Common;
- using DigitalLearningSolutions.Data.Models.CompetencyAssessments;
- using DigitalLearningSolutions.Data.Models.Frameworks;
- using DocumentFormat.OpenXml.Wordprocessing;
- using Microsoft.Extensions.Logging;
-
public interface ICompetencyAssessmentDataService
{
//GET DATA
@@ -28,6 +24,18 @@ public interface ICompetencyAssessmentDataService
CompetencyAssessmentTaskStatus GetOrInsertAndReturnAssessmentTaskStatus(int assessmentId, bool frameworkBased);
+ int[] GetLinkedFrameworkIds(int assessmentId);
+
+ int? GetPrimaryLinkedFrameworkId(int assessmentId);
+
+ int GetCompetencyCountByFrameworkId(int assessmentId, int frameworkId);
+
+ IEnumerable GetCompetenciesForCompetencyAssessment(int competencyAssessmentId);
+ IEnumerable GetLinkedFrameworksForCompetencyAssessment(int competencyAssessmentId);
+ int[] GetLinkedFrameworkCompetencyIds(int competencyAssessmentId, int frameworkId);
+ CompetencyAssessmentFeatures? GetCompetencyAssessmentFeaturesTaskStatus(int competencyAssessmentId);
+ int? GetSelfAssessmentStructure(int competencyAssessmentId);
+
//UPDATE DATA
bool UpdateCompetencyAssessmentName(int competencyAssessmentId, int adminId, string competencyAssessmentName);
@@ -44,10 +52,31 @@ int categoryId
bool UpdateBrandingTaskStatus(int assessmentId, bool taskStatus);
bool UpdateVocabularyTaskStatus(int assessmentId, bool taskStatus);
bool UpdateRoleProfileLinksTaskStatus(int assessmentId, bool taskStatus);
+ bool UpdateFrameworkLinksTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus);
+ bool RemoveSelfAssessmentFramework(int assessmentId, int frameworkId, int adminId);
+ bool UpdateSelectCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus);
+ bool UpdateOptionalCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus);
+ bool UpdateRoleRequirementsTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus);
+ void MoveCompetencyInSelfAssessment(int competencyAssessmentId,
+ int competencyId,
+ string direction
+ );
+ void MoveCompetencyGroupInSelfAssessment(int competencyAssessmentId,
+ int groupId,
+ string direction
+ );
+ public bool UpdateCompetencyAssessmentFeaturesTaskStatus(int id, bool descriptionStatus, bool providerandCategoryStatus, bool vocabularyStatus,
+ bool workingGroupStatus, bool AllframeworkCompetenciesStatus);
+ void UpdateSelfAssessmentFromFramework(int selfAssessmentId, int? frameworkId);
+
//INSERT DATA
int InsertCompetencyAssessment(int adminId, int centreId, string competencyAssessmentName);
bool InsertSelfAssessmentFramework(int adminId, int selfAssessmentId, int frameworkId);
+ bool InsertCompetenciesIntoAssessmentFromFramework(int[] selectedCompetencyIds, int frameworkId, int competencyAssessmentId);
+ bool InsertSelfAssessmentStructure(int selfAssessmentId, int? frameworkId);
//DELETE DATA
+ bool RemoveFrameworkCompetenciesFromAssessment(int competencyAssessmentId, int frameworkId);
+ bool RemoveCompetencyFromAssessment(int competencyAssessmentId, int competencyId);
}
public class CompetencyAssessmentDataService : ICompetencyAssessmentDataService
@@ -77,13 +106,15 @@ FROM AdminUsers
sa.Archived,
sa.LastEdit,
STUFF((
- SELECT
- ', ' + f.FrameworkName
- FROM
- Frameworks f
- WHERE
- f.ID = saf.FrameworkId
- FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS LinkedFrameworks,
+ SELECT
+ ', ' + f.FrameworkName
+ FROM
+ SelfAssessmentFrameworks saf2
+ INNER JOIN Frameworks f ON f.ID = saf2.FrameworkId
+ WHERE
+ saf2.SelfAssessmentId = sa.ID
+ FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, ''
+ ) AS LinkedFrameworks,
(SELECT ProfessionalGroup
FROM NRPProfessionalGroups
WHERE (ID = sa.NRPProfessionalGroupID)) AS NRPProfessionalGroup,
@@ -100,8 +131,7 @@ FROM NRPRoles
private const string SelfAssessmentTables =
@" LEFT OUTER JOIN
- SelfAssessmentReviews AS sar ON sac.ID = sar.SelfAssessmentCollaboratorID AND sar.Archived IS NULL AND sar.ReviewComplete IS NULL
- LEFT OUTER JOIN SelfAssessmentFrameworks AS saf ON sa.ID = saf.SelfAssessmentId";
+ SelfAssessmentReviews AS sar ON sac.ID = sar.SelfAssessmentCollaboratorID AND sar.Archived IS NULL AND sar.ReviewComplete IS NULL";
private readonly IDbConnection connection;
private readonly ILogger logger;
@@ -324,13 +354,27 @@ public bool UpdateCompetencyAssessmentVocabulary(int competencyAssessmentId, int
public bool InsertSelfAssessmentFramework(int adminId, int selfAssessmentId, int frameworkId)
{
+ bool isPrimary = Convert.ToInt32(connection.ExecuteScalar(
+ @"SELECT Count(1) FROM SelfAssessmentFrameworks
+ WHERE SelfAssessmentId = @selfAssessmentId AND IsPrimary = 1", new { selfAssessmentId })) == 0;
+
var numberOfAffectedRows = connection.Execute(
- @"INSERT INTO SelfAssessmentFrameworks (SelfAssessmentId, FrameworkId, CreatedByAdminId)
- SELECT @selfAssessmentId, @frameworkId, @adminId
+ @"INSERT INTO SelfAssessmentFrameworks (SelfAssessmentId, FrameworkId, CreatedByAdminId, IsPrimary)
+ SELECT @selfAssessmentId, @frameworkId, @adminId, @isPrimary
WHERE NOT EXISTS (SELECT 1 FROM SelfAssessmentFrameworks WHERE SelfAssessmentId = @selfAssessmentId AND FrameworkId = @frameworkId)"
+ ,
+ new { adminId, selfAssessmentId, frameworkId, isPrimary }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ numberOfAffectedRows = connection.Execute(
+ @"UPDATE SelfAssessmentFrameworks
+ SET RemovedDate = NULL, RemovedByAdminId = NULL, AmendedByAdminId = @adminId
+ WHERE SelfAssessmentId = @selfAssessmentId AND FrameworkId = @frameworkId"
,
new { adminId, selfAssessmentId, frameworkId }
);
+ }
if (numberOfAffectedRows < 1)
{
logger.LogWarning(
@@ -459,5 +503,367 @@ public bool UpdateRoleProfileLinksTaskStatus(int assessmentId, bool taskStatus)
}
return true;
}
+
+ public int[] GetLinkedFrameworkIds(int assessmentId)
+ {
+ return [.. connection.Query(
+ @"SELECT FrameworkId
+ FROM SelfAssessmentFrameworks
+ WHERE (SelfAssessmentId = @assessmentId) AND (RemovedDate IS NULL) AND (IsPrimary = 0)
+ ORDER BY ID",
+ new { assessmentId }
+ )];
+ }
+
+ public bool RemoveSelfAssessmentFramework(int assessmentId, int frameworkId, int adminId)
+ {
+ var numberOfAffectedRows = connection.Execute(
+ @"UPDATE SelfAssessmentFrameworks SET RemovedDate = @removedDate, RemovedByAdminId = @adminId
+ WHERE SelfAssessmentId = @assessmentId AND FrameworkId = @frameworkId",
+ new { removedDate = DateTime.Now, assessmentId, frameworkId, adminId }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not updating SelfAssessmentFrameworks as db update failed. " +
+ $"assessmentId: {assessmentId}, frameworkId: {frameworkId}, adminId: {adminId}"
+ );
+ return false;
+ }
+ return true;
+ }
+
+ public int? GetPrimaryLinkedFrameworkId(int assessmentId)
+ {
+ return connection.QuerySingleOrDefault(
+ @"SELECT TOP(1) FrameworkId
+ FROM SelfAssessmentFrameworks
+ WHERE (SelfAssessmentId = @assessmentId) AND (RemovedDate IS NULL) AND (IsPrimary = 1)
+ ORDER BY ID DESC",
+ new { assessmentId }
+ );
+ }
+
+ public bool UpdateFrameworkLinksTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus)
+ {
+ var numberOfAffectedRows = connection.Execute(
+ @"UPDATE SelfAssessmentTaskStatus SET FrameworkLinksTaskStatus = @taskStatus
+ WHERE SelfAssessmentId = @assessmentId AND (@previousStatus IS NULL OR FrameworkLinksTaskStatus = @previousStatus)",
+ new { assessmentId, taskStatus, previousStatus }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not updating FrameworkLinksTaskStatus as db update failed. " +
+ $"assessmentId: {assessmentId}, taskStatus: {taskStatus}"
+ );
+ return false;
+ }
+ return true;
+ }
+
+ public int GetCompetencyCountByFrameworkId(int assessmentId, int frameworkId)
+ {
+ return connection.ExecuteScalar(
+ @"SELECT COUNT(sas.CompetencyID) AS Competencies
+ FROM SelfAssessmentStructure AS sas INNER JOIN
+ FrameworkCompetencies AS fc ON sas.CompetencyID = fc.CompetencyID INNER JOIN
+ SelfAssessmentFrameworks AS saf ON fc.FrameworkID = saf.FrameworkId AND sas.SelfAssessmentID = saf.SelfAssessmentId
+ WHERE (saf.SelfAssessmentId = @assessmentId) AND (saf.FrameworkId = @frameworkId)",
+ new { assessmentId, frameworkId }
+ );
+ }
+
+ public bool RemoveFrameworkCompetenciesFromAssessment(int competencyAssessmentId, int frameworkId)
+ {
+ var numberOfAffectedRows = connection.Execute(
+ @"DELETE FROM SelfAssessmentStructure
+ FROM SelfAssessmentStructure INNER JOIN
+ FrameworkCompetencies AS fc ON SelfAssessmentStructure.CompetencyID = fc.CompetencyID INNER JOIN
+ SelfAssessmentFrameworks AS saf ON fc.FrameworkID = saf.FrameworkId AND SelfAssessmentStructure.SelfAssessmentID = saf.SelfAssessmentId
+ WHERE (saf.SelfAssessmentId = @competencyAssessmentId) AND (saf.FrameworkId = @frameworkId)",
+ new { competencyAssessmentId, frameworkId }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not removing competencies linked to source framework as db update failed. " +
+ $"assessmentId: {competencyAssessmentId}, taskStatus: {frameworkId}"
+ );
+ return false;
+ }
+ return true;
+ }
+
+ public bool UpdateSelectCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus)
+ {
+ var numberOfAffectedRows = connection.Execute(
+ @"UPDATE SelfAssessmentTaskStatus SET SelectCompetenciesTaskStatus = @taskStatus
+ WHERE SelfAssessmentId = @assessmentId AND (@previousStatus IS NULL OR SelectCompetenciesTaskStatus = @previousStatus)",
+ new { assessmentId, taskStatus, previousStatus }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not updating SelectCompetenciesTaskStatus as db update failed. " +
+ $"assessmentId: {assessmentId}, taskStatus: {taskStatus}"
+ );
+ return false;
+ }
+ return true;
+ }
+ public bool UpdateOptionalCompetenciesTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus)
+ {
+ var numberOfAffectedRows = connection.Execute(
+ @"UPDATE SelfAssessmentTaskStatus SET OptionalCompetenciesTaskStatus = @taskStatus
+ WHERE SelfAssessmentId = @assessmentId AND (@previousStatus IS NULL OR OptionalCompetenciesTaskStatus = @previousStatus)",
+ new { assessmentId, taskStatus, previousStatus }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not updating OptionalCompetenciesTaskStatus as db update failed. " +
+ $"assessmentId: {assessmentId}, taskStatus: {taskStatus}"
+ );
+ return false;
+ }
+ return true;
+ }
+ public bool UpdateRoleRequirementsTaskStatus(int assessmentId, bool taskStatus, bool? previousStatus)
+ {
+ var numberOfAffectedRows = connection.Execute(
+ @"UPDATE SelfAssessmentTaskStatus SET RoleRequirementsTaskStatus = @taskStatus
+ WHERE SelfAssessmentId = @assessmentId AND (@previousStatus IS NULL OR RoleRequirementsTaskStatus = @previousStatus)",
+ new { assessmentId, taskStatus, previousStatus }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not updating RoleRequirementsTaskStatus as db update failed. " +
+ $"assessmentId: {assessmentId}, taskStatus: {taskStatus}"
+ );
+ return false;
+ }
+ return true;
+ }
+
+ public IEnumerable GetCompetenciesForCompetencyAssessment(int competencyAssessmentId)
+ {
+ return connection.Query(
+ @"SELECT sas.ID AS StructureId, sas.CompetencyID, f.ID AS FrameworkId, f.FrameworkName, cg.ID AS GroupId, cg.Name AS GroupName, c.Name AS CompetencyName, c.Description AS CompetencyDescription, sas.Optional
+ FROM SelfAssessmentStructure AS sas INNER JOIN
+ Competencies AS c ON sas.CompetencyID = c.ID INNER JOIN
+ CompetencyGroups AS cg ON sas.CompetencyGroupID = cg.ID INNER JOIN
+ FrameworkCompetencies ON c.ID = FrameworkCompetencies.CompetencyID INNER JOIN
+ Frameworks AS f ON FrameworkCompetencies.FrameworkID = f.ID INNER JOIN
+ SelfAssessmentFrameworks ON f.ID = SelfAssessmentFrameworks.FrameworkId AND sas.SelfAssessmentID = SelfAssessmentFrameworks.SelfAssessmentId
+ WHERE (sas.SelfAssessmentID = @competencyAssessmentId)
+ ORDER BY sas.Ordering", new { competencyAssessmentId }
+ );
+ }
+
+ public IEnumerable GetLinkedFrameworksForCompetencyAssessment(int competencyAssessmentId)
+ {
+ return connection.Query(
+ @"SELECT f.ID,
+ FrameworkName,
+ f.OwnerAdminID,
+ f.BrandID,
+ f.CategoryID,
+ f.TopicID,
+ f.CreatedDate,
+ f.PublishStatusID,
+ f.UpdatedByAdminID
+ FROM SelfAssessmentFrameworks saf INNER JOIN
+ Frameworks AS f ON saf.FrameworkId = f.ID
+ WHERE (saf.SelfAssessmentId = @competencyAssessmentId) AND (saf.RemovedDate IS NULL)
+ ORDER BY f.FrameworkName", new { competencyAssessmentId }
+ );
+ }
+
+ public int[] GetLinkedFrameworkCompetencyIds(int competencyAssessmentId, int frameworkId)
+ {
+ return [.. connection.Query(
+ @"SELECT sas.CompetencyID
+ FROM SelfAssessmentStructure AS sas INNER JOIN
+ FrameworkCompetencies AS fc ON sas.CompetencyID = fc.CompetencyID
+ WHERE (sas.SelfAssessmentID = @competencyAssessmentId) AND (fc.FrameworkID = @frameworkId)
+ ORDER BY fc.Ordering",
+ new { competencyAssessmentId, frameworkId}
+ )];
+ }
+
+ public bool InsertCompetenciesIntoAssessmentFromFramework(int[] selectedCompetencyIds, int frameworkId, int competencyAssessmentId)
+ {
+ var currentMaxOrdering = connection.ExecuteScalar(
+ @"SELECT ISNULL(MAX(Ordering), 0) FROM SelfAssessmentStructure WHERE SelfAssessmentID = @competencyAssessmentId",
+ new { competencyAssessmentId }
+ );
+ var numberOfAffectedRows = connection.Execute(
+ @"INSERT INTO SelfAssessmentStructure (SelfAssessmentID, CompetencyID, Ordering, CompetencyGroupID)
+ SELECT
+ @competencyAssessmentId,
+ FC.CompetencyID,
+ ROW_NUMBER() OVER (ORDER BY FCG.Ordering, FC.Ordering) + @currentMaxOrdering,
+ FCG.CompetencyGroupID
+ FROM FrameworkCompetencies AS FC
+ INNER JOIN FrameworkCompetencyGroups AS FCG ON FC.FrameworkCompetencyGroupID = FCG.ID
+ WHERE FC.FrameworkID = @frameworkId
+ AND FC.CompetencyID IN @selectedCompetencyIds AND FC.CompetencyID NOT IN (SELECT CompetencyID FROM SelfAssessmentStructure WHERE SelfAssessmentID = @competencyAssessmentId)",
+ new { selectedCompetencyIds, frameworkId, competencyAssessmentId, currentMaxOrdering }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not inserting competencies into assessment as db update failed. " +
+ $"assessmentId: {competencyAssessmentId}, frameworkId: {frameworkId}, selectedCompetencyIds: {selectedCompetencyIds}"
+ );
+ return false;
+ }
+ return true;
+ }
+ public bool RemoveCompetencyFromAssessment(int competencyAssessmentId, int competencyId)
+ {
+ var numberOfAffectedRows = connection.Execute(
+ @"DELETE FROM SelfAssessmentStructure
+ WHERE SelfAssessmentID = @competencyAssessmentId AND CompetencyID = @competencyId",
+ new { competencyAssessmentId, competencyId }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not removing competency from assessment as db update failed. " +
+ $"assessmentId: {competencyAssessmentId}, competencyId: {competencyId}"
+ );
+ return false;
+ }
+ return true;
+ }
+
+ public void MoveCompetencyInSelfAssessment(int competencyAssessmentId, int competencyId, string direction)
+ {
+ connection.Execute(
+ "usp_MoveCompetencyInSelfAssessment",
+ new { SelfAssessmentID = competencyAssessmentId, CompetencyID = competencyId, Direction = direction },
+ commandType: CommandType.StoredProcedure
+ );
+
+ }
+
+ public void MoveCompetencyGroupInSelfAssessment(int competencyAssessmentId, int groupId, string direction)
+ {
+ connection.Execute(
+ "usp_MoveCompetencyGroupInSelfAssessment",
+ new { SelfAssessmentID = competencyAssessmentId, GroupID = groupId, Direction = direction },
+ commandType: CommandType.StoredProcedure
+ );
+ }
+
+ public bool UpdateCompetencyAssessmentFeaturesTaskStatus(int id, bool descriptionStatus, bool providerandCategoryStatus, bool vocabularyStatus,
+ bool workingGroupStatus, bool AllframeworkCompetenciesStatus)
+ {
+ var numberOfAffectedRows = connection.Execute(
+ @"IF EXISTS (SELECT 1 FROM SelfAssessmentTaskStatus WHERE SelfAssessmentId = @id)
+ BEGIN
+ UPDATE SelfAssessmentTaskStatus
+ SET IntroductoryTextTaskStatus = CASE WHEN @descriptionStatus = 1 THEN 1 ELSE NULL END,
+ BrandingTaskStatus = CASE WHEN @providerandCategoryStatus = 1 THEN 1 ELSE NULL END,
+ VocabularyTaskStatus = CASE WHEN @vocabularyStatus = 1 THEN 1 ELSE NULL END,
+ WorkingGroupTaskStatus = CASE WHEN @workingGroupStatus = 1 THEN 1 ELSE NULL END,
+ FrameworkLinksTaskStatus = CASE WHEN @AllframeworkCompetenciesStatus = 1 THEN 1 ELSE NULL END
+ WHERE SelfAssessmentId = @id;
+ END
+ ELSE
+ BEGIN
+ INSERT INTO SelfAssessmentTaskStatus
+ (SelfAssessmentId, IntroductoryTextTaskStatus, BrandingTaskStatus, VocabularyTaskStatus, WorkingGroupTaskStatus, FrameworkLinksTaskStatus)
+ VALUES
+ (
+ @id,
+ CASE WHEN @descriptionStatus = 1 THEN 1 ELSE NULL END,
+ CASE WHEN @providerandCategoryStatus = 1 THEN 1 ELSE NULL END,
+ CASE WHEN @vocabularyStatus = 1 THEN 1 ELSE NULL END,
+ CASE WHEN @workingGroupStatus = 1 THEN 1 ELSE NULL END,
+ CASE WHEN @AllframeworkCompetenciesStatus = 1 THEN 1 ELSE NULL END
+ );
+ END",
+ new { id, descriptionStatus, providerandCategoryStatus, vocabularyStatus, workingGroupStatus, AllframeworkCompetenciesStatus }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not updating SelfAssessmentTaskStatus as db update failed. " +
+ $"SelfAssessmentId: {id}, IntroductoryTextTaskStatus: {descriptionStatus}, BrandingTaskStatus: {providerandCategoryStatus}, " +
+ $"VocabularyTaskStatus: {vocabularyStatus}, WorkingGroupTaskStatus: {workingGroupStatus}, FrameworkLinksTaskStatus: {AllframeworkCompetenciesStatus}"
+ );
+ return false;
+ }
+ return true;
+ }
+
+ public CompetencyAssessmentFeatures? GetCompetencyAssessmentFeaturesTaskStatus(int competencyAssessmentId)
+ {
+ return connection.QueryFirstOrDefault(
+ @"SELECT s.ID, s.Name AS CompetencyAssessmentName, sts.IntroductoryTextTaskStatus AS DescriptionStatus, sts.BrandingTaskStatus AS ProviderandCategoryStatus,
+ sts.VocabularyTaskStatus AS VocabularyStatus, sts.WorkingGroupTaskStatus AS WorkingGroupStatus, sts.FrameworkLinksTaskStatus AS AllframeworkCompetenciesStatus
+ FROM SelfAssessments s INNER JOIN
+ SelfAssessmentTaskStatus sts ON s.ID = sts.SelfAssessmentId
+ WHERE s.ID = @competencyAssessmentId",
+ new { competencyAssessmentId }
+ );
+
+ }
+
+ public void UpdateSelfAssessmentFromFramework( int selfAssessmentId, int? frameworkId)
+ {
+
+ var numberOfAffectedRows = connection.Execute(
+ @"UPDATE s
+ SET
+ [Description] = COALESCE(F.[Description], 'No description provided'),
+ BrandID = F.BrandID,
+ CategoryID = F.CategoryID,
+ CreatedByCentreID = AU.CentreID,
+ CreatedByAdminID = F.OwnerAdminID
+ FROM SelfAssessments s
+ INNER JOIN Frameworks F ON F.ID = @frameworkId
+ INNER JOIN AdminUsers AU ON F.OwnerAdminID = AU.AdminID
+ WHERE s.id = @selfAssessmentId;"
+ ,
+ new {selfAssessmentId, frameworkId }
+ );
+ }
+ public bool InsertSelfAssessmentStructure(int selfAssessmentId, int? frameworkId)
+ {
+
+ var numberOfAffectedRows = connection.Execute(
+ @"INSERT INTO SelfAssessmentStructure (SelfAssessmentID, CompetencyID, Ordering, CompetencyGroupID)
+ SELECT s.ID, FC.CompetencyID, ROW_NUMBER() OVER( ORDER BY FCG.Ordering, FC.Ordering ), FCG.CompetencyGroupID
+ FROM FrameworkCompetencies AS FC
+ INNER JOIN FrameworkCompetencyGroups AS FCG ON FC.FrameworkCompetencyGroupID = FCG.ID INNER JOIN
+ SelfAssessments s ON s.id = @selfAssessmentId
+ WHERE FC.FrameworkID = @frameworkId"
+ ,
+ new { selfAssessmentId, frameworkId }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not inserting SelfAssessmentStructure record as db insert failed. " +
+ $"selfAssessmentId: {selfAssessmentId}, frameworkId: {frameworkId}"
+ );
+ return false;
+ }
+
+ return true;
+ }
+ public int? GetSelfAssessmentStructure(int competencyAssessmentId)
+ {
+ return connection.QueryFirstOrDefault(
+ @"SELECT 1 from dbo.SelfAssessmentStructure where selfassessmentid = @competencyAssessmentId",
+ new { competencyAssessmentId }
+ );
+
+ }
}
}
diff --git a/DigitalLearningSolutions.Data/DataServices/CompetencyLearningResourcesDataService.cs b/DigitalLearningSolutions.Data/DataServices/CompetencyLearningResourcesDataService.cs
index 323188bf45..f00b2b1a71 100644
--- a/DigitalLearningSolutions.Data/DataServices/CompetencyLearningResourcesDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/CompetencyLearningResourcesDataService.cs
@@ -14,6 +14,7 @@ public interface ICompetencyLearningResourcesDataService
IEnumerable GetCompetencyResourceAssessmentQuestionParameters(IEnumerable competencyLearningResourceIds);
int AddCompetencyLearningResource(int resourceRefID, string originalResourceName, string description, string resourceType, string link, string catalogue, decimal rating, int competencyID, int adminId);
+ IEnumerable GetActiveCompetencyLearningResourcesByCompetencyIdAndReferenceId(int competencyId, int referenceId);
}
public class CompetencyLearningResourcesDataService : ICompetencyLearningResourcesDataService
@@ -120,5 +121,20 @@ FROM CompetencyResourceAssessmentQuestionParameters
new { competencyLearningResourceIds }
);
}
+ public IEnumerable GetActiveCompetencyLearningResourcesByCompetencyIdAndReferenceId(int competencyId, int referenceId)
+ {
+ return connection.Query(
+ @"SELECT
+ clr.ID,
+ clr.CompetencyID,
+ clr.LearningResourceReferenceID,
+ clr.AdminID,
+ lrr.ResourceRefID AS LearningHubResourceReferenceId
+ FROM CompetencyLearningResources AS clr
+ INNER JOIN LearningResourceReferences AS lrr ON lrr.ID = clr.LearningResourceReferenceID
+ WHERE CompetencyID = @competencyId AND ResourceRefID = @referenceId AND clr.RemovedDate IS NULL",
+ new { competencyId, referenceId }
+ );
+ }
}
}
diff --git a/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs b/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs
index 125a66aaca..79245643d8 100644
--- a/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/FrameworkDataService.cs
@@ -51,9 +51,9 @@ public interface IFrameworkDataService
CollaboratorNotification? GetCollaboratorNotification(int id, int invitedByAdminId);
// Competencies/groups:
- IEnumerable GetFrameworkCompetencyGroups(int frameworkId);
+ IEnumerable GetFrameworkCompetencyGroups(int frameworkId, int? assessmentId);
- IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId);
+ IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId, int? assessmentId);
CompetencyGroupBase? GetCompetencyGroupBaseById(int Id);
@@ -62,6 +62,7 @@ public interface IFrameworkDataService
int GetMaxFrameworkCompetencyID();
int GetMaxFrameworkCompetencyGroupID();
+ int GetFrameworkCompetencyGroupId(int frameworkId, int competencyGroupId);
// Assessment questions:
IEnumerable GetAllCompetencyQuestions(int adminId);
@@ -129,9 +130,9 @@ bool zeroBased
IEnumerable GetAllCompetenciesForAdminId(string name, int adminId);
- int InsertCompetency(string name, string? description, int adminId);
+ int InsertCompetency(string name, string? description, int adminId, bool alwaysShowDescription = false);
- int InsertFrameworkCompetency(int competencyId, int? frameworkCompetencyGroupID, int adminId, int frameworkId, bool alwaysShowDescription = false);
+ int InsertFrameworkCompetency(int competencyId, int? frameworkCompetencyGroupID, int adminId, int frameworkId, bool addDefaultQuestions = true);
int AddCollaboratorToFramework(int frameworkId, string userEmail, bool canModify, int? centreID);
@@ -198,7 +199,7 @@ int adminId
void UpdateFrameworkConfig(int frameworkId, int adminId, string? frameworkConfig);
- void UpdateFrameworkCompetencyGroup(
+ bool UpdateFrameworkCompetencyGroup(
int frameworkCompetencyGroupId,
int competencyGroupId,
string name,
@@ -251,9 +252,9 @@ string direction
void RemoveCustomFlag(int flagId);
void RemoveCollaboratorFromFramework(int frameworkId, int id);
- void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int adminId);
+ void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int frameworkId, int adminId);
- void DeleteFrameworkCompetency(int frameworkCompetencyId, int adminId);
+ void DeleteFrameworkCompetency(int frameworkCompetencyId, int? frameworkCompetencyGroupId, int frameworkId, int adminId);
void DeleteFrameworkDefaultQuestion(
int frameworkId,
@@ -265,6 +266,7 @@ bool deleteFromExisting
void DeleteCompetencyAssessmentQuestion(int frameworkCompetencyId, int assessmentQuestionId, int adminId);
void DeleteCompetencyLearningResource(int competencyLearningResourceId, int adminId);
+ void UpdateFrameworkCompetencyFrameworkCompetencyGroup(int? competencyGroupId, int frameworkCompetencyGroupId, int adminId);
}
public class FrameworkDataService : IFrameworkDataService
@@ -275,14 +277,37 @@ public class FrameworkDataService : IFrameworkDataService
OwnerAdminID,
(SELECT Forename + ' ' + Surname + (CASE WHEN Active = 1 THEN '' ELSE ' (Inactive)' END) AS Expr1 FROM AdminUsers WHERE (AdminID = FW.OwnerAdminID)) AS Owner,
BrandID,
- CategoryID,
+ FW.CategoryID,
TopicID,
CreatedDate,
PublishStatusID,
UpdatedByAdminID,
(SELECT Forename + ' ' + Surname + (CASE WHEN Active = 1 THEN '' ELSE ' (Inactive)' END) AS Expr1 FROM AdminUsers AS AdminUsers_1 WHERE (AdminID = FW.UpdatedByAdminID)) AS UpdatedBy,
- CASE WHEN FW.OwnerAdminID = @adminId THEN 3 WHEN fwc.CanModify = 1 THEN 2 WHEN fwc.CanModify = 0 THEN 1 ELSE 0 END AS UserRole,
- fwr.ID AS FrameworkReviewID";
+ CASE
+ WHEN (aa.UserID = (SELECT UserID FROM AdminAccounts WHERE ID = @adminId)) THEN 3
+ WHEN (fwc.CanModify = 1) OR
+ (SELECT COUNT(*)
+ FROM FrameworkCollaborators fc
+ JOIN AdminAccounts aa1 ON fc.AdminID = aa1.ID
+ WHERE fc.FrameworkID = fw.ID
+ AND fc.CanModify = 1 AND fc.IsDeleted = 0
+ AND aa1.UserID = (SELECT aa2.UserID FROM AdminAccounts aa2 WHERE aa2.ID = @adminId)) > 0 THEN 2
+ WHEN (fwc.CanModify = 0) OR
+ (SELECT COUNT(*)
+ FROM FrameworkCollaborators fc
+ JOIN AdminAccounts aa3 ON fc.AdminID = aa3.ID
+ WHERE fc.FrameworkID = fw.ID
+ AND fc.CanModify = 0 AND fc.IsDeleted = 0
+ AND aa3.UserID = (SELECT aa4.UserID FROM AdminAccounts aa4 WHERE aa4.ID = @adminId)) > 0 THEN 1
+ ELSE 0
+ END AS UserRole,
+ (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)
+ AND aa3.Active = 1 ORDER BY fwr.ID DESC) AS FrameworkReviewID";
private const string BrandedFrameworkFields =
@", FW.Description, FW.FrameworkConfig AS Vocabulary, (SELECT BrandName
@@ -302,9 +327,8 @@ FROM CourseTopics
private const string FlagFields = @"fl.ID AS FlagId, fl.FrameworkId, fl.FlagName, fl.FlagGroup, fl.FlagTagClass";
private const string FrameworkTables =
- @"Frameworks AS FW LEFT OUTER JOIN
- FrameworkCollaborators AS fwc ON fwc.FrameworkID = FW.ID AND fwc.AdminID = @adminId AND COALESCE(IsDeleted, 0) = 0
- LEFT OUTER JOIN FrameworkReviews AS fwr ON fwc.ID = fwr.FrameworkCollaboratorID AND fwr.Archived IS NULL AND fwr.ReviewComplete IS NULL";
+ @"Frameworks AS FW INNER JOIN AdminAccounts AS aa ON aa.ID = fw.OwnerAdminID
+ LEFT OUTER JOIN FrameworkCollaborators AS fwc ON fwc.FrameworkID = FW.ID AND fwc.AdminID = @adminId AND COALESCE(IsDeleted, 0) = 0 ";
private const string AssessmentQuestionFields =
@"SELECT AQ.ID, AQ.Question, AQ.MinValue, AQ.MaxValue, AQ.AssessmentQuestionInputTypeID, AQI.InputTypeName, AQ.AddedByAdminId, CASE WHEN AQ.AddedByAdminId = @adminId THEN 1 ELSE 0 END AS UserIsOwner, AQ.CommentsPrompt, AQ.CommentsHint";
@@ -617,7 +641,7 @@ FROM [FrameworkCompetencyGroups]
return existingId;
}
- public int InsertCompetency(string name, string? description, int adminId)
+ public int InsertCompetency(string name, string? description, int adminId, bool alwaysShowDescription = false)
{
if ((name.Length == 0) | (adminId < 1))
{
@@ -629,10 +653,10 @@ public int InsertCompetency(string name, string? description, int adminId)
description = (description?.Trim() == "" ? null : description);
var existingId = connection.QuerySingle(
- @"INSERT INTO Competencies ([Name], [Description], UpdatedByAdminID)
+ @"INSERT INTO Competencies ([Name], [Description], UpdatedByAdminID, AlwaysShowDescription)
OUTPUT INSERTED.Id
- VALUES (@name, @description, @adminId)",
- new { name, description, adminId }
+ VALUES (@name, @description, @adminId, @alwaysShowDescription)",
+ new { name, description, adminId, alwaysShowDescription }
);
return existingId;
@@ -643,7 +667,7 @@ public int InsertFrameworkCompetency(
int? frameworkCompetencyGroupID,
int adminId,
int frameworkId,
- bool alwaysShowDescription = false
+ bool addDefaultQuestions = true
)
{
if ((competencyId < 1) | (adminId < 1) | (frameworkId < 1))
@@ -678,9 +702,11 @@ public int InsertFrameworkCompetency(
var numberOfAffectedRows = connection.Execute(
@"INSERT INTO FrameworkCompetencies ([CompetencyID], FrameworkCompetencyGroupID, UpdatedByAdminID, Ordering, FrameworkID)
VALUES (@competencyId, @frameworkCompetencyGroupID, @adminId, COALESCE
- ((SELECT MAX(Ordering) AS OrderNum
- FROM [FrameworkCompetencies]
- WHERE ([FrameworkCompetencyGroupID] = @frameworkCompetencyGroupID)), 0)+1, @frameworkId)",
+ ((SELECT MAX(Ordering) AS OrderNum
+ FROM [FrameworkCompetencies]
+ WHERE ((@frameworkCompetencyGroupID IS NULL AND FrameworkCompetencyGroupID IS NULL) OR
+ (@frameworkCompetencyGroupID IS NOT NULL AND FrameworkCompetencyGroupID = @frameworkCompetencyGroupID)) AND
+ FrameworkID = @frameworkId ), 0)+1, @frameworkId)",
new { competencyId, frameworkCompetencyGroupID, adminId, frameworkId }
);
if (numberOfAffectedRows < 1)
@@ -705,8 +731,10 @@ FROM [FrameworkCompetencies]
new { competencyId, frameworkCompetencyGroupID }
);
}
-
- AddDefaultQuestionsToCompetency(competencyId, frameworkId);
+ if (addDefaultQuestions)
+ {
+ AddDefaultQuestionsToCompetency(competencyId, frameworkId);
+ }
return existingId;
}
@@ -833,17 +861,22 @@ public void RemoveCustomFlag(int flagId)
);
}
- public IEnumerable GetFrameworkCompetencyGroups(int frameworkId)
+ public IEnumerable GetFrameworkCompetencyGroups(int frameworkId, int? assessmentId)
{
+ var assessmentFilter = assessmentId.HasValue ?
+ @$"AND c.ID NOT IN (SELECT CompetencyID
+ FROM SelfAssessmentStructure
+ WHERE (SelfAssessmentID = {assessmentId}))"
+ : string.Empty;
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, fcg.Ordering, fc.ID, c.ID AS CompetencyID, c.Name, c.Description, fc.Ordering,
+ (SELECT COUNT(*) FROM CompetencyAssessmentQuestions caq WHERE caq.CompetencyID = c.ID) 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
FrameworkCompetencies AS fc ON fcg.ID = fc.FrameworkCompetencyGroupID LEFT OUTER JOIN
- Competencies AS c ON fc.CompetencyID = c.ID LEFT OUTER JOIN
- CompetencyAssessmentQuestions AS caq ON c.ID = caq.CompetencyID
- WHERE (fcg.FrameworkID = @frameworkId)
+ Competencies AS c ON fc.CompetencyID = c.ID
+ WHERE (fcg.FrameworkID = @frameworkId) {assessmentFilter}
GROUP BY fcg.ID, fcg.CompetencyGroupID, cg.Name, fcg.Ordering, fc.ID, c.ID, c.Name, c.Description, fc.Ordering
ORDER BY fcg.Ordering, fc.Ordering",
(frameworkCompetencyGroup, frameworkCompetency) =>
@@ -854,29 +887,36 @@ FROM FrameworkCompetencyGroups AS fcg INNER JOIN
},
new { frameworkId }
);
- return result.GroupBy(frameworkCompetencyGroup => frameworkCompetencyGroup.CompetencyGroupID).Select(
- group =>
- {
- var groupedFrameworkCompetencyGroup = group.First();
- groupedFrameworkCompetencyGroup.FrameworkCompetencies = group.Where(frameworkCompetencyGroup => frameworkCompetencyGroup.FrameworkCompetencies.Count > 0)
- .Select(
- frameworkCompetencyGroup => frameworkCompetencyGroup.FrameworkCompetencies.Single()
- ).ToList();
- return groupedFrameworkCompetencyGroup;
- }
- );
+ return result
+ .GroupBy(fcg => fcg.CompetencyGroupID)
+ .Select(group =>
+ {
+ var groupedFrameworkCompetencyGroup = group.First();
+
+ // Flatten all FrameworkCompetencies from all instances in this group
+ groupedFrameworkCompetencyGroup.FrameworkCompetencies = group
+ .SelectMany(g => g.FrameworkCompetencies)
+ .Where(fc => fc != null).
+ Distinct().ToList();
+
+ return groupedFrameworkCompetencyGroup;
+ });
}
- public IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId)
+ public IEnumerable GetFrameworkCompetenciesUngrouped(int frameworkId, int? assessmentId)
{
+ var assessmentFilter = assessmentId.HasValue ?
+ @$"AND c.ID NOT IN (SELECT CompetencyID
+ FROM SelfAssessmentStructure
+ WHERE (SelfAssessmentID = {assessmentId}))"
+ : string.Empty;
return connection.Query(
- @"SELECT fc.ID, c.ID AS CompetencyID, c.Name, c.Description, fc.Ordering, COUNT(caq.AssessmentQuestionID) AS AssessmentQuestions,(select COUNT(CompetencyId) from CompetencyLearningResources where CompetencyID=c.ID) AS CompetencyLearningResourcesCount
+ @$"SELECT fc.ID, c.ID AS CompetencyID, c.Name, c.Description, fc.Ordering,
+ (SELECT COUNT(*) FROM CompetencyAssessmentQuestions caq WHERE caq.CompetencyID = c.ID) AS AssessmentQuestions,(select COUNT(CompetencyId) from CompetencyLearningResources where CompetencyID=c.ID) AS CompetencyLearningResourcesCount
FROM FrameworkCompetencies AS fc
INNER JOIN Competencies AS c ON fc.CompetencyID = c.ID
- LEFT OUTER JOIN
- CompetencyAssessmentQuestions AS caq ON c.ID = caq.CompetencyID
WHERE fc.FrameworkID = @frameworkId
- AND fc.FrameworkCompetencyGroupID IS NULL
+ AND fc.FrameworkCompetencyGroupID IS NULL {assessmentFilter}
GROUP BY fc.ID, c.ID, c.Name, c.Description, fc.Ordering
ORDER BY fc.Ordering",
new { frameworkId }
@@ -973,7 +1013,7 @@ FROM FrameworkCompetencies AS fc
);
}
- public void UpdateFrameworkCompetencyGroup(
+ public bool UpdateFrameworkCompetencyGroup(
int frameworkCompetencyGroupId,
int competencyGroupId,
string name,
@@ -986,7 +1026,7 @@ int adminId
logger.LogWarning(
$"Not updating framework competency group as it failed server side validation. AdminId: {adminId}, frameworkCompetencyGroupId: {frameworkCompetencyGroupId}, competencyGroupId: {competencyGroupId}, name: {name}"
);
- return;
+ return false;
}
var usedElsewhere = connection.QuerySingle(
@@ -1005,6 +1045,7 @@ int adminId
SET CompetencyGroupID = @newCompetencyGroupId, UpdatedByAdminID = @adminId
WHERE ID = @frameworkCompetencyGroupId",
new { newCompetencyGroupId, adminId, frameworkCompetencyGroupId }
+
);
if (numberOfAffectedRows < 1)
{
@@ -1012,22 +1053,32 @@ int adminId
"Not updating competency group id as db update failed. " +
$"newCompetencyGroupId: {newCompetencyGroupId}, admin id: {adminId}, frameworkCompetencyGroupId: {frameworkCompetencyGroupId}"
);
+ return false;
+ }
+ else
+ {
+ return true;
}
}
+ else
+ {
+ return false;
+ }
}
else
{
var numberOfAffectedRows = connection.Execute(
@"UPDATE CompetencyGroups SET Name = @name, UpdatedByAdminID = @adminId, Description = @description
- WHERE ID = @competencyGroupId",
+ WHERE ID = @competencyGroupId AND (Name <> @name OR ISNULL(Description, '') <> ISNULL(@description, ''))",
new { name, adminId, competencyGroupId, description }
);
if (numberOfAffectedRows < 1)
{
- logger.LogWarning(
- "Not updating competency group name as db update failed. " +
- $"Name: {name}, admin id: {adminId}, competencyGroupId: {competencyGroupId}"
- );
+ return false;
+ }
+ else
+ {
+ return true;
}
}
}
@@ -1119,7 +1170,7 @@ public void MoveFrameworkCompetency(int frameworkCompetencyId, bool singleStep,
);
}
- public void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int adminId)
+ public void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int competencyGroupId, int frameworkId, int adminId)
{
if ((frameworkCompetencyGroupId < 1) | (adminId < 1))
{
@@ -1150,7 +1201,23 @@ public void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int c
new { frameworkCompetencyGroupId }
);
- if (numberOfAffectedRows < 1)
+ if (numberOfAffectedRows > 0)
+ {
+ connection.Execute(
+ @"WITH Ranked AS (
+ SELECT ID,
+ ROW_NUMBER() OVER (PARTITION BY FrameworkID ORDER BY Ordering) AS NewOrder
+ FROM FrameworkCompetencyGroups
+ Where FrameworkID = @frameworkID
+ )
+ UPDATE fcg
+ SET fcg.Ordering = r.NewOrder
+ FROM FrameworkCompetencyGroups fcg
+ JOIN Ranked r ON fcg.ID = r.ID;",
+ new { frameworkId }
+ );
+ }
+ else
{
logger.LogWarning(
"Not deleting framework competency group as db update failed. " +
@@ -1195,7 +1262,7 @@ public void DeleteFrameworkCompetencyGroup(int frameworkCompetencyGroupId, int c
}
}
- public void DeleteFrameworkCompetency(int frameworkCompetencyId, int adminId)
+ public void DeleteFrameworkCompetency(int frameworkCompetencyId, int? frameworkCompetencyGroupId, int frameworkId, int adminId)
{
var competencyId = connection.QuerySingle(
@"SELECT CompetencyID FROM FrameworkCompetencies WHERE ID = @frameworkCompetencyId",
@@ -1219,7 +1286,24 @@ public void DeleteFrameworkCompetency(int frameworkCompetencyId, int adminId)
@"DELETE FROM FrameworkCompetencies WHERE ID = @frameworkCompetencyId",
new { frameworkCompetencyId }
);
- if (numberOfAffectedRows < 1)
+ if (numberOfAffectedRows > 0)
+ {
+ connection.Execute(
+ @"WITH Ranked AS (
+ SELECT ID,
+ ROW_NUMBER() OVER (PARTITION BY FrameworkID ORDER BY Ordering) AS NewOrder
+ FROM FrameworkCompetencies
+ Where (FrameworkCompetencyGroupID = @frameworkCompetencyGroupID) OR (FrameworkCompetencyGroupID IS NULL AND @frameworkCompetencyGroupID IS NULL) AND
+ FrameworkID = @frameworkID
+ )
+ UPDATE fc
+ SET fc.Ordering = r.NewOrder
+ FROM FrameworkCompetencies fc
+ JOIN Ranked r ON fc.ID = r.ID;",
+ new { frameworkCompetencyGroupId, frameworkId }
+ );
+ }
+ else
{
logger.LogWarning(
"Not deleting framework competency as db update failed. " +
@@ -1823,9 +1907,20 @@ FROM Competencies AS C INNER JOIN
public int GetAdminUserRoleForFrameworkId(int adminId, int frameworkId)
{
return connection.QuerySingle(
- @"SELECT CASE WHEN FW.OwnerAdminID = @adminId THEN 3 WHEN COALESCE (fwc.CanModify, 0) = 1 THEN 2 WHEN COALESCE (fwc.CanModify, 0) = 0 THEN 1 ELSE 0 END AS UserRole
- FROM Frameworks AS FW LEFT OUTER JOIN
- FrameworkCollaborators AS fwc ON fwc.FrameworkID = FW.ID AND fwc.AdminID = @adminId AND fwc.IsDeleted = 0
+ @"SELECT CASE
+ WHEN (aa.UserID = (SELECT UserID FROM AdminAccounts WHERE ID = @adminId)) THEN 3
+ WHEN (fwc.CanModify = 1) OR
+ (SELECT COUNT(*)
+ FROM FrameworkCollaborators fc
+ JOIN AdminAccounts aa1 ON fc.AdminID = aa1.ID
+ WHERE fc.FrameworkID = fw.ID
+ AND fc.CanModify = 1 AND fc.IsDeleted = 0
+ AND aa1.UserID = (SELECT aa2.UserID FROM AdminAccounts aa2 WHERE aa2.ID = @adminId)) > 0 THEN 2
+ WHEN fwc.CanModify = 0 THEN 1 ELSE 0
+ END AS UserRole
+ FROM Frameworks AS FW INNER JOIN
+ AdminAccounts AS aa ON aa.ID = fw.OwnerAdminID LEFT OUTER JOIN
+ FrameworkCollaborators AS fwc ON fwc.FrameworkID = FW.ID AND fwc.AdminID = @adminId AND fwc.IsDeleted = 0
WHERE (FW.ID = @frameworkId)",
new { adminId, frameworkId }
);
@@ -1984,7 +2079,7 @@ FROM FrameworkComments
public IEnumerable GetReviewersForFrameworkId(int frameworkId)
{
return connection.Query(
- @"SELECT
+ @"SELECT DISTINCT
fc.ID,
fc.FrameworkID,
fc.AdminID,
@@ -1995,8 +2090,11 @@ public IEnumerable GetReviewersForFrameworkId(int frameworkI
FROM FrameworkCollaborators fc
INNER JOIN AdminUsers AS au ON fc.AdminID = au.AdminID
LEFT OUTER JOIN FrameworkReviews ON fc.ID = FrameworkReviews.FrameworkCollaboratorID
- WHERE (fc.FrameworkID = @FrameworkID) AND (FrameworkReviews.ID IS NULL) AND (fc.IsDeleted=0) OR
- (fc.FrameworkID = @FrameworkID) AND (FrameworkReviews.Archived IS NOT NULL) AND (fc.IsDeleted=0)",
+ WHERE ((fc.FrameworkID = @FrameworkID) AND (FrameworkReviews.ID IS NULL) AND (fc.IsDeleted=0)) OR
+ ((fc.FrameworkID = @FrameworkID) AND (FrameworkReviews.Archived IS NOT NULL) AND (fc.IsDeleted=0))
+ AND
+ (fc.ID Not in ( Select FrameworkCollaboratorID from FrameworkReviews where FrameworkID = @FrameworkID AND
+ FrameworkReviews.Archived IS NULL AND FrameworkCollaboratorID = fc.ID))",
new { frameworkId }
);
}
@@ -2049,10 +2147,13 @@ FROM FrameworkReviews AS FR INNER JOIN
{
return connection.Query(
@"SELECT FR.ID, FR.FrameworkID, FR.FrameworkCollaboratorID, FC.UserEmail, CAST(CASE WHEN FC.AdminID IS NULL THEN 0 ELSE 1 END AS bit) AS IsRegistered, FR.ReviewRequested, FR.ReviewComplete, FR.SignedOff, FR.FrameworkCommentID, FC1.Comments AS Comment, FR.SignOffRequired
- FROM FrameworkReviews AS FR INNER JOIN
- FrameworkCollaborators AS FC ON FR.FrameworkCollaboratorID = FC.ID LEFT OUTER JOIN
- FrameworkComments AS FC1 ON FR.FrameworkCommentID = FC1.ID
- WHERE FR.ID = @reviewId AND FR.FrameworkID = @frameworkId AND FC.AdminID = @adminId AND FR.Archived IS NULL AND IsDeleted = 0",
+ FROM FrameworkReviews AS FR INNER JOIN
+ FrameworkCollaborators AS FC ON FR.FrameworkCollaboratorID = FC.ID INNER JOIN
+ AdminAccounts AS aa ON aa.ID = FC.AdminID LEFT OUTER JOIN
+ FrameworkComments AS FC1 ON FR.FrameworkCommentID = FC1.ID
+ WHERE FR.ID = @reviewId AND FR.FrameworkID = @frameworkId AND
+ aa.UserID = (SELECT aa1.UserID FROM AdminAccounts aa1 WHERE aa1.ID = @adminId) AND
+ FR.Archived IS NULL AND IsDeleted = 0",
new { frameworkId, adminId, reviewId }
).FirstOrDefault();
}
@@ -2096,7 +2197,7 @@ public void SubmitFrameworkReview(int frameworkId, int reviewId, bool signedOff,
AU1.Email AS OwnerEmail,
FW.FrameworkName
FROM FrameworkReviews AS FR
- INNER JOIN FrameworkCollaborators AS FC ON FR.FrameworkCollaboratorID = FC.ID AND FWC.IsDeleted = 0
+ INNER JOIN FrameworkCollaborators AS FC ON FR.FrameworkCollaboratorID = FC.ID AND FC.IsDeleted = 0
INNER JOIN AdminUsers AS AU ON FC.AdminID = AU.AdminID
INNER JOIN Frameworks AS FW ON FR.FrameworkID = FW.ID
INNER JOIN AdminUsers AS AU1 ON FW.OwnerAdminID = AU1.AdminID
@@ -2407,7 +2508,7 @@ public IEnumerable GetBulkCompetenciesForFramework(int framework
else
{
return connection.Query(
- @"SELECT fc.ID, cg.Name AS CompetencyGroup, cg.Description AS GroupDescription, c.Name AS Competency, c.Description AS CompetencyDescription, c.AlwaysShowDescription, STUFF((
+ @"SELECT fc.ID, ISNULL(cg.Name, '') AS CompetencyGroup, cg.Description AS GroupDescription, c.Name AS Competency, c.Description AS CompetencyDescription, c.AlwaysShowDescription, STUFF((
SELECT ', ' + f.FlagName
FROM Flags AS f
INNER JOIN CompetencyFlags AS cf ON f.ID = cf.FlagID
@@ -2415,12 +2516,12 @@ FROM Flags AS f
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS FlagsCsv
FROM
Competencies AS c INNER JOIN
- FrameworkCompetencies AS fc ON c.ID = fc.CompetencyID INNER JOIN
- FrameworkCompetencyGroups AS fcg ON fc.FrameworkCompetencyGroupID = fcg.ID INNER JOIN
+ FrameworkCompetencies AS fc ON c.ID = fc.CompetencyID LEFT JOIN
+ FrameworkCompetencyGroups AS fcg ON fc.FrameworkCompetencyGroupID = fcg.ID LEFT JOIN
CompetencyGroups AS cg ON fcg.CompetencyGroupID = cg.ID
WHERE (fc.FrameworkID = @frameworkId)
GROUP BY fc.ID, c.ID, cg.Name, cg.Description, c.Name, c.Description, c.AlwaysShowDescription, fcg.Ordering, fc.Ordering
- ORDER BY fcg.Ordering, fc.Ordering",
+ ORDER BY COALESCE(fcg.Ordering,99999), fc.Ordering, fc.ID",
new { frameworkId }
);
}
@@ -2430,12 +2531,38 @@ public List GetFrameworkCompetencyOrder(int frameworkId, List framewor
{
return connection.Query(
@"SELECT fc.ID
- FROM FrameworkCompetencies AS fc INNER JOIN
+ FROM FrameworkCompetencies AS fc LEFT JOIN
FrameworkCompetencyGroups AS fcg ON fc.FrameworkCompetencyGroupID = fcg.ID
WHERE (fc.FrameworkID = @frameworkId) AND (fc.ID IN @frameworkCompetencyIds)
- ORDER BY fcg.Ordering, fc.Ordering",
+ ORDER BY COALESCE(fcg.Ordering,99999), fc.Ordering, fc.ID",
new { frameworkId, frameworkCompetencyIds }
).ToList();
}
+
+ public int GetFrameworkCompetencyGroupId(int frameworkId, int competencyGroupId)
+ {
+ return connection.Query(
+ @"SELECT MAX(ID) FROM FrameworkCompetencyGroups
+ WHERE FrameworkID = @frameworkId AND CompetencyGroupID = @competencyGroupId",
+ new { frameworkId, competencyGroupId }
+ ).Single();
+ }
+
+ public void UpdateFrameworkCompetencyFrameworkCompetencyGroup(int? competencyGroupId, int frameworkCompetencyGroupId, int adminId)
+ {
+ var numberOfAffectedRows = connection.Execute(
+ @"UPDATE FrameworkCompetencies
+ SET FrameworkCompetencyGroupId = @frameworkCompetencyGroupId, UpdatedByAdminID = @adminId
+ WHERE ID = @competencyGroupId AND FrameworkCompetencyGroupId <> @frameworkCompetencyGroupId",
+ new { frameworkCompetencyGroupId, competencyGroupId, adminId }
+ );
+ if (numberOfAffectedRows < 1)
+ {
+ logger.LogWarning(
+ "Not updating framework competencies framework competency group id as db update failed. " +
+ $"frameworkCompetencyGroupId: {frameworkCompetencyGroupId}, competencyGroupId: {competencyGroupId}."
+ );
+ }
+ }
}
}
diff --git a/DigitalLearningSolutions.Data/DataServices/LoginDataService.cs b/DigitalLearningSolutions.Data/DataServices/LoginDataService.cs
new file mode 100644
index 0000000000..109d5a9273
--- /dev/null
+++ b/DigitalLearningSolutions.Data/DataServices/LoginDataService.cs
@@ -0,0 +1,63 @@
+namespace DigitalLearningSolutions.Data.DataServices
+{
+ using Dapper;
+ using System.Data;
+
+ public interface ILoginDataService
+ {
+ void UpdateLastAccessedForUsersTable(int Id);
+
+ void UpdateLastAccessedForDelegatesAccountsTable(int Id);
+
+ void UpdateLastAccessedForAdminAccountsTable(int Id);
+ }
+
+ public class LoginDataService : ILoginDataService
+ {
+ private readonly IDbConnection connection;
+
+ public LoginDataService(IDbConnection connection)
+ {
+ this.connection = connection;
+ }
+
+ public void UpdateLastAccessedForUsersTable(int Id)
+ {
+ connection.Execute(
+ @"UPDATE Users SET
+ LastAccessed = GetUtcDate()
+ WHERE ID = @Id",
+ new
+ {
+ Id
+ }
+ );
+ }
+
+ public void UpdateLastAccessedForDelegatesAccountsTable(int Id)
+ {
+ connection.Execute(
+ @"UPDATE DelegateAccounts SET
+ LastAccessed = GetUtcDate()
+ WHERE ID = @Id",
+ new
+ {
+ Id
+ }
+ );
+ }
+
+ public void UpdateLastAccessedForAdminAccountsTable(int Id)
+ {
+ connection.Execute(
+ @"UPDATE AdminAccounts SET
+ LastAccessed = GetUtcDate()
+ WHERE ID = @Id",
+ new
+ {
+ Id
+ }
+ );
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs b/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs
index b127b9b42d..8486fe3db2 100644
--- a/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs
@@ -1,10 +1,9 @@
namespace DigitalLearningSolutions.Data.DataServices
{
using Dapper;
+ using DigitalLearningSolutions.Data.Factories;
using DigitalLearningSolutions.Data.Models.PlatformReports;
- using DigitalLearningSolutions.Data.Models.SelfAssessments;
using DigitalLearningSolutions.Data.Models.TrackingSystem;
- using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
@@ -37,9 +36,9 @@ IEnumerable GetFilteredCourseActivity(
);
DateTime? GetStartOfCourseActivity();
}
- public class PlatformReportsDataService : IPlatformReportsDataService
+ public class PlatformReportsDataService(IReadOnlyDbConnectionFactory factory) : IPlatformReportsDataService
{
- private readonly IDbConnection connection;
+ private readonly IDbConnection connection = factory.CreateConnection();
private readonly string selectSelfAssessmentActivity = @"SELECT Cast(al.ActivityDate As Date) As ActivityDate, SUM(CAST(al.Enrolled AS Int)) AS Enrolled, SUM(CAST((al.Submitted | al.SignedOff) AS Int)) AS Completed
FROM ReportSelfAssessmentActivityLog AS al WITH (NOLOCK) INNER JOIN
Centres AS ce WITH (NOLOCK) ON al.CentreID = ce.CentreID INNER JOIN
@@ -60,10 +59,6 @@ private string GetSelfAssessmentWhereClause(bool supervised)
return supervised ? " (sa.SupervisorResultsReview = 1 OR SupervisorSelfAssessmentReview = 1)" : " (sa.SupervisorResultsReview = 0 AND SupervisorSelfAssessmentReview = 0)";
}
- public PlatformReportsDataService(IDbConnection connection)
- {
- this.connection = connection;
- }
public PlatformUsageSummary GetPlatformUsageSummary()
{
return connection.QueryFirstOrDefault(
diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs
index bb1dc7649f..aae3988660 100644
--- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/CandidateAssessmentsDataService.cs
@@ -12,28 +12,31 @@ public IEnumerable GetSelfAssessmentsForCandidate(int del
{
return connection.Query(
@"SELECT SelfAssessment.Id,
- SelfAssessment.Name,
- SelfAssessment.Description,
- SelfAssessment.IncludesSignposting,
- SelfAssessment.IncludeRequirementsFilters,
- SelfAssessment. IsSupervisorResultsReviewed,
- SelfAssessment.ReviewerCommentsLabel,
- SelfAssessment. Vocabulary,
- SelfAssessment. NumberOfCompetencies,
- SelfAssessment.StartedDate,
- SelfAssessment.LastAccessed,
- SelfAssessment.CompleteByDate,
- SelfAssessment.CandidateAssessmentId,
- SelfAssessment.UserBookmark,
- SelfAssessment.UnprocessedUpdates,
- SelfAssessment.LaunchCount,
- SelfAssessment. IsSelfAssessment,
- SelfAssessment.SubmittedDate,
- SelfAssessment. CentreName,
- SelfAssessment.EnrolmentMethodId,
- Signoff.SignedOff,
- Signoff.Verified,
- EnrolledByForename +' '+EnrolledBySurname AS EnrolledByFullName
+ SelfAssessment.Name,
+ SelfAssessment.Description,
+ SelfAssessment.IncludesSignposting,
+ SelfAssessment.IncludeRequirementsFilters,
+ SelfAssessment. IsSupervisorResultsReviewed,
+ SelfAssessment.ReviewerCommentsLabel,
+ SelfAssessment. Vocabulary,
+ SelfAssessment. NumberOfCompetencies,
+ SelfAssessment.StartedDate,
+ SelfAssessment.LastAccessed,
+ SelfAssessment.CompleteByDate,
+ SelfAssessment.CandidateAssessmentId,
+ SelfAssessment.UserBookmark,
+ SelfAssessment.UnprocessedUpdates,
+ SelfAssessment.LaunchCount,
+ SelfAssessment. IsSelfAssessment,
+ SelfAssessment.SubmittedDate,
+ SelfAssessment. CentreName,
+ SelfAssessment.EnrolmentMethodId,
+ SelfAssessment.RetirementDate,
+ SelfAssessment.EnrolmentCutoffDate,
+ SelfAssessment.RetirementReason,
+ Signoff.SignedOff,
+ Signoff.Verified,
+ EnrolledByForename +' '+EnrolledBySurname AS EnrolledByFullName
FROM (SELECT
CA.SelfAssessmentID AS Id,
SA.Name,
@@ -56,7 +59,10 @@ public IEnumerable GetSelfAssessmentsForCandidate(int del
CR.CentreName AS CentreName,
CA.EnrolmentMethodId,
uEnrolledBy.FirstName AS EnrolledByForename,
- uEnrolledBy.LastName AS EnrolledBySurname
+ uEnrolledBy.LastName AS EnrolledBySurname,
+ SA.RetirementDate,
+ SA.EnrolmentCutoffDate,
+ SA.RetirementReason
FROM Centres AS CR INNER JOIN
CandidateAssessments AS CA INNER JOIN
SelfAssessments AS SA ON CA.SelfAssessmentID = SA.ID ON CR.CentreID = CA.CentreID INNER JOIN
@@ -71,7 +77,7 @@ Competencies AS C RIGHT OUTER JOIN
AND (ISNULL(@adminIdCategoryID, 0) = 0 OR sa.CategoryID = @adminIdCategoryId)
GROUP BY
CA.SelfAssessmentID, SA.Name, SA.Description, SA.IncludesSignposting, SA.SupervisorResultsReview,
- SA.ReviewerCommentsLabel, SA.IncludeRequirementsFilters,
+ SA.ReviewerCommentsLabel, SA.IncludeRequirementsFilters, SA.RetirementDate,SA.EnrolmentCutoffDate,SA.RetirementReason,
COALESCE(SA.Vocabulary, 'Capability'), CA.StartedDate, CA.LastAccessed, CA.CompleteByDate,
CA.ID,
CA.UserBookmark, CA.UnprocessedUpdates, CA.LaunchCount, CA.SubmittedDate, CR.CentreName,CA.EnrolmentMethodId,
@@ -193,6 +199,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 +248,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 +332,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(
@@ -490,11 +513,13 @@ public IEnumerable GetAccessor(int selfAssessmentId, int delegateUserI
{
return connection.Query(
@"SELECT CASE WHEN AccessorPRN IS NOT NULL THEN AccessorName+', '+AccessorPRN ELSE AccessorName END AS AccessorList,AccessorName,AccessorPRN
- FROM (SELECT COALESCE(au.Forename + ' ' + au.Surname + (CASE WHEN au.Active = 1 THEN '' ELSE ' (Inactive)' END), sd.SupervisorEmail) AS AccessorName,
+ FROM (SELECT DISTINCT COALESCE(au.Forename + ' ' + au.Surname + (CASE WHEN au.Active = 1 THEN '' ELSE ' (Inactive)' END), sd.SupervisorEmail) AS AccessorName,
u.ProfessionalRegistrationNumber AS AccessorPRN
FROM SupervisorDelegates AS sd
INNER JOIN CandidateAssessmentSupervisors AS cas
ON sd.ID = cas.SupervisorDelegateId
+ INNER JOIN SelfAssessmentResultSupervisorVerifications AS srsv
+ ON cas.ID = srsv.CandidateAssessmentSupervisorID
INNER JOIN CandidateAssessments AS ca
ON cas.CandidateAssessmentID = ca.ID
LEFT OUTER JOIN AdminUsers AS au
@@ -504,7 +529,8 @@ LEFT OUTER JOIN SelfAssessmentSupervisorRoles AS sasr
ON cas.SelfAssessmentSupervisorRoleID = sasr.ID
INNER JOIN Users AS u ON U.PrimaryEmail = au.Email
WHERE
- (sd.Removed IS NULL) AND (cas.Removed IS NULL) AND (ca.DelegateUserID = @DelegateUserID) AND (ca.SelfAssessmentID = @selfAssessmentId)) Accessor
+ (ca.DelegateUserID = @DelegateUserID) AND (ca.SelfAssessmentID = @selfAssessmentId)
+ AND (srsv.Verified IS NOT NULL)) Accessor
ORDER BY AccessorName, AccessorPRN DESC",
new { selfAssessmentId, delegateUserID }
);
diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs
index ed3800215e..cdb71bb90d 100644
--- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs
@@ -1,128 +1,124 @@
-namespace DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService
-{
- using Dapper;
- using DigitalLearningSolutions.Data.Models.SelfAssessments.Export;
- using Microsoft.Extensions.Logging;
- using System.Collections.Generic;
- using System.Data;
-
- public interface IDCSAReportDataService
- {
- IEnumerable GetDelegateCompletionStatusForCentre(int centreId);
- IEnumerable GetOutcomeSummaryForCentre(int centreId);
- }
- public partial class DCSAReportDataService : IDCSAReportDataService
- {
- private readonly IDbConnection connection;
- private readonly ILogger logger;
-
- public DCSAReportDataService(IDbConnection connection, ILogger logger)
- {
- this.connection = connection;
- this.logger = logger;
- }
-
- public IEnumerable GetDelegateCompletionStatusForCentre(int centreId)
- {
- return connection.Query(
- @"SELECT DATEPART(month, ca.StartedDate) AS EnrolledMonth, DATEPART(yyyy, ca.StartedDate) AS EnrolledYear, u.FirstName, u.LastName, COALESCE (ucd.Email, u.PrimaryEmail) AS Email, da.Answer1 AS CentreField1, da.Answer2 AS CentreField2, da.Answer3 AS CentreField3,
- CASE WHEN (ca.SubmittedDate IS NOT NULL) THEN 'Submitted' WHEN (ca.UserBookmark LIKE N'/LearningPortal/SelfAssessment/1/Review' AND ca.SubmittedDate IS NULL) THEN 'Reviewing' ELSE 'Incomplete' END AS Status
- FROM CandidateAssessments AS ca INNER JOIN
- DelegateAccounts AS da ON ca.DelegateUserID = da.UserID INNER JOIN
- Users AS u ON da.UserID = u.ID LEFT OUTER JOIN
- UserCentreDetails AS ucd ON da.CentreID = ucd.CentreID AND u.ID = ucd.UserID
- WHERE (ca.SelfAssessmentID = 1) AND (da.CentreID = @centreId) AND (u.Active = 1) AND (da.Active = 1)",
- new { centreId }
- );
- }
-
- public IEnumerable GetOutcomeSummaryForCentre(int centreId)
- {
- return connection.Query(
- @"SELECT DATEPART(month, ca.StartedDate) AS EnrolledMonth, DATEPART(yyyy, ca.StartedDate) AS EnrolledYear, jg.JobGroupName AS JobGroup, da.Answer1 AS CentreField1, da.Answer2 AS CentreField2, da.Answer3 AS CentreField3, CASE WHEN (ca.SubmittedDate IS NOT NULL)
- THEN 'Submitted' WHEN (ca.UserBookmark LIKE N'/LearningPortal/SelfAssessment/1/Review' AND ca.SubmittedDate IS NULL) THEN 'Reviewing' ELSE 'Incomplete' END AS Status,
- (SELECT COUNT(*) AS LearningLaunched
- FROM CandidateAssessmentLearningLogItems AS calli INNER JOIN
- LearningLogItems AS lli ON calli.LearningLogItemID = lli.LearningLogItemID
- 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,
- (SELECT COUNT(*) AS LearningLaunched
- FROM CandidateAssessmentLearningLogItems AS calli INNER JOIN
- LearningLogItems AS lli ON calli.LearningLogItemID = lli.LearningLogItemID
- WHERE (NOT (lli.LearningResourceReferenceID IS NULL)) AND (calli.CandidateAssessmentID = ca.ID) AND (NOT (lli.CompletedDate IS NULL))) +
- (SELECT COUNT(*) AS FilteredCompleted
- FROM FilteredLearningActivity AS fla
- WHERE (CandidateId = da.ID) AND (CompletedDate IS NOT NULL)) AS LearningCompleted,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 1)) AS DataInformationAndContentConfidence,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 1)) AS DataInformationAndContentRelevance,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 2)) AS TeachingLearningAndSelfDevelopmentConfidence,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 2)) AS TeachingLearningAndSelfDevelopmentRelevance,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 3)) AS CommunicationCollaborationAndParticipationConfidence,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 3)) AS CommunicationCollaborationAndParticipationRelevance,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 4)) AS TechnicalProficiencyConfidence,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 4)) AS TechnicalProficiencyRelevance,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 5)) AS CreationInnovationAndResearchConfidence,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 5)) AS CreationInnovationAndResearchRelevance,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 6)) AS DigitalIdentityWellbeingSafetyAndSecurityConfidence,
- (SELECT AVG(sar.Result) AS AvgConfidence
- FROM SelfAssessmentResults AS sar INNER JOIN
- Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
- SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
- WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 6)) AS DigitalIdentityWellbeingSafetyAndSecurityRelevance
- FROM CandidateAssessments AS ca INNER JOIN
- DelegateAccounts AS da ON ca.DelegateUserID = da.UserID INNER JOIN
- Users AS u ON da.UserID = u.ID INNER JOIN
- JobGroups AS jg ON u.JobGroupID = jg.JobGroupID
- WHERE (ca.SelfAssessmentID = 1) AND (da.CentreID = @centreId) AND (u.Active = 1) AND (da.Active = 1)",
- new { centreId }
- );
- }
-
- }
-}
+namespace DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService
+{
+ using Dapper;
+ using DigitalLearningSolutions.Data.Factories;
+ using DigitalLearningSolutions.Data.Models.SelfAssessments.Export;
+ using Microsoft.Extensions.Logging;
+ using System.Collections.Generic;
+ using System.Data;
+
+ public interface IDCSAReportDataService
+ {
+ IEnumerable GetDelegateCompletionStatusForCentre(int centreId);
+ IEnumerable GetOutcomeSummaryForCentre(int centreId);
+ }
+ public partial class DCSAReportDataService(IReadOnlyDbConnectionFactory factory, ILogger logger) : IDCSAReportDataService
+ {
+ private readonly IDbConnection connection = factory.CreateConnection();
+ private readonly ILogger logger = logger;
+
+ public IEnumerable GetDelegateCompletionStatusForCentre(int centreId)
+ {
+ return connection.Query(
+ @"SELECT DATEPART(month, ca.StartedDate) AS EnrolledMonth, DATEPART(yyyy, ca.StartedDate) AS EnrolledYear, u.FirstName, u.LastName, COALESCE (ucd.Email, u.PrimaryEmail) AS Email, da.Answer1 AS RegistrationAnswer1, da.Answer2 AS RegistrationAnswer2, da.Answer3 AS RegistrationAnswer3,
+ da.Answer4 AS RegistrationAnswer4, da.Answer5 AS RegistrationAnswer5, da.Answer6 AS RegistrationAnswer6, CASE WHEN (ca.SubmittedDate IS NOT NULL) THEN 'Submitted' WHEN (ca.UserBookmark LIKE N'/LearningPortal/SelfAssessment/1/Review' AND ca.SubmittedDate IS NULL) THEN 'Reviewing' ELSE 'Incomplete' END AS Status
+ FROM CandidateAssessments AS ca INNER JOIN
+ DelegateAccounts AS da ON ca.DelegateUserID = da.UserID INNER JOIN
+ Users AS u ON da.UserID = u.ID LEFT OUTER JOIN
+ UserCentreDetails AS ucd ON da.CentreID = ucd.CentreID AND u.ID = ucd.UserID
+ WHERE (ca.SelfAssessmentID = 1) AND (da.CentreID = @centreId) AND (u.Active = 1) AND (da.Active = 1)",
+ new { centreId }
+ );
+ }
+
+ public IEnumerable GetOutcomeSummaryForCentre(int centreId)
+ {
+ return connection.Query(
+ @"SELECT DATEPART(month, ca.StartedDate) AS EnrolledMonth, DATEPART(yyyy, ca.StartedDate) AS EnrolledYear, jg.JobGroupName AS JobGroup, da.Answer1 AS RegistrationAnswer1, da.Answer2 AS RegistrationAnswer2, da.Answer3 AS RegistrationAnswer3,
+ da.Answer4 AS RegistrationAnswer4, da.Answer5 AS RegistrationAnswer5, da.Answer6 AS RegistrationAnswer6, CASE WHEN (ca.SubmittedDate IS NOT NULL)
+ THEN 'Submitted' WHEN (ca.UserBookmark LIKE N'/LearningPortal/SelfAssessment/1/Review' AND ca.SubmittedDate IS NULL) THEN 'Reviewing' ELSE 'Incomplete' END AS Status,
+ (SELECT COUNT(*) AS LearningLaunched
+ FROM CandidateAssessmentLearningLogItems AS calli INNER JOIN
+ LearningLogItems AS lli ON calli.LearningLogItemID = lli.LearningLogItemID
+ WHERE (NOT (lli.LearningResourceReferenceID IS NULL)) AND (calli.CandidateAssessmentID = ca.ID)) +
+ (SELECT COUNT(*) AS FilteredLearning
+ FROM FilteredLearningActivity AS fla
+ WHERE (CandidateId = da.ID)) AS LearningLaunched,
+ (SELECT COUNT(*) AS LearningLaunched
+ FROM CandidateAssessmentLearningLogItems AS calli INNER JOIN
+ LearningLogItems AS lli ON calli.LearningLogItemID = lli.LearningLogItemID
+ WHERE (NOT (lli.LearningResourceReferenceID IS NULL)) AND (calli.CandidateAssessmentID = ca.ID) AND (NOT (lli.CompletedDate IS NULL))) +
+ (SELECT COUNT(*) AS FilteredCompleted
+ FROM FilteredLearningActivity AS fla
+ WHERE (CandidateId = da.ID) AND (CompletedDate IS NOT NULL)) AS LearningCompleted,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 1)) AS DataInformationAndContentConfidence,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 1)) AS DataInformationAndContentRelevance,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 2)) AS TeachingLearningAndSelfDevelopmentConfidence,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 2)) AS TeachingLearningAndSelfDevelopmentRelevance,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 3)) AS CommunicationCollaborationAndParticipationConfidence,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 3)) AS CommunicationCollaborationAndParticipationRelevance,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 4)) AS TechnicalProficiencyConfidence,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 4)) AS TechnicalProficiencyRelevance,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 5)) AS CreationInnovationAndResearchConfidence,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 5)) AS CreationInnovationAndResearchRelevance,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 1) AND (sas.CompetencyGroupID = 6)) AS DigitalIdentityWellbeingSafetyAndSecurityConfidence,
+ (SELECT AVG(sar.Result) AS AvgConfidence
+ FROM SelfAssessmentResults AS sar INNER JOIN
+ Competencies AS co ON sar.CompetencyID = co.ID INNER JOIN
+ SelfAssessmentStructure AS sas ON co.ID = sas.CompetencyID
+ WHERE (sar.DelegateUserID = da.UserID) AND (sar.AssessmentQuestionID = 2) AND (sas.CompetencyGroupID = 6)) AS DigitalIdentityWellbeingSafetyAndSecurityRelevance
+ FROM CandidateAssessments AS ca INNER JOIN
+ DelegateAccounts AS da ON ca.DelegateUserID = da.UserID INNER JOIN
+ Users AS u ON da.UserID = u.ID INNER JOIN
+ JobGroups AS jg ON u.JobGroupID = jg.JobGroupID
+ WHERE (ca.SelfAssessmentID = 1) AND (da.CentreID = @centreId) AND (u.Active = 1) AND (da.Active = 1)",
+ new { centreId }
+ );
+ }
+
+ }
+}
diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs
index 324a1050a7..ad32f35ba2 100644
--- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAssessmentDataService.cs
@@ -16,6 +16,8 @@ public interface ISelfAssessmentDataService
{
//Self Assessments
string? GetSelfAssessmentNameById(int selfAssessmentId);
+ SelfAssessment? GetSelfAssessmentById(int selfAssessmentId);
+ SelfAssessment GetSelfAssessmentRetirementDateById(int selfAssessmentId);
// CompetencyDataService
IEnumerable GetCompetencyIdsForSelfAssessment(int selfAssessmentId);
@@ -89,6 +91,7 @@ int competencyId
void SetBookmark(int selfAssessmentId, int delegateUserId, string bookmark);
+ void MarkProgressAgreed(int selfAssessmentId, int delegateUserId);
IEnumerable GetCandidateAssessments(int delegateUserId, int selfAssessmentId);
// SelfAssessmentSupervisorDataService
@@ -204,6 +207,67 @@ FROM SelfAssessments
return name;
}
+ public SelfAssessment? GetSelfAssessmentById(int selfAssessmentId)
+ {
+ return connection.Query(
+ @"SELECT [ID]
+ ,[Name]
+ ,[Description]
+ ,[IncludesSignposting]
+ ,[BrandID]
+ ,[CreatedDate]
+ ,[CreatedByCentreID]
+ ,[CreatedByAdminID]
+ ,[ArchivedDate]
+ ,[ArchivedByAdminID]
+ ,[IncludeDevelopment]
+ ,[ParentSelfAssessmentID]
+ ,[NRPProfessionalGroupID]
+ ,[NRPSubGroupID]
+ ,[NRPRoleID]
+ ,[PublishStatusID]
+ ,[UpdatedByAdminID]
+ ,[National]
+ ,[Public]
+ ,[Archived]
+ ,[LastEdit]
+ ,[SupervisorSelfAssessmentReview]
+ ,[SupervisorResultsReview]
+ ,[RAGResults]
+ ,[LinearNavigation]
+ ,[CategoryID]
+ ,[UseDescriptionExpanders]
+ ,[ManageOptionalCompetenciesPrompt]
+ ,[Vocabulary]
+ ,[SignOffRequestorStatement]
+ ,[SignOffSupervisorStatement]
+ ,[QuestionLabel]
+ ,[DescriptionLabel]
+ ,[EnforceRoleRequirementsForSignOff]
+ ,[ReviewerCommentsLabel]
+ ,[ManageSupervisorsDescription]
+ ,[IncludeRequirementsFilters]
+ ,[MinimumOptionalCompetencies]
+ ,[RetirementDate]
+ ,[EnrolmentCutoffDate]
+ ,[RetirementReason]
+ FROM SelfAssessments
+ WHERE ID = @selfAssessmentId",
+ new { selfAssessmentId }
+ ).SingleOrDefault();
+ }
+ public SelfAssessment GetSelfAssessmentRetirementDateById(int selfAssessmentId)
+ {
+ var date = connection.QueryFirstOrDefault(
+ @"SELECT Id,Name,[RetirementDate]
+ FROM SelfAssessments
+ WHERE ID = @selfAssessmentId"
+ ,
+ new { selfAssessmentId }
+ );
+ return date;
+ }
+
public (IEnumerable, int) GetSelfAssessmentDelegates(string searchString, int offSet, int itemsPerPage, string sortBy, string sortDirection,
int? selfAssessmentId, int centreId, bool? isDelegateActive, bool? removed, bool? submitted, bool? signedOff)
{
diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs
index 50168d4e64..3a593d515c 100644
--- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs
@@ -1,135 +1,49 @@
-namespace DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService
-{
- using Dapper;
- using DigitalLearningSolutions.Data.Models.SelfAssessments.Export;
- using DigitalLearningSolutions.Data.Models.SelfAssessments;
- using Microsoft.Extensions.Logging;
- using System.Collections.Generic;
- using System.Data;
-
- public interface ISelfAssessmentReportDataService
- {
- IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId);
- IEnumerable GetSelfAssessmentReportDataForCentre(int centreId, int selfAssessmentId);
- }
- public partial class SelfAssessmentReportDataService : ISelfAssessmentReportDataService
- {
- private readonly IDbConnection connection;
- private readonly ILogger logger;
-
- public SelfAssessmentReportDataService(IDbConnection connection, ILogger logger)
- {
- this.connection = connection;
- this.logger = logger;
- }
-
- public IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId)
- {
- return connection.Query(
- @"SELECT csa.SelfAssessmentID AS Id, sa.Name,
- (SELECT COUNT (DISTINCT da.UserID) AS Learners
- FROM CandidateAssessments AS ca1 INNER JOIN
- DelegateAccounts AS da ON ca1.DelegateUserID = da.UserID
- WHERE (da.CentreID = @centreId) AND (ca1.RemovedDate IS NULL) AND (ca1.SelfAssessmentID = csa.SelfAssessmentID) AND ca1.NonReportable=0) AS LearnerCount
- FROM CentreSelfAssessments AS csa INNER JOIN
- SelfAssessments AS sa ON csa.SelfAssessmentID = sa.ID
- WHERE (csa.CentreID = @centreId) AND (sa.CategoryID = @categoryId) AND (sa.SupervisorResultsReview = 1) AND (sa.ArchivedDate IS NULL) OR
- (csa.CentreID = @centreId) AND (sa.CategoryID = @categoryId) AND (sa.ArchivedDate IS NULL) AND (sa.SupervisorSelfAssessmentReview = 1) OR
- (csa.CentreID = @centreId) AND (sa.SupervisorResultsReview = 1) AND (sa.ArchivedDate IS NULL) AND (@categoryId = 0) OR
- (csa.CentreID = @centreId) AND (sa.ArchivedDate IS NULL) AND (sa.SupervisorSelfAssessmentReview = 1) AND (@categoryId = 0)
- ORDER BY sa.Name",
- new { centreId, categoryId = categoryId ??= 0 }
- );
- }
-
- public IEnumerable GetSelfAssessmentReportDataForCentre(int centreId, int selfAssessmentId)
- {
- return connection.Query(
- @"WITH LatestAssessmentResults AS
- (
- SELECT s.DelegateUserID
- , CASE WHEN COALESCE (rr.LevelRAG, 0) = 3 THEN s.ID ELSE NULL END AS SelfAssessed
- , CASE WHEN sv.Verified IS NOT NULL AND sv.SignedOff = 1 AND COALESCE (rr.LevelRAG, 0) = 3 THEN s.ID ELSE NULL END AS Confirmed
- , CASE WHEN sas.Optional = 1 THEN s.CompetencyID ELSE NULL END AS Optional
- FROM SelfAssessmentResults AS s LEFT OUTER JOIN
- SelfAssessmentStructure AS sas ON sas.SelfAssessmentID = @selfAssessmentId AND s.CompetencyID = sas.CompetencyID LEFT OUTER JOIN
- SelfAssessmentResultSupervisorVerifications AS sv ON s.ID = sv.SelfAssessmentResultId AND sv.Superceded = 0 LEFT OUTER JOIN
- CompetencyAssessmentQuestionRoleRequirements AS rr ON s.CompetencyID = rr.CompetencyID AND s.AssessmentQuestionID = rr.AssessmentQuestionID AND sas.SelfAssessmentID = rr.SelfAssessmentID AND s.Result = rr.LevelValue
- WHERE (sas.SelfAssessmentID = @selfAssessmentId)
- )
- SELECT
- sa.Name AS SelfAssessment
- , u.LastName + ', ' + u.FirstName AS Learner
- , da.Active AS LearnerActive
- , u.ProfessionalRegistrationNumber AS PRN
- , jg.JobGroupName AS JobGroup
- , CASE WHEN c.CustomField1PromptID = 10 THEN da.Answer1 WHEN c.CustomField2PromptID = 10 THEN da.Answer2 WHEN c.CustomField3PromptID = 10 THEN da.Answer3 WHEN c.CustomField4PromptID = 10 THEN da.Answer4 WHEN c.CustomField5PromptID = 10 THEN da.Answer5 WHEN c.CustomField6PromptID = 10 THEN da.Answer6 ELSE '' END AS 'ProgrammeCourse'
- , CASE WHEN c.CustomField1PromptID = 4 THEN da.Answer1 WHEN c.CustomField2PromptID = 4 THEN da.Answer2 WHEN c.CustomField3PromptID = 4 THEN da.Answer3 WHEN c.CustomField4PromptID = 4 THEN da.Answer4 WHEN c.CustomField5PromptID = 4 THEN da.Answer5 WHEN c.CustomField6PromptID = 4 THEN da.Answer6 ELSE '' END AS 'Organisation'
- , CASE WHEN c.CustomField1PromptID = 1 THEN da.Answer1 WHEN c.CustomField2PromptID = 1 THEN da.Answer2 WHEN c.CustomField3PromptID = 1 THEN da.Answer3 WHEN c.CustomField4PromptID = 1 THEN da.Answer4 WHEN c.CustomField5PromptID = 1 THEN da.Answer5 WHEN c.CustomField6PromptID = 1 THEN da.Answer6 ELSE '' END AS 'DepartmentTeam'
- , dbo.GetOtherCentresForSelfAssessment(da.UserID, @SelfAssessmentID, c.CentreID) AS OtherCentres
- , CASE
- WHEN aa.ID IS NULL THEN 'Learner'
- WHEN aa.IsCentreManager = 1 THEN 'Centre Manager'
- WHEN aa.IsCentreAdmin = 1 AND aa.IsCentreManager = 0 THEN 'Centre Admin'
- WHEN aa.IsSupervisor = 1 THEN 'Supervisor'
- WHEN aa.IsNominatedSupervisor = 1 THEN 'Nominated supervisor'
- END AS DLSRole
- , da.DateRegistered AS Registered
- , ca.StartedDate AS Started
- , ca.LastAccessed
- , COALESCE(COUNT(DISTINCT LAR.Optional), NULL) AS [OptionalProficienciesAssessed]
- , COALESCE(COUNT(DISTINCT LAR.SelfAssessed), NULL) AS [SelfAssessedAchieved]
- , COALESCE(COUNT(DISTINCT LAR.Confirmed), NULL) AS [ConfirmedResults]
- , max(casv.Requested) AS SignOffRequested
- , max(1*casv.SignedOff) AS SignOffAchieved
- , min(casv.Verified) AS ReviewedDate
- FROM
- CandidateAssessments AS ca INNER JOIN
- DelegateAccounts AS da ON ca.DelegateUserID = da.UserID and da.CentreID = @centreId INNER JOIN
- Users as u ON u.ID = da.UserID INNER JOIN
- SelfAssessments AS sa INNER JOIN
- CentreSelfAssessments AS csa ON sa.ID = csa.SelfAssessmentID INNER JOIN
- Centres AS c ON csa.CentreID = c.CentreID ON da.CentreID = c.CentreID AND ca.SelfAssessmentID = sa.ID INNER JOIN
- JobGroups AS jg ON u.JobGroupID = jg.JobGroupID LEFT OUTER JOIN
- AdminAccounts AS aa ON da.UserID = aa.UserID AND aa.CentreID = da.CentreID AND aa.Active = 1 LEFT OUTER JOIN
- CandidateAssessmentSupervisors AS cas ON ca.ID = cas.CandidateAssessmentID left JOIN
- CandidateAssessmentSupervisorVerifications AS casv ON casv.CandidateAssessmentSupervisorID = cas.ID LEFT JOIN
- SupervisorDelegates AS sd ON cas.SupervisorDelegateId = sd.ID
- LEFT OUTER JOIN LatestAssessmentResults AS LAR ON LAR.DelegateUserID = ca.DelegateUserID
- WHERE
- (sa.ID = @SelfAssessmentID) AND (sa.ArchivedDate IS NULL) AND (c.Active = 1) AND (ca.RemovedDate IS NULL AND ca.NonReportable = 0)
- Group by sa.Name
- , u.LastName + ', ' + u.FirstName
- , da.Active
- , u.ProfessionalRegistrationNumber
- , c.CustomField1PromptID
- , c.CustomField2PromptID
- , c.CustomField3PromptID
- , c.CustomField4PromptID
- , c.CustomField5PromptID
- , c.CustomField6PromptID
- , c.CentreID
- , jg.JobGroupName
- , da.ID
- , da.Answer1
- , da.Answer2
- , da.Answer3
- , da.Answer4
- , da.Answer5
- , da.Answer6
- , da.DateRegistered
- , da.UserID
- , aa.ID
- , aa.IsCentreManager
- , aa.IsCentreAdmin
- , aa.IsSupervisor
- , aa.IsNominatedSupervisor
- , ca.StartedDate
- , ca.LastAccessed
- ORDER BY
- SelfAssessment, u.LastName + ', ' + u.FirstName",
- new { centreId, selfAssessmentId }
- );
- }
- }
-}
+namespace DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService
+{
+ using Dapper;
+ using DigitalLearningSolutions.Data.Models.SelfAssessments.Export;
+ using DigitalLearningSolutions.Data.Models.SelfAssessments;
+ using Microsoft.Extensions.Logging;
+ using System.Collections.Generic;
+ using System.Data;
+ using DigitalLearningSolutions.Data.Factories;
+
+ public interface ISelfAssessmentReportDataService
+ {
+ IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId);
+ IEnumerable GetSelfAssessmentReportDataForCentre(int centreId, int selfAssessmentId);
+ }
+ public partial class SelfAssessmentReportDataService(IReadOnlyDbConnectionFactory factory, ILogger logger) : ISelfAssessmentReportDataService
+ {
+ private readonly IDbConnection connection = factory.CreateConnection();
+ private readonly ILogger logger = logger;
+
+ public IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId)
+ {
+ return connection.Query(
+ @"SELECT csa.SelfAssessmentID AS Id, sa.Name,
+ (SELECT COUNT (DISTINCT da.UserID) AS Learners
+ FROM CandidateAssessments AS ca1 INNER JOIN
+ DelegateAccounts AS da ON ca1.DelegateUserID = da.UserID
+ WHERE (da.CentreID = @centreId) AND (ca1.RemovedDate IS NULL) AND (ca1.SelfAssessmentID = csa.SelfAssessmentID) AND ca1.NonReportable=0) AS LearnerCount
+ FROM CentreSelfAssessments AS csa INNER JOIN
+ SelfAssessments AS sa ON csa.SelfAssessmentID = sa.ID
+ WHERE (csa.CentreID = @centreId) AND (sa.CategoryID = @categoryId) AND (sa.SupervisorResultsReview = 1) AND (sa.ArchivedDate IS NULL) OR
+ (csa.CentreID = @centreId) AND (sa.CategoryID = @categoryId) AND (sa.ArchivedDate IS NULL) AND (sa.SupervisorSelfAssessmentReview = 1) OR
+ (csa.CentreID = @centreId) AND (sa.SupervisorResultsReview = 1) AND (sa.ArchivedDate IS NULL) AND (@categoryId = 0) OR
+ (csa.CentreID = @centreId) AND (sa.ArchivedDate IS NULL) AND (sa.SupervisorSelfAssessmentReview = 1) AND (@categoryId = 0)
+ ORDER BY sa.Name",
+ new { centreId, categoryId = categoryId ??= 0 }
+ );
+ }
+
+ public IEnumerable GetSelfAssessmentReportDataForCentre(int centreId, int selfAssessmentId)
+ {
+ return connection.Query("usp_GetSelfAssessmentReport",
+ new { selfAssessmentId, centreId },
+ commandType: CommandType.StoredProcedure,
+ commandTimeout: 150
+ );
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs b/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs
index 3409396ab1..6dbc94777b 100644
--- a/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/SupervisorDataService.cs
@@ -547,6 +547,13 @@ public bool RemoveSupervisorDelegateById(int supervisorDelegateId, int delegateU
WHERE cas.SupervisorDelegateId = @supervisorDelegateId AND cas.Removed IS NULL AND sarsv.Verified IS NULL", new { supervisorDelegateId }
);
+ connection.Execute(
+ @"DELETE FROM casv FROM CandidateAssessmentSupervisorVerifications casv INNER JOIN
+ CandidateAssessmentSupervisors cas ON casv.CandidateAssessmentSupervisorID = cas.ID
+ WHERE cas.SupervisorDelegateId = @supervisorDelegateId
+ AND casv.Verified IS NULL AND casv.SignedOff = 0", new { supervisorDelegateId }
+ );
+
var numberOfAffectedRows = connection.Execute(
@"UPDATE SupervisorDelegates SET Removed = getUTCDate()
WHERE ID = @supervisorDelegateId AND Removed IS NULL AND (DelegateUserID = @delegateUserId OR SupervisorAdminID = @adminId)",
@@ -689,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 }
);
}
@@ -789,7 +796,8 @@ FROM CandidateAssessments AS CA
WHERE (DelegateUserID = @delegateUserId) AND (RemovedDate IS NULL)
AND (CompletedDate IS NULL)))
AND ((rp.SupervisorSelfAssessmentReview = 1) OR (rp.SupervisorResultsReview = 1))
- AND (ISNULL(@categoryId, 0) = 0 OR rp.CategoryID = @categoryId)", new { delegateUserId, centreId, categoryId }
+ AND (ISNULL(@categoryId, 0) = 0 OR rp.CategoryID = @categoryId) AND
+ ((CAST(rp.RetirementDate AS DATE) >= CAST(GETUTCDATE() AS DATE)) OR rp.RetirementDate IS NULL)", new { delegateUserId, centreId, categoryId }
);
}
diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs
index d93094c97b..9f293ad394 100644
--- a/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs
@@ -105,6 +105,7 @@ FROM AdminAccounts AS aa
aa.IsSupervisor,
aa.IsTrainer,
aa.CategoryID,
+ aa.LastAccessed,
CASE
WHEN aa.CategoryID IS NULL THEN 'All'
ELSE cc.CategoryName
@@ -369,7 +370,7 @@ public IEnumerable GetAdminAccountsByUserId(int userId)
}
string BaseSelectQuery = $@"SELECT aa.ID, aa.UserID, aa.CentreID, aa.Active, aa.IsCentreAdmin, aa.IsReportsViewer, aa.IsSuperAdmin, aa.IsCentreManager,
- aa.IsContentManager, aa.IsContentCreator, aa.IsSupervisor, aa.IsTrainer, aa.CategoryID, aa.IsFrameworkDeveloper, aa.IsFrameworkContributor,aa.ImportOnly,
+ aa.LastAccessed, aa.IsContentManager, aa.IsContentCreator, aa.IsSupervisor, aa.IsTrainer, aa.CategoryID, aa.IsFrameworkDeveloper, aa.IsFrameworkContributor,aa.ImportOnly,
aa.IsWorkforceManager, aa.IsWorkforceContributor, aa.IsLocalWorkforceManager, aa.IsNominatedSupervisor,
u.ID, u.PrimaryEmail, u.FirstName, u.LastName, u.Active, u.FailedLoginCount,
c.CentreID, c.CentreName,
diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs
index 8e2a424ed5..f2068623a9 100644
--- a/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs
@@ -87,6 +87,7 @@ FROM DelegateAccounts AS da
c.CentreName,
da.CentreID,
da.DateRegistered,
+ da.LastAccessed,
da.RegistrationConfirmationHash,
c.Active AS CentreActive,
COALESCE(ucd.Email, u.PrimaryEmail) AS EmailAddress,
@@ -124,6 +125,7 @@ FROM AdminAccounts aa
c.CentreName,
da.CentreID,
da.DateRegistered,
+ da.LastAccessed,
da.RegistrationConfirmationHash,
c.Active AS CentreActive,
COALESCE(ucd.Email, u.PrimaryEmail) AS EmailAddress,
@@ -350,6 +352,8 @@ public List GetDelegateUserCardsForExportByCentreId(String sea
if (sortBy == "SearchableName")
orderBy = " ORDER BY LTRIM(LastName) " + sortDirection + ", LTRIM(FirstName) ";
+ else if(sortBy == "LastAccessed")
+ orderBy = " ORDER BY LastAccessed " + sortDirection;
else
orderBy = " ORDER BY DateRegistered " + sortDirection;
diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserDataService.cs
index ca48e7e67b..dd583caffa 100644
--- a/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserDataService.cs
@@ -111,6 +111,7 @@ FROM DelegateAccounts AS da
ce.CentreName,
ce.Active AS CentreActive,
da.DateRegistered,
+ da.LastAccessed,
da.CandidateNumber,
da.Answer1,
da.Answer2,
diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs
index aba887ccf5..178e2f62dc 100644
--- a/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs
@@ -308,6 +308,7 @@ public partial class UserDataService : IUserDataService
u.ProfessionalRegistrationNumber,
u.ProfileImage,
u.Active,
+ u.LastAccessed,
u.ResetPasswordID,
u.TermsAgreed,
u.FailedLoginCount,
@@ -619,6 +620,7 @@ public void UpdateUserDetailsAccount(string firstName, string lastName, string p
ce.CentreName,
ce.Active AS CentreActive,
da.DateRegistered,
+ da.LastAccessed,
da.CandidateNumber,
da.Approved,
da.SelfReg,
diff --git a/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj b/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj
index 2d7bea0631..9f8678de17 100644
--- a/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj
+++ b/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj
@@ -11,15 +11,16 @@
-
+
+
-
+
diff --git a/DigitalLearningSolutions.Data/Factories/ReadOnlyConnectionFactory.cs b/DigitalLearningSolutions.Data/Factories/ReadOnlyConnectionFactory.cs
new file mode 100644
index 0000000000..0015ca8775
--- /dev/null
+++ b/DigitalLearningSolutions.Data/Factories/ReadOnlyConnectionFactory.cs
@@ -0,0 +1,29 @@
+namespace DigitalLearningSolutions.Data.Factories
+{
+ using Microsoft.Data.SqlClient;
+ using Microsoft.Extensions.Configuration;
+ using System;
+ using System.Data;
+ public interface IReadOnlyDbConnectionFactory
+ {
+ IDbConnection CreateConnection();
+ }
+ public class ReadOnlyDbConnectionFactory : IReadOnlyDbConnectionFactory
+ {
+ private const string ReadOnlyConnectionName = "ReadOnlyConnection";
+ private readonly string connectionString;
+
+ public ReadOnlyDbConnectionFactory(IConfiguration config)
+ {
+ // Ensure the connection string is not null to avoid CS8601
+ connectionString = config.GetConnectionString(ReadOnlyConnectionName)
+ ?? throw new InvalidOperationException($"Connection string '{ReadOnlyConnectionName}' is not configured.");
+ }
+
+ public IDbConnection CreateConnection()
+ {
+ // Ensure the connection is not enlisted in the trasaction scope to avoid distributed transaction errors:
+ return new SqlConnection(connectionString + ";Enlist=false");
+ }
+ }
+}
diff --git a/DigitalLearningSolutions.Data/Helpers/GenericSortingHelper.cs b/DigitalLearningSolutions.Data/Helpers/GenericSortingHelper.cs
index 7f4380d941..0165d19655 100644
--- a/DigitalLearningSolutions.Data/Helpers/GenericSortingHelper.cs
+++ b/DigitalLearningSolutions.Data/Helpers/GenericSortingHelper.cs
@@ -250,6 +250,9 @@ public static class DelegateSortByOptions
public static readonly (string DisplayText, string PropertyName) Name =
("Name", nameof(DelegateUserCard.SearchableName));
+ public static readonly (string DisplayText, string PropertyName) LastAccessed =
+ ("Last accessed date", nameof(DelegateUserCard.LastAccessed));
+
public static readonly (string DisplayText, string PropertyName) RegistrationDate =
("Registration Date", nameof(DelegateUserCard.DateRegistered));
}
diff --git a/DigitalLearningSolutions.Data/Models/CompetencyAssessments/Competency.cs b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/Competency.cs
new file mode 100644
index 0000000000..d28685ef92
--- /dev/null
+++ b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/Competency.cs
@@ -0,0 +1,15 @@
+namespace DigitalLearningSolutions.Data.Models.CompetencyAssessments
+{
+ public class Competency
+ {
+ public int StructureId { get; set; }
+ public int CompetencyID { get; set; }
+ public int FrameworkId { get; set; }
+ public string? FrameworkName { get; set; }
+ public string? GroupName { get; set; }
+ public int GroupId { get; set; }
+ public string? CompetencyName { get; set; }
+ public string? CompetencyDescription { get; set; }
+ public bool Optional { get; set; }
+ }
+}
diff --git a/DigitalLearningSolutions.Data/Models/CompetencyAssessments/CompetencyAssessmentFeatures.cs b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/CompetencyAssessmentFeatures.cs
new file mode 100644
index 0000000000..f68bfe7a67
--- /dev/null
+++ b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/CompetencyAssessmentFeatures.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DigitalLearningSolutions.Data.Models.CompetencyAssessments
+{
+ public class CompetencyAssessmentFeatures
+ {
+ public int ID { get; set; }
+ public string CompetencyAssessmentName { get; set; } = string.Empty;
+ public int UserRole { get; set; }
+ public bool DescriptionStatus { get; set; }
+ public bool ProviderandCategoryStatus { get; set; }
+ public bool VocabularyStatus { get; set; }
+ public bool WorkingGroupStatus { get; set; }
+ public bool AllframeworkCompetenciesStatus { get; set; }
+ }
+}
diff --git a/DigitalLearningSolutions.Data/Models/CompetencyAssessments/LinkedFrameworks.cs b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/LinkedFrameworks.cs
new file mode 100644
index 0000000000..c7fb0e4f2d
--- /dev/null
+++ b/DigitalLearningSolutions.Data/Models/CompetencyAssessments/LinkedFrameworks.cs
@@ -0,0 +1,8 @@
+namespace DigitalLearningSolutions.Data.Models.CompetencyAssessments
+{
+ using DigitalLearningSolutions.Data.Models.Frameworks;
+ public class LinkedFramework : BaseFramework
+ {
+ public int AssessmentFrameworkCompetencyCount { get; set; } = 0;
+ }
+}
diff --git a/DigitalLearningSolutions.Data/Models/Frameworks/BaseFramework.cs b/DigitalLearningSolutions.Data/Models/Frameworks/BaseFramework.cs
index 0291d3d13f..38adaf1cb8 100644
--- a/DigitalLearningSolutions.Data/Models/Frameworks/BaseFramework.cs
+++ b/DigitalLearningSolutions.Data/Models/Frameworks/BaseFramework.cs
@@ -22,7 +22,6 @@ public class BaseFramework : BaseSearchableItem
public string? UpdatedBy { get; set; }
public int UserRole { get; set; }
public int? FrameworkReviewID { get; set; }
-
public override string SearchableName
{
get => SearchableNameOverrideForFuzzySharp ?? FrameworkName;
diff --git a/DigitalLearningSolutions.Data/Models/Frameworks/FrameworkCompetency.cs b/DigitalLearningSolutions.Data/Models/Frameworks/FrameworkCompetency.cs
index a42d3d10fe..cf2117457c 100644
--- a/DigitalLearningSolutions.Data/Models/Frameworks/FrameworkCompetency.cs
+++ b/DigitalLearningSolutions.Data/Models/Frameworks/FrameworkCompetency.cs
@@ -1,6 +1,7 @@
namespace DigitalLearningSolutions.Data.Models.Frameworks
{
using DigitalLearningSolutions.Data.Models.SearchSortFilterPaginate;
+ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
public class FrameworkCompetency : BaseSearchableItem
{
@@ -15,6 +16,7 @@ public class FrameworkCompetency : BaseSearchableItem
public int CompetencyLearningResourcesCount { get; set; }
public string? FrameworkName { get; set; }
public bool? AlwaysShowDescription { get; set; }
+ public IEnumerable CompetencyFlags { get; set; } = new List();
public override string SearchableName
{
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.Data/Models/SelfAssessments/Export/DCSADelegateCompletionStatus.cs b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSADelegateCompletionStatus.cs
index 31a69d5bc1..ee33332727 100644
--- a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSADelegateCompletionStatus.cs
+++ b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSADelegateCompletionStatus.cs
@@ -1,16 +1,31 @@
-namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export
-{
- using System;
- public class DCSADelegateCompletionStatus
- {
- public int? EnrolledMonth { get; set; }
- public int? EnrolledYear { get; set; }
- public string? FirstName { get; set; }
- public string? LastName { get; set; }
- public string? Email { get; set; }
- public string? CentreField1 { get; set; }
- public string? CentreField2 { get; set; }
- public string? CentreField3 { get; set; }
- public string? Status { get; set; }
- }
-}
+namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export
+{
+ using System;
+ public class DCSADelegateCompletionStatus
+ {
+ public int? EnrolledMonth { get; set; }
+ public int? EnrolledYear { get; set; }
+ public string? FirstName { get; set; }
+ public string? LastName { get; set; }
+ public string? Email { get; set; }
+ public string? RegistrationAnswer1 { get; set; }
+ public string? RegistrationAnswer2 { get; set; }
+ public string? RegistrationAnswer3 { get; set; }
+ public string? RegistrationAnswer4 { get; set; }
+ public string? RegistrationAnswer5 { get; set; }
+ public string? RegistrationAnswer6 { get; set; }
+ public string? Status { get; set; }
+
+
+ public string?[] CentreRegistrationPrompts =>
+ new[]
+ {
+ RegistrationAnswer1,
+ RegistrationAnswer2,
+ RegistrationAnswer3,
+ RegistrationAnswer4,
+ RegistrationAnswer5,
+ RegistrationAnswer6,
+ };
+ }
+}
diff --git a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSAOutcomeSummary.cs b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSAOutcomeSummary.cs
index adac779073..673130a83f 100644
--- a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSAOutcomeSummary.cs
+++ b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/DCSAOutcomeSummary.cs
@@ -1,28 +1,42 @@
-namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export
-{
- using System;
- public class DCSAOutcomeSummary
- {
- public int? EnrolledMonth { get; set; }
- public int? EnrolledYear { get; set; }
- public string? JobGroup { get; set; }
- public string? CentreField1 { get; set; }
- public string? CentreField2 { get; set; }
- public string? CentreField3 { get; set; }
- public string? Status { get; set; }
- public int? LearningLaunched { get; set; }
- public int? LearningCompleted { get; set; }
- public int? DataInformationAndContentConfidence { get; set; }
- public int? DataInformationAndContentRelevance { get; set; }
- public int? TeachinglearningAndSelfDevelopmentConfidence { get; set; }
- public int? TeachinglearningAndSelfDevelopmentRelevance { get; set; }
- public int? CommunicationCollaborationAndParticipationConfidence { get; set; }
- public int? CommunicationCollaborationAndParticipationRelevance { get; set; }
- public int? TechnicalProficiencyConfidence { get; set; }
- public int? TechnicalProficiencyRelevance { get; set; }
- public int? CreationInnovationAndResearchConfidence { get; set; }
- public int? CreationInnovationAndResearchRelevance { get; set; }
- public int? DigitalIdentityWellbeingSafetyAndSecurityConfidence { get; set; }
- public int? DigitalIdentityWellbeingSafetyAndSecurityRelevance { get; set; }
- }
-}
+namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export
+{
+ using System;
+ public class DCSAOutcomeSummary
+ {
+ public int? EnrolledMonth { get; set; }
+ public int? EnrolledYear { get; set; }
+ public string? JobGroup { get; set; }
+ public string? RegistrationAnswer1 { get; set; }
+ public string? RegistrationAnswer2 { get; set; }
+ public string? RegistrationAnswer3 { get; set; }
+ public string? RegistrationAnswer4 { get; set; }
+ public string? RegistrationAnswer5 { get; set; }
+ public string? RegistrationAnswer6 { get; set; }
+ public string? Status { get; set; }
+ public int? LearningLaunched { get; set; }
+ public int? LearningCompleted { get; set; }
+ public int? DataInformationAndContentConfidence { get; set; }
+ public int? DataInformationAndContentRelevance { get; set; }
+ public int? TeachinglearningAndSelfDevelopmentConfidence { get; set; }
+ public int? TeachinglearningAndSelfDevelopmentRelevance { get; set; }
+ public int? CommunicationCollaborationAndParticipationConfidence { get; set; }
+ public int? CommunicationCollaborationAndParticipationRelevance { get; set; }
+ public int? TechnicalProficiencyConfidence { get; set; }
+ public int? TechnicalProficiencyRelevance { get; set; }
+ public int? CreationInnovationAndResearchConfidence { get; set; }
+ public int? CreationInnovationAndResearchRelevance { get; set; }
+ public int? DigitalIdentityWellbeingSafetyAndSecurityConfidence { get; set; }
+ public int? DigitalIdentityWellbeingSafetyAndSecurityRelevance { get; set; }
+
+ public string?[] CentreRegistrationPrompts =>
+ new[]
+ {
+ RegistrationAnswer1,
+ RegistrationAnswer2,
+ RegistrationAnswer3,
+ RegistrationAnswer4,
+ RegistrationAnswer5,
+ RegistrationAnswer6,
+ };
+ }
+}
diff --git a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/SelfAssessmentReportData.cs b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/SelfAssessmentReportData.cs
index 27c3d31bc7..847162bf47 100644
--- a/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/SelfAssessmentReportData.cs
+++ b/DigitalLearningSolutions.Data/Models/SelfAssessments/Export/SelfAssessmentReportData.cs
@@ -1,26 +1,41 @@
-namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export
-{
- using System;
- public class SelfAssessmentReportData
- {
- public string? SelfAssessment { get; set; }
- public string? Learner { get; set; }
- public bool LearnerActive { get; set; }
- public string? PRN { get; set; }
- public string? JobGroup { get; set; }
- public string? ProgrammeCourse { get; set; }
- public string? Organisation { get; set; }
- public string? DepartmentTeam { get; set; }
- public string? OtherCentres { get; set; }
- public string? DLSRole { get; set; }
- public DateTime? Registered { get; set; }
- public DateTime? Started { get; set; }
- public DateTime? LastAccessed { get; set; }
- public int? OptionalProficienciesAssessed { get; set; }
- public int? SelfAssessedAchieved { get; set; }
- public int? ConfirmedResults { get; set; }
- public DateTime? SignOffRequested { get; set; }
- public bool SignOffAchieved { get; set; }
- public DateTime? ReviewedDate { get; set; }
- }
-}
+namespace DigitalLearningSolutions.Data.Models.SelfAssessments.Export
+{
+ using System;
+ public class SelfAssessmentReportData
+ {
+ public string? SelfAssessment { get; set; }
+ public string? Learner { get; set; }
+ public bool LearnerActive { get; set; }
+ public string? PRN { get; set; }
+ public string? JobGroup { get; set; }
+ public string? RegistrationAnswer1 { get; set; }
+ public string? RegistrationAnswer2 { get; set; }
+ public string? RegistrationAnswer3 { get; set; }
+ public string? RegistrationAnswer4 { get; set; }
+ public string? RegistrationAnswer5 { get; set; }
+ public string? RegistrationAnswer6 { get; set; }
+ public string? OtherCentres { get; set; }
+ public string? DLSRole { get; set; }
+ public DateTime? Registered { get; set; }
+ public DateTime? Started { get; set; }
+ public DateTime? LastAccessed { get; set; }
+ public int? OptionalProficienciesAssessed { get; set; }
+ public int? SelfAssessedAchieved { get; set; }
+ public int? ConfirmedResults { get; set; }
+ public DateTime? SignOffRequested { get; set; }
+ public bool SignOffAchieved { get; set; }
+ public DateTime? ReviewedDate { get; set; }
+
+ // we need this for iteration across the registration answers from Delegate Accounts which match the custom fields of Centres.
+ public string?[] CentreRegistrationPrompts =>
+ new[]
+ {
+ RegistrationAnswer1,
+ RegistrationAnswer2,
+ RegistrationAnswer3,
+ RegistrationAnswer4,
+ RegistrationAnswer5,
+ RegistrationAnswer6,
+ };
+ }
+}
diff --git a/DigitalLearningSolutions.Data/Models/SelfAssessments/SelfAssessment.cs b/DigitalLearningSolutions.Data/Models/SelfAssessments/SelfAssessment.cs
index 1beabfb672..dbd1da0de6 100644
--- a/DigitalLearningSolutions.Data/Models/SelfAssessments/SelfAssessment.cs
+++ b/DigitalLearningSolutions.Data/Models/SelfAssessments/SelfAssessment.cs
@@ -1,4 +1,6 @@
-namespace DigitalLearningSolutions.Data.Models.SelfAssessments
+using System;
+
+namespace DigitalLearningSolutions.Data.Models.SelfAssessments
{
public class SelfAssessment : CurrentLearningItem
{
@@ -11,6 +13,10 @@ public class SelfAssessment : CurrentLearningItem
public string? ManageOptionalCompetenciesPrompt { get; set; }
public string? QuestionLabel { get; set; }
public string? DescriptionLabel { get; set; }
+ public DateTime? RetirementDate { get; set; }
+
+ public DateTime? EnrolmentCutoffDate { get; set; }
+ public string? RetirementReason { get; set; }
}
}
diff --git a/DigitalLearningSolutions.Data/Models/SessionData/Supervisor/SessionEnrolOnCompetencyAssessment.cs b/DigitalLearningSolutions.Data/Models/SessionData/Supervisor/SessionEnrolOnCompetencyAssessment.cs
index 1fd5fee129..a3271d569d 100644
--- a/DigitalLearningSolutions.Data/Models/SessionData/Supervisor/SessionEnrolOnCompetencyAssessment.cs
+++ b/DigitalLearningSolutions.Data/Models/SessionData/Supervisor/SessionEnrolOnCompetencyAssessment.cs
@@ -6,5 +6,6 @@ public class SessionEnrolOnCompetencyAssessment
public int? SelfAssessmentID { get; set; }
public DateTime? CompleteByDate { get; set; }
public int? SelfAssessmentSupervisorRoleId { get; set; }
+ public bool ActionConfirmed { get; set; }
}
}
diff --git a/DigitalLearningSolutions.Data/Models/SuperAdmin/SuperAdminDelegateAccount.cs b/DigitalLearningSolutions.Data/Models/SuperAdmin/SuperAdminDelegateAccount.cs
index a18268f104..aac978b35a 100644
--- a/DigitalLearningSolutions.Data/Models/SuperAdmin/SuperAdminDelegateAccount.cs
+++ b/DigitalLearningSolutions.Data/Models/SuperAdmin/SuperAdminDelegateAccount.cs
@@ -19,6 +19,7 @@ public SuperAdminDelegateAccount(DelegateEntity delegateEntity)
LearningHubAuthId = delegateEntity.UserAccount.LearningHubAuthId;
RegistrationConfirmationHash = delegateEntity.DelegateAccount.RegistrationConfirmationHash;
DateRegistered = delegateEntity.DelegateAccount.DateRegistered;
+ LastAccessed = delegateEntity.DelegateAccount.LastAccessed;
SelfReg = delegateEntity.DelegateAccount.SelfReg;
Active = delegateEntity.DelegateAccount.Active;
EmailVerified = delegateEntity.UserAccount.EmailVerified;
diff --git a/DigitalLearningSolutions.Data/Models/User/AdminAccount.cs b/DigitalLearningSolutions.Data/Models/User/AdminAccount.cs
index eaff1da52f..51a109785b 100644
--- a/DigitalLearningSolutions.Data/Models/User/AdminAccount.cs
+++ b/DigitalLearningSolutions.Data/Models/User/AdminAccount.cs
@@ -1,5 +1,7 @@
namespace DigitalLearningSolutions.Data.Models.User
{
+ using System;
+
public class AdminAccount
{
public int Id { get; set; }
@@ -26,6 +28,7 @@ public class AdminAccount
public bool IsWorkforceContributor { get; set; }
public bool IsLocalWorkforceManager { get; set; }
public bool IsNominatedSupervisor { get; set; }
+ public DateTime? LastAccessed { get; set; }
public bool IsCmsAdministrator => ImportOnly && IsContentManager;
public bool IsCmsManager => IsContentManager && !ImportOnly;
diff --git a/DigitalLearningSolutions.Data/Models/User/AdminEntity.cs b/DigitalLearningSolutions.Data/Models/User/AdminEntity.cs
index e070aca124..a9b71e5143 100644
--- a/DigitalLearningSolutions.Data/Models/User/AdminEntity.cs
+++ b/DigitalLearningSolutions.Data/Models/User/AdminEntity.cs
@@ -3,6 +3,7 @@
using DigitalLearningSolutions.Data.Helpers;
using DigitalLearningSolutions.Data.Models.Centres;
using DigitalLearningSolutions.Data.Models.SearchSortFilterPaginate;
+ using System;
public class AdminEntity : BaseSearchableItem
{
@@ -72,6 +73,7 @@ public override string SearchableName
public bool IsSuperAdmin => AdminAccount.IsSuperAdmin;
public bool IsReportsViewer => AdminAccount.IsReportsViewer;
public bool IsActive => AdminAccount.Active;
+ public DateTime? LastAccessed => AdminAccount.LastAccessed;
}
}
diff --git a/DigitalLearningSolutions.Data/Models/User/DelegateAccount.cs b/DigitalLearningSolutions.Data/Models/User/DelegateAccount.cs
index 3f720e350b..66d458fd13 100644
--- a/DigitalLearningSolutions.Data/Models/User/DelegateAccount.cs
+++ b/DigitalLearningSolutions.Data/Models/User/DelegateAccount.cs
@@ -12,6 +12,7 @@ public class DelegateAccount
public bool CentreActive { get; set; }
public string CandidateNumber { get; set; } = string.Empty;
public DateTime DateRegistered { get; set; }
+ public DateTime? LastAccessed { get; set; }
public string? Answer1 { get; set; }
public string? Answer2 { get; set; }
public string? Answer3 { get; set; }
diff --git a/DigitalLearningSolutions.Data/Models/User/DelegateUser.cs b/DigitalLearningSolutions.Data/Models/User/DelegateUser.cs
index 7033b8d13f..f007b355b1 100644
--- a/DigitalLearningSolutions.Data/Models/User/DelegateUser.cs
+++ b/DigitalLearningSolutions.Data/Models/User/DelegateUser.cs
@@ -9,6 +9,7 @@ public class DelegateUser : User
public int UserId { get; set; }
public string CandidateNumber { get; set; } = string.Empty;
public DateTime? DateRegistered { get; set; }
+ public DateTime? LastAccessed { get; set; }
public int JobGroupId { get; set; }
public string? JobGroupName { get; set; }
public string? Answer1 { get; set; }
diff --git a/DigitalLearningSolutions.Data/Models/User/DelegateUserCard.cs b/DigitalLearningSolutions.Data/Models/User/DelegateUserCard.cs
index ed04402308..1e9caf4e9d 100644
--- a/DigitalLearningSolutions.Data/Models/User/DelegateUserCard.cs
+++ b/DigitalLearningSolutions.Data/Models/User/DelegateUserCard.cs
@@ -23,6 +23,7 @@ public DelegateUserCard(DelegateEntity delegateEntity)
Password = delegateEntity.UserAccount.PasswordHash;
CandidateNumber = delegateEntity.DelegateAccount.CandidateNumber;
DateRegistered = delegateEntity.DelegateAccount.DateRegistered;
+ LastAccessed = delegateEntity.DelegateAccount.LastAccessed;
JobGroupId = delegateEntity.UserAccount.JobGroupId;
JobGroupName = delegateEntity.UserAccount.JobGroupName;
Answer1 = delegateEntity.DelegateAccount.Answer1;
diff --git a/DigitalLearningSolutions.Data/Models/User/UserAccount.cs b/DigitalLearningSolutions.Data/Models/User/UserAccount.cs
index 4e1c975bf1..6afca34e14 100644
--- a/DigitalLearningSolutions.Data/Models/User/UserAccount.cs
+++ b/DigitalLearningSolutions.Data/Models/User/UserAccount.cs
@@ -14,6 +14,7 @@ public class UserAccount
public string? ProfessionalRegistrationNumber { get; set; }
public byte[]? ProfileImage { get; set; }
public bool Active { get; set; }
+ public DateTime? LastAccessed { get; set; }
public int? ResetPasswordId { get; set; }
public DateTime? TermsAgreed { get; set; }
public int FailedLoginCount { get; set; }
diff --git a/DigitalLearningSolutions.Web.AutomatedUiTests/DigitalLearningSolutions.Web.AutomatedUiTests.csproj b/DigitalLearningSolutions.Web.AutomatedUiTests/DigitalLearningSolutions.Web.AutomatedUiTests.csproj
index a26a174269..4da1fb06bd 100644
--- a/DigitalLearningSolutions.Web.AutomatedUiTests/DigitalLearningSolutions.Web.AutomatedUiTests.csproj
+++ b/DigitalLearningSolutions.Web.AutomatedUiTests/DigitalLearningSolutions.Web.AutomatedUiTests.csproj
@@ -9,17 +9,17 @@
-
+
-
+
-
+ runtime; build; native; contentfiles; analyzers; buildtransitiveall
-
+ runtime; build; native; contentfiles; analyzers; buildtransitiveall
diff --git a/DigitalLearningSolutions.Web.IntegrationTests/DigitalLearningSolutions.Web.IntegrationTests.csproj b/DigitalLearningSolutions.Web.IntegrationTests/DigitalLearningSolutions.Web.IntegrationTests.csproj
index 2ca3bc89c9..fa0060eb19 100644
--- a/DigitalLearningSolutions.Web.IntegrationTests/DigitalLearningSolutions.Web.IntegrationTests.csproj
+++ b/DigitalLearningSolutions.Web.IntegrationTests/DigitalLearningSolutions.Web.IntegrationTests.csproj
@@ -10,12 +10,12 @@
-
+ runtime; build; native; contentfiles; analyzers; buildtransitiveall
-
+ runtime; build; native; contentfiles; analyzers; buildtransitiveall
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/Register/RegisterDelegateByCentreControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/Register/RegisterDelegateByCentreControllerTests.cs
index 0d4a77fc61..0040c2b514 100644
--- a/DigitalLearningSolutions.Web.Tests/Controllers/Register/RegisterDelegateByCentreControllerTests.cs
+++ b/DigitalLearningSolutions.Web.Tests/Controllers/Register/RegisterDelegateByCentreControllerTests.cs
@@ -176,7 +176,7 @@ public void LearnerInformationPost_updates_tempdata_correctly()
const string answer4 = "answer4";
const string answer5 = "answer5";
const string answer6 = "answer6";
- const string professionalRegistrationNumber = "PRN1234";
+ const string professionalRegistrationNumber = "PR123456";
var model = new LearnerInformationViewModel
{
JobGroup = jobGroupId,
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.Tests/Controllers/TrackingSystem/Delegates/EditDelegateControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/EditDelegateControllerTests.cs
index 5c5df8ac44..c08f3dbe35 100644
--- a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/EditDelegateControllerTests.cs
+++ b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/Delegates/EditDelegateControllerTests.cs
@@ -175,7 +175,7 @@ public void Index_post_returns_view_with_model_error_with_invalid_prn()
result.As().Model.Should().BeOfType();
AssertModelStateErrorIsExpected(
result,
- "Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed"
+ ErrorMessagesTestHelper.InvalidFormatError
);
A.CallTo(() => userService.GetDelegateById(A._)).MustNotHaveHappened();
}
diff --git a/DigitalLearningSolutions.Web.Tests/DigitalLearningSolutions.Web.Tests.csproj b/DigitalLearningSolutions.Web.Tests/DigitalLearningSolutions.Web.Tests.csproj
index ef4dd6e67f..6b1d9df392 100644
--- a/DigitalLearningSolutions.Web.Tests/DigitalLearningSolutions.Web.Tests.csproj
+++ b/DigitalLearningSolutions.Web.Tests/DigitalLearningSolutions.Web.Tests.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/DigitalLearningSolutions.Web.Tests/Helpers/ProfessionalRegistrationNumberHelperTests.cs b/DigitalLearningSolutions.Web.Tests/Helpers/ProfessionalRegistrationNumberHelperTests.cs
index d78c74f042..a7270f280d 100644
--- a/DigitalLearningSolutions.Web.Tests/Helpers/ProfessionalRegistrationNumberHelperTests.cs
+++ b/DigitalLearningSolutions.Web.Tests/Helpers/ProfessionalRegistrationNumberHelperTests.cs
@@ -1,11 +1,12 @@
namespace DigitalLearningSolutions.Web.Tests.Helpers
{
- using System.Linq;
using DigitalLearningSolutions.Web.Helpers;
+ using DigitalLearningSolutions.Web.Tests.TestHelpers;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NUnit.Framework;
+ using System.Linq;
public class ProfessionalRegistrationNumberHelperTests
{
@@ -70,7 +71,7 @@ public void ValidateProfessionalRegistrationNumber_does_not_set_errors_when_vali
{
// Given
var state = new ModelStateDictionary();
- const string validPrn = "abc-123";
+ const string validPrn = "AB123456";
// When
ProfessionalRegistrationNumberHelper.ValidateProfessionalRegistrationNumber(
@@ -104,22 +105,13 @@ public void ValidateProfessionalRegistrationNumber_sets_error_when_hasPrn_is_not
}
}
- [TestCase(null, "Enter a professional registration number")]
- [TestCase("", "Enter a professional registration number")]
- [TestCase("123", "Professional registration number must be between 5 and 20 characters")]
- [TestCase("0123456789-0123456789", "Professional registration number must be between 5 and 20 characters")]
- [TestCase(
- "01234_",
- "Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed"
- )]
- [TestCase(
- "01234 ",
- "Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed"
- )]
- [TestCase(
- "01234$",
- "Invalid professional registration number format - Only alphanumeric characters (a-z, A-Z and 0-9) and hyphens (-) allowed"
- )]
+ [TestCase(null, ErrorMessagesTestHelper.MissingNumberError)]
+ [TestCase("", ErrorMessagesTestHelper.MissingNumberError)]
+ [TestCase("1234", ErrorMessagesTestHelper.LengthError)]
+ [TestCase("1234", ErrorMessagesTestHelper.LengthError)]
+ [TestCase("01234_", ErrorMessagesTestHelper.InvalidFormatError)]
+ [TestCase("01234 ", ErrorMessagesTestHelper.InvalidFormatError)]
+ [TestCase("01234$", ErrorMessagesTestHelper.InvalidFormatError)]
public void ValidateProfessionalRegistrationNumber_sets_error_when_prn_is_invalid(
string prn,
string expectedError
diff --git a/DigitalLearningSolutions.Web.Tests/Services/LoginServiceTests.cs b/DigitalLearningSolutions.Web.Tests/Services/LoginServiceTests.cs
index f8dad9821e..a77c38f7b8 100644
--- a/DigitalLearningSolutions.Web.Tests/Services/LoginServiceTests.cs
+++ b/DigitalLearningSolutions.Web.Tests/Services/LoginServiceTests.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+ using DigitalLearningSolutions.Data.DataServices;
using DigitalLearningSolutions.Data.Enums;
using DigitalLearningSolutions.Data.Helpers;
using DigitalLearningSolutions.Data.Models;
@@ -32,6 +33,7 @@ private static readonly (string?, List<(int centreId, string centreName, string
private LoginService loginService = null!;
private IUserService userService = null!;
private IUserVerificationService userVerificationService = null!;
+ private ILoginDataService loginDataService = null!;
[SetUp]
public void Setup()
@@ -39,7 +41,7 @@ public void Setup()
userVerificationService = A.Fake(x => x.Strict());
userService = A.Fake(x => x.Strict());
- loginService = new LoginService(userService, userVerificationService);
+ loginService = new LoginService(userService, userVerificationService, loginDataService);
}
[Test]
diff --git a/DigitalLearningSolutions.Web.Tests/TestHelpers/ErrorMessagesTestHelper.cs b/DigitalLearningSolutions.Web.Tests/TestHelpers/ErrorMessagesTestHelper.cs
new file mode 100644
index 0000000000..7636944e7a
--- /dev/null
+++ b/DigitalLearningSolutions.Web.Tests/TestHelpers/ErrorMessagesTestHelper.cs
@@ -0,0 +1,17 @@
+
+namespace DigitalLearningSolutions.Web.Tests.TestHelpers
+{
+ public static class ErrorMessagesTestHelper
+ {
+ public const string InvalidFormatError =
+ "Invalid professional registration number format. " +
+ "Valid formats include: 7 digits (e.g., 1234567), 1–2 letters followed by 6 digits (e.g., AB123456), " +
+ "4–8 digits, an optional 'P' plus 5–6 digits, 'C' or 'P' plus 6 digits, " +
+ "an optional letter plus 5–6 digits, 'L' plus 4–6 digits, " +
+ "or 2 digits followed by a hyphen and 4–5 alphanumeric characters (e.g., 12-AB123).";
+
+ public const string MissingNumberError = "Enter a professional registration number";
+ public const string LengthError = "Professional registration number must be between 5 and 20 characters";
+
+ }
+}
diff --git a/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessments.cs b/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessments.cs
index aee8442d14..4464dee99c 100644
--- a/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessments.cs
+++ b/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessments.cs
@@ -1,20 +1,20 @@
namespace DigitalLearningSolutions.Web.Controllers.CompetencyAssessmentsController
{
+ using DigitalLearningSolutions.Data.Enums;
using DigitalLearningSolutions.Data.Models.CompetencyAssessments;
+ using DigitalLearningSolutions.Data.Models.Frameworks;
+ using DigitalLearningSolutions.Data.Models.SelfAssessments;
+ using DigitalLearningSolutions.Web.Attributes;
+ using DigitalLearningSolutions.Web.Helpers;
+ using DigitalLearningSolutions.Web.Models.Enums;
using DigitalLearningSolutions.Web.ViewModels.CompetencyAssessments;
+ using GDS.MultiPageFormData.Enums;
using Microsoft.AspNetCore.Mvc;
+ using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Logging;
+ using Serilog.Extensions.Hosting;
using System.Collections.Generic;
- using DigitalLearningSolutions.Data.Enums;
- using DigitalLearningSolutions.Web.Attributes;
- using DigitalLearningSolutions.Web.Models.Enums;
- using DigitalLearningSolutions.Web.Helpers;
using System.Linq;
- using AspNetCoreGeneratedDocument;
- using Microsoft.AspNetCore.Mvc.Rendering;
- using DigitalLearningSolutions.Data.Models.Centres;
- using DigitalLearningSolutions.Data.Models.Frameworks;
- using Microsoft.CodeAnalysis.CSharp.Syntax;
public partial class CompetencyAssessmentsController
{
@@ -83,7 +83,6 @@ public IActionResult ViewCompetencyAssessments(string tabname, string? searchStr
isWorkforceManager
);
}
-
var currentTab = tabname == "All" ? CompetencyAssessmentsTab.AllCompetencyAssessments : CompetencyAssessmentsTab.MyCompetencyAssessments;
CompetencyAssessmentsViewModel? model = new CompetencyAssessmentsViewModel(
isWorkforceManager,
@@ -97,12 +96,18 @@ public IActionResult ViewCompetencyAssessments(string tabname, string? searchStr
[Route("/CompetencyAssessments/{actionName}/Name/{competencyAssessmentId}")]
[Route("/CompetencyAssessments/Framework/{frameworkId}/{actionName}/Name")]
+ [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/{actionName}/Name")]
[Route("/CompetencyAssessments/{actionName}/Name")]
[SetSelectedTab(nameof(NavMenuTab.CompetencyAssessments))]
public IActionResult CompetencyAssessmentName(string actionName, int competencyAssessmentId = 0, int? frameworkId = null)
{
var adminId = GetAdminID();
var competencyAssessmentBase = new CompetencyAssessmentBase();
+ if ((frameworkId.HasValue && frameworkId.Value != 0 && actionName == "New"))
+ {
+ var data = new CompetencyAssessmentFeaturesViewModel();
+ SetcompetencyAssessmentFeaturesData(data);
+ }
if (competencyAssessmentId > 0)
{
competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
@@ -130,6 +135,7 @@ public IActionResult CompetencyAssessmentName(string actionName, int competencyA
[HttpPost]
[Route("/CompetencyAssessments/{actionName}/Name/{competencyAssessmentId}")]
[Route("/CompetencyAssessments/Framework/{frameworkId}/{actionName}/Name")]
+ [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/{actionName}/Name")]
[Route("/CompetencyAssessments/{actionName}/Name")]
[SetSelectedTab(nameof(NavMenuTab.CompetencyAssessments))]
public IActionResult SaveProfileName(CompetencyAssessmentBase competencyAssessmentBase, string actionName, int competencyAssessmentId = 0, int? frameworkId = null)
@@ -154,6 +160,7 @@ public IActionResult SaveProfileName(CompetencyAssessmentBase competencyAssessme
return View("Name", competencyAssessmentBase);
}
competencyAssessmentId = competencyAssessmentService.InsertCompetencyAssessment(adminId, userCentreId, competencyAssessmentBase.CompetencyAssessmentName, frameworkId);
+ if(frameworkId.HasValue && frameworkId.Value != 0) return RedirectToAction("CompetencyAssessmentFeatures", new { competencyAssessmentId, frameworkId });
}
else
{
@@ -164,6 +171,9 @@ public IActionResult SaveProfileName(CompetencyAssessmentBase competencyAssessme
ModelState.AddModelError(nameof(CompetencyAssessmentBase.CompetencyAssessmentName), "Another competency assessment exists with that name. Please choose a different name.");
return View("Name", competencyAssessmentBase);
}
+ if (frameworkId.HasValue && frameworkId.Value != 0
+ && competencyAssessmentId != 0
+ && actionName == "Edit") return RedirectToAction("CompetencyAssessmentFeatures", new { competencyAssessmentId, frameworkId });
}
return RedirectToAction("ManageCompetencyAssessment", new { competencyAssessmentId, frameworkId });
}
@@ -389,5 +399,332 @@ public IActionResult SaveVocabulary(EditVocabularyViewModel model)
competencyAssessmentService.UpdateVocabularyTaskStatus(model.ID, model.TaskStatus ?? false);
return RedirectToAction("ManageCompetencyAssessment", new { competencyAssessmentId = model.ID });
}
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Frameworks/{actionName}")]
+ public IActionResult SelectFrameworkSources(int competencyAssessmentId, string actionName)
+ {
+ var adminId = GetAdminID();
+ var frameworks = frameworkService.GetAllFrameworks(adminId);
+ var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
+ if (competencyAssessmentBase == null)
+ {
+ logger.LogWarning($"Failed to load Vocabulary page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}");
+ return StatusCode(500);
+ }
+ if (competencyAssessmentBase.UserRole < 2)
+ {
+ return StatusCode(403);
+ }
+ var primaryFrameworkId = competencyAssessmentService.GetPrimaryLinkedFrameworkId(competencyAssessmentId);
+ var additionalFrameworks = competencyAssessmentService.GetLinkedFrameworkIds(competencyAssessmentId);
+ var competencyAssessmentTaskStatus = competencyAssessmentService.GetCompetencyAssessmentTaskStatus(competencyAssessmentId, null);
+ var model = new SelectFrameworkSourcesViewModel(competencyAssessmentBase, frameworks, additionalFrameworks, primaryFrameworkId, competencyAssessmentTaskStatus.FrameworkLinksTaskStatus, actionName);
+ return View(model);
+ }
+ [HttpPost]
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Frameworks/{actionName}")]
+ public IActionResult SelectFrameworkSources(SelectFrameworkSourcesFormData model, string actionName)
+ {
+ var adminId = GetAdminID();
+ var competencyAssessmentId = model.CompetencyAssessmentId;
+ if (!ModelState.IsValid)
+ {
+
+ var frameworks = frameworkService.GetAllFrameworks(adminId);
+ var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
+ if (competencyAssessmentBase == null)
+ {
+ logger.LogWarning($"Failed to load Vocabulary page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}");
+ return StatusCode(500);
+ }
+ if (competencyAssessmentBase.UserRole < 2)
+ {
+ return StatusCode(403);
+ }
+ var primaryFrameworkId = competencyAssessmentService.GetPrimaryLinkedFrameworkId(competencyAssessmentId);
+ var additionalFrameworks = competencyAssessmentService.GetLinkedFrameworkIds(competencyAssessmentId);
+ var viewModel = new SelectFrameworkSourcesViewModel(competencyAssessmentBase, frameworks, additionalFrameworks, primaryFrameworkId, model.TaskStatus, model.ActionName);
+ return View("SelectFrameworkSources", viewModel);
+ }
+ if (actionName == "AddFramework")
+ {
+ competencyAssessmentService.InsertSelfAssessmentFramework(adminId, competencyAssessmentId, model.FrameworkId);
+ return RedirectToAction("SelectFrameworkSources", new { competencyAssessmentId, actionName = "Summary" });
+ }
+ else
+ {
+ competencyAssessmentService.UpdateFrameworkLinksTaskStatus(model.CompetencyAssessmentId, model.TaskStatus ?? false, null);
+ return RedirectToAction("ManageCompetencyAssessment", new { competencyAssessmentId = model.CompetencyAssessmentId });
+ }
+ }
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Frameworks/{frameworkId}/Remove")]
+ public IActionResult RemoveFramework(int frameworkId, int competencyAssessmentId)
+ {
+ var frameworkCompetencyCount = competencyAssessmentService.GetCompetencyCountByFrameworkId(competencyAssessmentId, frameworkId);
+ if (frameworkCompetencyCount > 0)
+ {
+ var adminId = GetAdminID();
+ var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
+ var framework = frameworkService.GetFrameworkDetailByFrameworkId(frameworkId, adminId);
+ var model = new ConfirmRemoveFrameworkSourceViewModel(competencyAssessmentBase, framework, frameworkCompetencyCount);
+ return View("ConfirmRemoveFrameworkSource", model);
+ }
+ else
+ {
+ var adminId = GetAdminID();
+ competencyAssessmentService.RemoveSelfAssessmentFramework(competencyAssessmentId, frameworkId, adminId);
+ }
+ return RedirectToAction("SelectFrameworkSources", new { competencyAssessmentId, actionName = "Summary" });
+ }
+ [HttpPost]
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Frameworks/{frameworkId}/Remove")]
+ public IActionResult RemoveFramework(ConfirmRemoveFrameworkSourceViewModel model)
+ {
+ if (!ModelState.IsValid)
+ {
+ return View("ConfirmRemoveFrameworkSource", model);
+ }
+ var adminId = GetAdminID();
+ competencyAssessmentService.RemoveFrameworkCompetenciesFromAssessment(model.CompetencyAssessmentId, model.FrameworkId);
+ competencyAssessmentService.RemoveSelfAssessmentFramework(model.CompetencyAssessmentId, model.FrameworkId, adminId);
+ return RedirectToAction("SelectFrameworkSources", new { model.CompetencyAssessmentId, actionName = "Summary" });
+ }
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies")]
+ public IActionResult ViewSelectedCompetencies(int competencyAssessmentId)
+ {
+
+ var competencies = competencyAssessmentService.GetCompetenciesForCompetencyAssessment(competencyAssessmentId);
+ var linkedFrameworks = competencyAssessmentService.GetLinkedFrameworksForCompetencyAssessment(competencyAssessmentId);
+ if (!competencies.Any())
+ {
+ return RedirectToAction("AddCompetenciesSelectFramework", new { competencyAssessmentId });
+ }
+ var adminId = GetAdminID();
+ var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
+ if (competencyAssessmentBase == null)
+ {
+ logger.LogWarning($"Failed to load Competencies page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}");
+ return StatusCode(500);
+ }
+ if (competencyAssessmentBase.UserRole < 2)
+ {
+ return StatusCode(403);
+ }
+ var competencyAssessmentTaskStatus = competencyAssessmentService.GetCompetencyAssessmentTaskStatus(competencyAssessmentId, null);
+ var model = new ViewSelectedCompetenciesViewModel(competencyAssessmentBase, competencies, linkedFrameworks, competencyAssessmentTaskStatus.SelectCompetenciesTaskStatus);
+ return View(model);
+ }
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies/Add/SelectFramework")]
+ public IActionResult AddCompetenciesSelectFramework(int competencyAssessmentId)
+ {
+ var linkedFrameworks = competencyAssessmentService.GetLinkedFrameworksForCompetencyAssessment(competencyAssessmentId);
+ if (!linkedFrameworks.Any())
+ {
+ return RedirectToAction("SelectFrameworkSources", new { competencyAssessmentId, actionName = "AddFramework" });
+ }
+ var adminId = GetAdminID();
+ var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
+ if (competencyAssessmentBase == null)
+ {
+ logger.LogWarning($"Failed to load Competencies page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}");
+ return StatusCode(500);
+ }
+ if (competencyAssessmentBase.UserRole < 2)
+ {
+ return StatusCode(403);
+ }
+
+ var model = new AddCompetenciesSelectFrameworkViewModel(competencyAssessmentBase, linkedFrameworks);
+ return View(model);
+ }
+ [HttpPost]
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies/Add/SelectFramework")]
+ public IActionResult AddCompetenciesSelectFramework(AddCompetenciesSelectFrameworkFormData formdata)
+ {
+ if (!ModelState.IsValid)
+ {
+ var competencyAssessmentId = formdata.ID;
+ var linkedFrameworks = competencyAssessmentService.GetLinkedFrameworksForCompetencyAssessment(competencyAssessmentId);
+ var adminId = GetAdminID();
+ var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
+ var model = new AddCompetenciesSelectFrameworkViewModel(competencyAssessmentBase, linkedFrameworks);
+ model.FrameworkId = formdata.FrameworkId;
+ return View("AddCompetenciesSelectFramework", model);
+ }
+ else
+ {
+ return RedirectToAction("AddCompetencies", new { competencyAssessmentId = formdata.ID, frameworkId = formdata.FrameworkId });
+ }
+ }
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies/Add/{frameworkId}")]
+ public IActionResult AddCompetencies(int competencyAssessmentId, int frameworkId)
+ {
+ var adminId = GetAdminID();
+ var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
+ if (competencyAssessmentBase == null)
+ {
+ logger.LogWarning($"Failed to load Competencies page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}");
+ return StatusCode(500);
+ }
+ if (competencyAssessmentBase.UserRole < 2)
+ {
+ return StatusCode(403);
+ }
+ var framework = frameworkService.GetBaseFrameworkByFrameworkId(frameworkId, adminId);
+ var selectedFrameworkCompetencies = competencyAssessmentService.GetLinkedFrameworkCompetencyIds(competencyAssessmentId, frameworkId);
+ var groupedCompetencies = frameworkService.GetFrameworkCompetencyGroups(frameworkId, competencyAssessmentId);
+ var ungroupedCompetencies = frameworkService.GetFrameworkCompetenciesUngrouped(frameworkId, competencyAssessmentId);
+ var competencyIds = ungroupedCompetencies.Select(c => c.CompetencyID).ToArray();
+ var competencyFlags = frameworkService.GetSelectedCompetencyFlagsByCompetecyIds(competencyIds);
+ foreach (var competency in ungroupedCompetencies)
+ competency.CompetencyFlags = competencyFlags.Where(f => f.CompetencyId == competency.CompetencyID);
+ foreach (var group in groupedCompetencies)
+ {
+ competencyIds = group.FrameworkCompetencies.Select(c => c.CompetencyID).ToArray();
+ competencyFlags = frameworkService.GetSelectedCompetencyFlagsByCompetecyIds(competencyIds);
+ foreach (var competency in group.FrameworkCompetencies)
+ competency.CompetencyFlags = competencyFlags.Where(f => f.CompetencyId == competency.CompetencyID);
+ }
+ var model = new AddCompetenciesViewModel(competencyAssessmentBase, groupedCompetencies, ungroupedCompetencies, frameworkId, framework.FrameworkName, selectedFrameworkCompetencies);
+ return View(model);
+ }
+ [HttpPost]
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies/Add/{frameworkId}")]
+ public IActionResult AddComptencies(AddCompetenciesFormData model, int competencyAssessmentId, int frameworkId)
+ {
+ if (!ModelState.IsValid)
+ {
+ //reload model and view
+ }
+ if (model.SelectedCompetencyIds != null)
+ {
+ competencyAssessmentService.InsertCompetenciesIntoAssessmentFromFramework(model.SelectedCompetencyIds, frameworkId, competencyAssessmentId);
+ }
+ competencyAssessmentService.UpdateSelectCompetenciesTaskStatus(competencyAssessmentId, false, null);
+ return RedirectToAction("ViewSelectedCompetencies", new { competencyAssessmentId });
+ }
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies/Delete/{competencyId}")]
+ public IActionResult DeleteCompetency(int competencyAssessmentId, int competencyId)
+ {
+ competencyAssessmentService.RemoveCompetencyFromAssessment(competencyAssessmentId, competencyId);
+ return RedirectToAction("ViewSelectedCompetencies", new { competencyAssessmentId });
+ }
+ public IActionResult MoveCompetencyInSelfAssessment(int competencyAssessmentId, int competencyId, string direction)
+ {
+ var adminId = GetAdminID();
+ var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
+ if (competencyAssessmentBase == null)
+ {
+ logger.LogWarning($"Failed to load Competencies page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}");
+ return StatusCode(500);
+ }
+ if (competencyAssessmentBase.UserRole < 2)
+ {
+ return StatusCode(403);
+ }
+ competencyAssessmentService.MoveCompetencyInSelfAssessment(competencyAssessmentId, competencyId, direction);
+ return new RedirectResult(Url.Action("ViewSelectedCompetencies", new { competencyAssessmentId }) + "#competency-" + competencyId.ToString());
+ }
+ public IActionResult MoveCompetencyGroupInSelfAssessment(int competencyAssessmentId, int groupId, string direction)
+ {
+ var adminId = GetAdminID();
+ var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
+ if (competencyAssessmentBase == null)
+ {
+ logger.LogWarning($"Failed to load Competencies page for competencyAssessmentId: {competencyAssessmentId} adminId: {adminId}");
+ return StatusCode(500);
+ }
+ if (competencyAssessmentBase.UserRole < 2)
+ {
+ return StatusCode(403);
+ }
+ competencyAssessmentService.MoveCompetencyGroupInSelfAssessment(competencyAssessmentId, groupId, direction);
+ return new RedirectResult(Url.Action("ViewSelectedCompetencies", new { competencyAssessmentId }) + "#group-" + groupId.ToString());
+ }
+ [HttpPost]
+ [Route("/CompetencyAssessments/{competencyAssessmentId}/Competencies")]
+ public IActionResult ViewSelectedCompetencies(ViewSelectedCompetenciesFormData model)
+ {
+ if (model.TaskStatus == null)
+ {
+ model.TaskStatus = false;
+ }
+ competencyAssessmentService.UpdateSelectCompetenciesTaskStatus(model.ID, model.TaskStatus.Value, null);
+ return RedirectToAction("ManageCompetencyAssessment", new { competencyAssessmentId = model.ID });
+ }
+
+ [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/Features")]
+ public IActionResult CompetencyAssessmentFeatures(int competencyAssessmentId, int? frameworkId = null)
+ {
+
+ var adminId = GetAdminID();
+ var data = GetcompetencyAssessmentFeaturesData();
+ if (!string.IsNullOrEmpty(data.CompetencyAssessmentName)) return View(data);
+ var competencyAssessmentBase = competencyAssessmentService.GetCompetencyAssessmentBaseById(competencyAssessmentId, adminId);
+ if (competencyAssessmentBase == null) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 500 });
+ if (competencyAssessmentBase.UserRole < 2) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 });
+ var baseModel = new CompetencyAssessmentFeaturesViewModel(competencyAssessmentBase.ID,
+ competencyAssessmentBase.CompetencyAssessmentName,
+ competencyAssessmentBase.UserRole,
+ frameworkId);
+ return View(baseModel);
+ }
+ [HttpPost]
+ [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/Features")]
+ public IActionResult CompetencyAssessmentFeatures(CompetencyAssessmentFeaturesViewModel featuresViewModel)
+ {
+ if (featuresViewModel == null) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 500 });
+ SetcompetencyAssessmentFeaturesData(featuresViewModel);
+ return RedirectToAction("CompetencyAssessmentSummary", new { competencyAssessmentId = featuresViewModel.ID,featuresViewModel.FrameworkId });
+ }
+
+ [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/Summary")]
+ public IActionResult CompetencyAssessmentSummary(int competencyAssessmentId, int? frameworkId = null)
+ {
+ if (competencyAssessmentService.GetSelfAssessmentStructure(competencyAssessmentId) != 0) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 });
+ if (competencyAssessmentId == 0) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 });
+ var data = GetcompetencyAssessmentFeaturesData();
+ if (data == null) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 500 });
+ SetcompetencyAssessmentFeaturesData(data);
+ return View(data);
+ }
+ [HttpPost]
+ [Route("/CompetencyAssessments/Framework/{frameworkId}/{competencyAssessmentId}/Summary")]
+ public IActionResult CompetencyAssessmentSummary(CompetencyAssessmentFeaturesViewModel competency)
+ {
+ var data = GetcompetencyAssessmentFeaturesData();
+ if (data.ID == 0) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 403 });
+ if (competencyAssessmentService.GetSelfAssessmentStructure(data.ID) != 0) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 });
+ var features = competencyAssessmentService.UpdateCompetencyAssessmentFeaturesTaskStatus(data.ID,
+ data.DescriptionStatus,
+ data.ProviderandCategoryStatus,
+ data.VocabularyStatus,
+ data.WorkingGroupStatus,
+ data.AllframeworkCompetenciesStatus);
+ if (!features) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 500 });
+ competencyAssessmentService.UpdateSelfAssessmentFromFramework(data.ID , data.FrameworkId );
+ var insertSelfAssessment = competencyAssessmentService.InsertSelfAssessmentStructure(data.ID, data.FrameworkId);
+ if (!insertSelfAssessment) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 500 });
+ multiPageFormService.ClearMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("AssessmentFeaturesDataCWF"), TempData);
+ TempData.Clear();
+ return RedirectToAction("ManageCompetencyAssessment", new { competencyAssessmentId = competency.ID, competency.FrameworkId });
+ }
+
+ private void SetcompetencyAssessmentFeaturesData(CompetencyAssessmentFeaturesViewModel data)
+ {
+ multiPageFormService.SetMultiPageFormData(
+ data,
+ MultiPageFormDataFeature.AddCustomWebForm("AssessmentFeaturesDataCWF"),
+ TempData
+ );
+ }
+
+ private CompetencyAssessmentFeaturesViewModel GetcompetencyAssessmentFeaturesData()
+ {
+ var data = multiPageFormService.GetMultiPageFormData(
+ MultiPageFormDataFeature.AddCustomWebForm("AssessmentFeaturesDataCWF"),
+ TempData
+ ).GetAwaiter().GetResult();
+ return data;
+ }
}
}
diff --git a/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessmentsController.cs b/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessmentsController.cs
index c057b43776..a07a957ee0 100644
--- a/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessmentsController.cs
+++ b/DigitalLearningSolutions.Web/Controllers/CompetencyAssessmentsController/CompetencyAssessmentsController.cs
@@ -2,6 +2,7 @@
{
using DigitalLearningSolutions.Web.Helpers;
using DigitalLearningSolutions.Web.Services;
+ using GDS.MultiPageFormData;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
@@ -16,13 +17,15 @@ public partial class CompetencyAssessmentsController : Controller
private readonly IFrameworkNotificationService frameworkNotificationService;
private readonly ILogger logger;
private readonly IConfiguration config;
+ private readonly IMultiPageFormService multiPageFormService;
public CompetencyAssessmentsController(
ICompetencyAssessmentService competencyAssessmentService,
IFrameworkService frameworkService,
ICommonService commonService,
IFrameworkNotificationService frameworkNotificationService,
ILogger logger,
- IConfiguration config)
+ IConfiguration config,
+ IMultiPageFormService multiPageFormService)
{
this.competencyAssessmentService = competencyAssessmentService;
this.frameworkService = frameworkService;
@@ -30,6 +33,7 @@ public CompetencyAssessmentsController(
this.frameworkNotificationService = frameworkNotificationService;
this.logger = logger;
this.config = config;
+ this.multiPageFormService = multiPageFormService;
}
public IActionResult Index()
{
diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/AssessmentQuestions.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/AssessmentQuestions.cs
index f4ffce91d1..519c8881bd 100644
--- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/AssessmentQuestions.cs
+++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/AssessmentQuestions.cs
@@ -456,6 +456,7 @@ public IActionResult EditAssessmentQuestionOptions(AssessmentQuestionDetail asse
return RedirectToAction("EditAssessmentQuestionOptions", "Frameworks", new { frameworkId, assessmentQuestionId, frameworkCompetencyId });
}
assessmentQuestionDetail.ScoringInstructions = SanitizerHelper.SanitizeHtmlData(assessmentQuestionDetail.ScoringInstructions);
+ if (string.IsNullOrWhiteSpace(StringHelper.StripHtmlTags(assessmentQuestionDetail?.ScoringInstructions))) { assessmentQuestionDetail.ScoringInstructions = null; }
SessionAssessmentQuestion sessionAssessmentQuestion = multiPageFormService
.GetMultiPageFormData(MultiPageFormDataFeature.EditAssessmentQuestion, TempData)
.GetAwaiter()
diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Competencies.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Competencies.cs
index 0d26aee6a3..19a918296f 100644
--- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Competencies.cs
+++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Competencies.cs
@@ -65,13 +65,14 @@ public IActionResult AddEditFrameworkCompetencyGroup(int frameworkId, Competency
var adminId = GetAdminId();
var userRole = frameworkService.GetAdminUserRoleForFrameworkId(adminId, frameworkId);
if (userRole < 2) return StatusCode(403);
+ if (string.IsNullOrWhiteSpace(StringHelper.StripHtmlTags(competencyGroupBase?.Description))) { competencyGroupBase.Description = null; }
if (competencyGroupBase.ID > 0)
{
- frameworkService.UpdateFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupBase.CompetencyGroupID, competencyGroupBase.Name, SanitizerHelper.SanitizeHtmlData
+ frameworkService.UpdateFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupBase.CompetencyGroupID, competencyGroupBase.Name.Trim(), SanitizerHelper.SanitizeHtmlData
(competencyGroupBase.Description), adminId);
return new RedirectResult(Url.Action("ViewFramework", new { tabname = "Structure", frameworkId }) + "#fcgroup-" + frameworkCompetencyGroupId.ToString());
}
- var newCompetencyGroupId = frameworkService.InsertCompetencyGroup(competencyGroupBase.Name, SanitizerHelper.SanitizeHtmlData(competencyGroupBase.Description), adminId, frameworkId);
+ var newCompetencyGroupId = frameworkService.InsertCompetencyGroup(competencyGroupBase.Name.Trim(), SanitizerHelper.SanitizeHtmlData(competencyGroupBase.Description), adminId, frameworkId);
if (newCompetencyGroupId > 0)
{
var newFrameworkCompetencyGroupId = frameworkService.InsertFrameworkCompetencyGroup(newCompetencyGroupId, frameworkId, adminId);
@@ -102,7 +103,7 @@ public IActionResult DeleteFrameworkCompetencyGroup(int frameworkId, int compete
var adminId = GetAdminId();
- frameworkService.DeleteFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupId, adminId);
+ frameworkService.DeleteFrameworkCompetencyGroup(frameworkCompetencyGroupId, competencyGroupId, frameworkId, adminId);
return new RedirectResult(Url.Action("ViewFramework", new { tabname = "Structure", frameworkId, frameworkCompetencyGroupId }));
}
@@ -141,14 +142,14 @@ public IActionResult AddEditFrameworkCompetency(int frameworkId, FrameworkCompet
frameworkCompetency.Description?.Trim();
var description = HttpUtility.HtmlDecode(HttpUtility.HtmlDecode(frameworkCompetency.Description));
var detailFramework = frameworkService.GetDetailFrameworkByFrameworkId(frameworkId, GetAdminId());
- if (string.IsNullOrWhiteSpace(description)) { frameworkCompetency.Description = null; }
+ if (string.IsNullOrWhiteSpace(StringHelper.StripHtmlTags(description))) { frameworkCompetency.Description = null; }
if (!ModelState.IsValid)
{
ModelState.Remove(nameof(FrameworkCompetency.Name));
ModelState.AddModelError(nameof(FrameworkCompetency.Name), "Please enter a valid competency statement (between 3 and 500 characters)");
var competencyFlags = frameworkService.GetCompetencyFlagsByFrameworkId(frameworkId, frameworkCompetency?.CompetencyID).ToList();
if (competencyFlags != null)
- competencyFlags.ForEach(f => f.Selected = selectedFlagIds.Contains(f.FlagId));
+ competencyFlags.ForEach(f => f.Selected = selectedFlagIds != null ? selectedFlagIds.Contains(f.FlagId) : false);
if (detailFramework == null)
return StatusCode((int)HttpStatusCode.NotFound);
var model = new FrameworkCompetencyViewModel()
@@ -158,6 +159,7 @@ public IActionResult AddEditFrameworkCompetency(int frameworkId, FrameworkCompet
FrameworkCompetency = frameworkCompetency,
CompetencyFlags = competencyFlags
};
+ ModelState.Remove("SearchableName");
return View("Developer/Competency", model);
}
var adminId = GetAdminId();
@@ -167,7 +169,7 @@ public IActionResult AddEditFrameworkCompetency(int frameworkId, FrameworkCompet
{
- frameworkService.UpdateFrameworkCompetency(frameworkCompetencyId, frameworkCompetency.Name, frameworkCompetency.Description, adminId);
+ frameworkService.UpdateFrameworkCompetency(frameworkCompetencyId, frameworkCompetency.Name.Trim(), frameworkCompetency.Description, adminId);
frameworkService.UpdateCompetencyFlags(frameworkId, frameworkCompetency.CompetencyID, selectedFlagIds);
return new RedirectResult(Url.Action("ViewFramework", new { tabname = "Structure", frameworkId, frameworkCompetencyGroupId, frameworkCompetencyId }) + "#fc-" + frameworkCompetencyId.ToString());
}
@@ -214,7 +216,7 @@ public IActionResult AddDuplicateCompetency(int frameworkId, string competencyNa
private IActionResult SaveCompetency(int adminId, int frameworkId, FrameworkCompetency frameworkCompetency, int frameworkCompetencyId, int? frameworkCompetencyGroupId, int[]? selectedFlagIds)
{
- var newCompetencyId = frameworkService.InsertCompetency(frameworkCompetency.Name, frameworkCompetency.Description, adminId);
+ var newCompetencyId = frameworkService.InsertCompetency(frameworkCompetency.Name.Trim(), frameworkCompetency.Description, adminId);
if (newCompetencyId > 0)
{
var newFrameworkCompetencyId = frameworkService.InsertFrameworkCompetency(newCompetencyId, frameworkCompetencyGroupId, adminId, frameworkId);
@@ -235,7 +237,7 @@ public IActionResult DeleteFrameworkCompetency(int frameworkId, int frameworkCom
{
var userRole = frameworkService.GetAdminUserRoleForFrameworkId(GetAdminId(), frameworkId);
if (userRole < 2) return StatusCode(403);
- frameworkService.DeleteFrameworkCompetency(frameworkCompetencyId, GetAdminId());
+ frameworkService.DeleteFrameworkCompetency(frameworkCompetencyId, frameworkCompetencyGroupId, frameworkId, GetAdminId());
return frameworkCompetencyGroupId != null ? new RedirectResult(Url.Action("ViewFramework", new { tabname = "Structure", frameworkId, frameworkCompetencyGroupId }) + "#fcgroup-" + frameworkCompetencyGroupId.ToString()) : new RedirectResult(Url.Action("ViewFramework", new { tabname = "Structure", frameworkId }) + "#fc-ungrouped");
}
[Route("/Frameworks/{frameworkId}/Competency/{frameworkCompetencyGroupId:int=0}/{frameworkCompetencyId}/Preview/")]
diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Frameworks.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Frameworks.cs
index b2f67f73e5..3d3a85d9cf 100644
--- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Frameworks.cs
+++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Frameworks.cs
@@ -582,14 +582,33 @@ public IActionResult EditFrameworkFlag(CustomFlagViewModel model, int frameworkI
{
if (ModelState.IsValid)
{
+ var flags = frameworkService.GetCustomFlagsByFrameworkId(frameworkId, null)
+ .Where(fn => fn.FlagName?.Trim().ToLower() == model.FlagName?.Trim().ToLower()).ToList();
+
+ bool nameExists = flags.Any(x => x.FlagName?.Trim().ToLower() == model.FlagName?.Trim().ToLower());
+ bool idExists = flags.Any(x => x.FlagId == flagId);
+
if (actionName == "Edit")
{
- frameworkService.UpdateFrameworkCustomFlag(frameworkId, model.Id, model.FlagName, model.FlagGroup, model.FlagTagClass);
+ if (nameExists && !idExists)
+ {
+ ModelState.AddModelError(nameof(model.FlagName), "A custom flag already exists.");
+ return View("Developer/EditCustomFlag", model);
+ }
+ else
+ frameworkService.UpdateFrameworkCustomFlag(frameworkId, model.Id, model.FlagName?.Trim(), model.FlagGroup?.Trim(), model.FlagTagClass);
}
else
{
- frameworkService.AddCustomFlagToFramework(frameworkId, model.FlagName, model.FlagGroup, model.FlagTagClass);
+ if (nameExists)
+ {
+ ModelState.AddModelError(nameof(model.FlagName), "A custom flag already exists.");
+ return View("Developer/EditCustomFlag", model);
+ }
+ else
+ frameworkService.AddCustomFlagToFramework(frameworkId, model.FlagName?.Trim(), model.FlagGroup?.Trim(), model.FlagTagClass);
}
+
return RedirectToAction("EditFrameworkFlags", "Frameworks", new { frameworkId });
}
return View("Developer/EditCustomFlag", model);
@@ -704,9 +723,9 @@ public IActionResult ViewFramework(string tabname, int frameworkId, int? framewo
model.TabNavLinks = new TabsNavViewModel(FrameworkTab.Details, routeData);
break;
case "Structure":
- model.FrameworkCompetencyGroups = frameworkService.GetFrameworkCompetencyGroups(frameworkId).ToList();
+ model.FrameworkCompetencyGroups = frameworkService.GetFrameworkCompetencyGroups(frameworkId, null).ToList();
model.CompetencyFlags = frameworkService.GetCompetencyFlagsByFrameworkId(frameworkId, null, selected: true);
- model.FrameworkCompetencies = frameworkService.GetFrameworkCompetenciesUngrouped(frameworkId);
+ model.FrameworkCompetencies = frameworkService.GetFrameworkCompetenciesUngrouped(frameworkId, null);
model.TabNavLinks = new TabsNavViewModel(FrameworkTab.Structure, routeData);
break;
case "Comments":
@@ -718,7 +737,8 @@ public IActionResult ViewFramework(string tabname, int frameworkId, int? framewo
}
[Route("/Framework/{frameworkId}/Structure/PrintLayout")]
- public IActionResult PrintLayout(int frameworkId) {
+ public IActionResult PrintLayout(int frameworkId)
+ {
var adminId = GetAdminId();
var detailFramework = frameworkService.GetFrameworkDetailByFrameworkId(frameworkId, adminId);
var routeData = new Dictionary { { "frameworkId", detailFramework?.ID.ToString() } };
@@ -726,9 +746,9 @@ public IActionResult PrintLayout(int frameworkId) {
{
DetailFramework = detailFramework,
};
- model.FrameworkCompetencyGroups = frameworkService.GetFrameworkCompetencyGroups(frameworkId).ToList();
+ model.FrameworkCompetencyGroups = frameworkService.GetFrameworkCompetencyGroups(frameworkId, null).ToList();
model.CompetencyFlags = frameworkService.GetCompetencyFlagsByFrameworkId(frameworkId, null, selected: true);
- model.FrameworkCompetencies = frameworkService.GetFrameworkCompetenciesUngrouped(frameworkId);
+ model.FrameworkCompetencies = frameworkService.GetFrameworkCompetenciesUngrouped(frameworkId, null);
return View("Developer/FrameworkPrintLayout", model);
}
[ResponseCache(CacheProfileName = "Never")]
diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/ImportCompetencies.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/ImportCompetencies.cs
index 752befe216..e272f6da1e 100644
--- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/ImportCompetencies.cs
+++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/ImportCompetencies.cs
@@ -10,6 +10,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Linq.Expressions;
namespace DigitalLearningSolutions.Web.Controllers.FrameworksController
{
@@ -45,7 +46,13 @@ public IActionResult DownloadCompetencies(int frameworkId, int DownloadOption, s
public IActionResult StartImport(ImportCompetenciesFormData model, int frameworkId, string tabname, bool isNotBlank)
{
if (!ModelState.IsValid)
- return View("Developer/Import/Index", model);
+ {
+ var adminId = GetAdminId();
+ var framework = frameworkService.GetFrameworkDetailByFrameworkId(frameworkId, adminId);
+ var viewModel = new ImportCompetenciesViewModel(framework, isNotBlank);
+ viewModel.ImportFile = model.ImportFile;
+ return View("Developer/Import/Index", viewModel);
+ }
try
{
var adminUserID = User.GetAdminIdKnownNotNull();
@@ -73,7 +80,7 @@ public IActionResult StartImport(ImportCompetenciesFormData model, int framework
}
catch (InvalidHeadersException)
{
- return View("Developer/Import/ImportFailed");
+ return RedirectToAction("ImportFailed", "Frameworks", new { frameworkId, tabname, isNotBlank });
}
}
[Route("/Framework/{frameworkId}/{tabname}/Import/Uploaded")]
@@ -97,9 +104,18 @@ public IActionResult ImportCompleted()
catch (InvalidHeadersException)
{
FileHelper.DeleteFile(webHostEnvironment, data.CompetenciesFileName);
- return View("Developer/Import/ImportFailed");
+ return RedirectToAction("ImportFailed", "Frameworks", new { data.FrameworkId, tabname = "Structure", data.IsNotBlank });
}
}
+ [Route("/Framework/{frameworkId}/{tabname}/Import/Failed")]
+ public IActionResult ImportFailed(int frameworkId, string tabname, bool isNotBlank)
+ {
+ var adminId = GetAdminId();
+ var framework = frameworkService.GetFrameworkDetailByFrameworkId(frameworkId, adminId);
+ var viewModel = new ImportCompetenciesViewModel(framework, isNotBlank);
+ return View("Developer/Import/ImportFailed", viewModel);
+ }
+
[Route("/Framework/{frameworkId}/{tabname}/Import/Ordering")]
public IActionResult ApplyCompetencyOrdering()
{
@@ -165,15 +181,16 @@ public IActionResult AddAssessmentQuestions()
public IActionResult AddAssessmentQuestions(AddAssessmentQuestionsFormData model)
{
var data = GetBulkUploadData();
- data.AddDefaultAssessmentQuestions = model.AddDefaultAssessmentQuestions;
+
if (model.AddDefaultAssessmentQuestions)
{
- data.DefaultQuestionIDs = model.DefaultAssessmentQuestionIDs;
+ data.DefaultQuestionIDs = model.DefaultAssessmentQuestionIDs ?? [];
}
else
{
data.DefaultQuestionIDs = [];
}
+ data.AddDefaultAssessmentQuestions = (data.DefaultQuestionIDs.Count > 0 && model.AddDefaultAssessmentQuestions);
data.AddCustomAssessmentQuestion = model.AddCustomAssessmentQuestion;
if (model.AddCustomAssessmentQuestion)
{
@@ -183,9 +200,8 @@ public IActionResult AddAssessmentQuestions(AddAssessmentQuestionsFormData model
{
data.CustomAssessmentQuestionID = null;
}
- if (data.CompetenciesToUpdateCount > 0)
+ if (data.CompetenciesToUpdateCount > 0 && (data.DefaultQuestionIDs.Count + (data.CustomAssessmentQuestionID != null ? 1 : 0) > 0))
{
- data.AddAssessmentQuestionsOption = 2;
setBulkUploadData(data);
return RedirectToAction("AddQuestionsToWhichCompetencies", "Frameworks", new { frameworkId = data.FrameworkId, tabname = data.TabName });
}
@@ -230,6 +246,7 @@ public IActionResult AddQuestionsToWhichCompetencies(int AddAssessmentQuestionsO
[Route("/Framework/{frameworkId}/{tabname}/Import/Summary")]
public IActionResult ImportSummary()
{
+ if (!TempData.Any()) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 });
var data = GetBulkUploadData();
var model = new ImportSummaryViewModel(data);
return View("Developer/Import/ImportSummary", model);
@@ -245,11 +262,6 @@ public IActionResult ImportSummarySubmit()
var workbook = new XLWorkbook(filePath);
var results = importCompetenciesFromFileService.ProcessCompetenciesFromFile(workbook, adminId, data.FrameworkId, data.FrameworkVocubulary, data.ReorderCompetenciesOption, data.AddAssessmentQuestionsOption, data.AddCustomAssessmentQuestion ? (int)data.CustomAssessmentQuestionID : 0, data.AddDefaultAssessmentQuestions ? data.DefaultQuestionIDs : []);
data.ImportCompetenciesResult = results;
- //TO DO apply ordering changes if required:
- if (data.ReorderCompetenciesOption == 2 && data.CompetenciesToReorderCount > 0)
- {
-
- }
setBulkUploadData(data);
return RedirectToAction("UploadResults", "Frameworks", new { frameworkId = data.FrameworkId, tabname = data.TabName });
}
@@ -263,12 +275,25 @@ public IActionResult UploadResults()
return View("Developer/Import/UploadResults", model);
}
[Route("CancelImport")]
- public IActionResult CancelImport()
+ public IActionResult CancelImport(int? frameworkId)
{
- var data = GetBulkUploadData();
- var frameworkId = data.FrameworkId;
- FileHelper.DeleteFile(webHostEnvironment, data.CompetenciesFileName);
- TempData.Clear();
+ try
+ {
+ var data = GetBulkUploadData();
+ frameworkId = data.FrameworkId;
+ if (!string.IsNullOrWhiteSpace(data.CompetenciesFileName))
+ {
+ FileHelper.DeleteFile(webHostEnvironment, data.CompetenciesFileName);
+ }
+ }
+ catch
+ {
+
+ }
+ finally
+ {
+ TempData.Clear();
+ }
return RedirectToAction("ViewFramework", new { frameworkId, tabname = "Structure" });
}
private void setupBulkUploadData(int frameworkId, int adminUserID, string competenciessFileName, string tabName, bool isNotBlank)
diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Review.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Review.cs
index 16c2665150..8ecf417de2 100644
--- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Review.cs
+++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Review.cs
@@ -63,6 +63,7 @@ public IActionResult LoadReview(int frameworkId, int reviewId)
{
var adminId = GetAdminId();
var framework = frameworkService.GetBaseFrameworkByFrameworkId(frameworkId, adminId);
+ if (framework.FrameworkReviewID == 0 || framework.FrameworkReviewID == null) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 });
if (framework == null) return StatusCode(404);
if (framework.UserRole < 1) return StatusCode(403);
var frameworkName = framework.FrameworkName;
@@ -81,8 +82,27 @@ public IActionResult LoadReview(int frameworkId, int reviewId)
public IActionResult SubmitFrameworkReview(int frameworkId, int reviewId, string? comment, bool signedOff)
{
var adminId = GetAdminId();
+ var framework = frameworkService.GetBaseFrameworkByFrameworkId(frameworkId, adminId);
+ if (framework.FrameworkReviewID == 0 || framework.FrameworkReviewID == null) return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 });
int? commentId = null;
- if (!string.IsNullOrWhiteSpace(comment)) commentId = frameworkService.InsertComment(frameworkId, adminId, comment, null);
+ if (string.IsNullOrWhiteSpace(comment))
+ {
+ ModelState.AddModelError("comment", "Please enter comment");
+ var frameworkReview = frameworkService.GetFrameworkReview(frameworkId, adminId, reviewId);
+ frameworkReview.SignedOff = signedOff;
+ var model = new SubmitReviewViewModel()
+ {
+ FrameworkId = frameworkId,
+ FrameworkName = framework.FrameworkName,
+ FrameworkReview = frameworkReview
+ };
+ return View("Developer/SubmitReview", model);
+ }
+ else
+ {
+ commentId = frameworkService.InsertComment(frameworkId, adminId, comment, null);
+ }
+
frameworkService.SubmitFrameworkReview(frameworkId, reviewId, signedOff, commentId);
frameworkNotificationService.SendReviewOutcomeNotification(reviewId, User.GetCentreIdKnownNotNull());
return RedirectToAction("ViewFramework", "Frameworks", new { frameworkId, tabname = "Structure" });
diff --git a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Signposting.cs b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Signposting.cs
index 9acf3bd6c9..d994dee7a9 100644
--- a/DigitalLearningSolutions.Web/Controllers/FrameworksController/Signposting.cs
+++ b/DigitalLearningSolutions.Web/Controllers/FrameworksController/Signposting.cs
@@ -107,6 +107,13 @@ public IActionResult ConfirmAddCompetencyLearningResourceSummary(CompetencyResou
{
var frameworkCompetency = frameworkService.GetFrameworkCompetencyById(model.FrameworkCompetencyId.Value);
string plainTextDescription = DisplayStringHelper.RemoveMarkup(model.Description);
+ var competencyLearningResource = competencyLearningResourcesService.GetActiveCompetencyLearningResourcesByCompetencyIdAndReferenceId(frameworkCompetency.CompetencyID, model.ReferenceId);
+ if (competencyLearningResource.Any())
+ {
+ ModelState.Clear();
+ ModelState.AddModelError("LearningResourceExists", "This learning resource is already signposted to the selected competency.");
+ return View("Developer/AddCompetencyLearningResourceSummary", model);
+ }
int competencyLearningResourceId = competencyLearningResourcesService.AddCompetencyLearningResource(model.ReferenceId, model.ResourceName, plainTextDescription, model.ResourceType, model.Link, model.SelectedCatalogue, model.Rating.Value, frameworkCompetency.CompetencyID, GetAdminId());
return RedirectToAction("StartSignpostingParametersSession", "Frameworks", new { model.FrameworkId, model.FrameworkCompetencyId, model.FrameworkCompetencyGroupId, competencyLearningResourceId });
}
diff --git a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Available.cs b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Available.cs
index 6e8a5a3d90..b9e16e7ac6 100644
--- a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Available.cs
+++ b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/Available.cs
@@ -4,8 +4,10 @@
using DigitalLearningSolutions.Data.Models.SearchSortFilterPaginate;
using DigitalLearningSolutions.Web.Attributes;
using DigitalLearningSolutions.Web.Helpers;
+ using DigitalLearningSolutions.Web.Services;
using DigitalLearningSolutions.Web.ViewModels.LearningPortal.Available;
using Microsoft.AspNetCore.Mvc;
+ using System;
using System.Linq;
public partial class LearningPortalController
@@ -58,8 +60,41 @@ public IActionResult AllAvailableItems()
public IActionResult EnrolOnSelfAssessment(int selfAssessmentId)
{
+ var selfAssessment = selfAssessmentService.GetSelfAssessmentRetirementDateById(selfAssessmentId);
+ if(CheckRetirementDate(selfAssessment.RetirementDate)) return RedirectToAction("ConfirmRetirement", new { selfAssessmentId });
courseService.EnrolOnSelfAssessment(selfAssessmentId, User.GetUserIdKnownNotNull(), User.GetCentreIdKnownNotNull());
return RedirectToAction("SelfAssessment", new { selfAssessmentId });
}
+
+ [Route("/LearningPortal/Retirement/{selfAssessmentId:int}/confirm")]
+ public IActionResult ConfirmRetirement(int selfAssessmentId)
+ {
+ var selfAssessment = selfAssessmentService.GetSelfAssessmentRetirementDateById(selfAssessmentId);
+ var model = new RetirementViewModel(selfAssessmentId, selfAssessment.RetirementDate, selfAssessment.Name);
+ return View("Available/ConfirmRetirement", model);
+ }
+ [HttpPost]
+ [Route("/LearningPortal/Retirement/{selfAssessmentId:int}/confirm")]
+ public IActionResult ConfirmRetirement(RetirementViewModel retirementViewModel)
+ {
+ if (!ModelState.IsValid && !retirementViewModel.ActionConfirmed)
+ {
+ var selfAssessment = selfAssessmentService.GetSelfAssessmentRetirementDateById(retirementViewModel.SelfAssessmentId);
+ var model = new RetirementViewModel(retirementViewModel.SelfAssessmentId , selfAssessment.RetirementDate, selfAssessment.Name);
+ return View("Available/ConfirmRetirement", model);
+ }
+ var date = selfAssessmentService.GetSelfAssessmentRetirementDateById(retirementViewModel.SelfAssessmentId);
+ courseService.EnrolOnSelfAssessment(retirementViewModel.SelfAssessmentId, User.GetUserIdKnownNotNull(), User.GetCentreIdKnownNotNull());
+ return RedirectToAction("SelfAssessment", new { retirementViewModel.SelfAssessmentId });
+ }
+ private bool CheckRetirementDate(DateTime? date)
+ {
+ if (date == null)
+ return false;
+
+ DateTime twoWeeksbeforeRetirementdate = DateTime.Today.AddDays(14);
+ DateTime today = DateTime.Today;
+ return (date >= today && date <= twoWeeksbeforeRetirementdate);
+ }
}
}
diff --git a/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs b/DigitalLearningSolutions.Web/Controllers/LearningPortalController/SelfAssessment.cs
index 4947c6bd75..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,7 +1597,30 @@ ManageOptionalCompetenciesViewModel model
}
- if (!selfAssessmentService.HasMinimumOptionalCompetencies(selfAssessmentId, delegateUserId))
+ 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)
+ .Where(quest => quest.Required)
+ .All(quest => !((quest.Result == null || quest.Verified == null || quest.SignedOff != true) && quest.Required)) != true;
+
+ if (!selfAssessmentService.HasMinimumOptionalCompetencies(selfAssessmentId, delegateUserId) || isVerificationPending)
{
var supervisorsSignOffs = selfAssessmentService.GetSupervisorSignOffsForCandidateAssessment(selfAssessmentId, delegateUserId);
diff --git a/DigitalLearningSolutions.Web/Controllers/LoginController.cs b/DigitalLearningSolutions.Web/Controllers/LoginController.cs
index 20280e18a8..80984a593b 100644
--- a/DigitalLearningSolutions.Web/Controllers/LoginController.cs
+++ b/DigitalLearningSolutions.Web/Controllers/LoginController.cs
@@ -82,6 +82,14 @@ public async Task Index(LoginViewModel model, string timeZone = "
var loginResult = loginService.AttemptLogin(model.Username!.Trim(), model.Password!);
+ if (loginResult.LoginAttemptResult == LoginAttemptResult.LogIntoSingleCentre ||
+ loginResult.LoginAttemptResult == LoginAttemptResult.ChooseACentre ||
+ loginResult.LoginAttemptResult == LoginAttemptResult.UnverifiedEmail)
+ {
+ loginService.UpdateLastAccessedForUsersTable(loginResult.UserEntity.UserAccount.Id);
+ }
+
+
switch (loginResult.LoginAttemptResult)
{
case LoginAttemptResult.InvalidCredentials:
@@ -219,11 +227,17 @@ int centreIdToLogInto
IsPersistent = rememberMe,
IssuedUtc = clockUtility.UtcNow,
};
+ var centreAccountSet = userEntity?.GetCentreAccountSet(centreIdToLogInto);
- var adminAccount = userEntity!.GetCentreAccountSet(centreIdToLogInto)?.AdminAccount;
+ if (centreAccountSet?.DelegateAccount?.Id != null)
+ {
+ loginService.UpdateLastAccessedForDelegatesAccountsTable(centreAccountSet.DelegateAccount.Id);
+ }
+ var adminAccount = centreAccountSet?.AdminAccount;
if (adminAccount?.Active == true)
{
+ loginService.UpdateLastAccessedForAdminAccountsTable(adminAccount.Id);
sessionService.StartAdminSession(adminAccount.Id);
}
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 21b555391c..66e3c32a3c 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);
}
@@ -387,7 +391,7 @@ public IActionResult ReviewDelegateSelfAssessment(int supervisorDelegateId, int
foreach (var competency in competencies)
{
competency.CompetencyFlags = flags.Where(f => f.CompetencyId == competency.Id);
- };
+ }
if (superviseDelegate.DelegateUserID != null)
{
@@ -781,12 +785,22 @@ public IActionResult EnrolSetCompetencyAssessment(int supervisorDelegateId, int
return View("EnrolDelegateOnProfileAssessment", model);
}
+ if (sessionEnrolOnCompetencyAssessment.SelfAssessmentID != selfAssessmentID)
+ sessionEnrolOnCompetencyAssessment = new SessionEnrolOnCompetencyAssessment();
+
sessionEnrolOnCompetencyAssessment.SelfAssessmentID = selfAssessmentID;
multiPageFormService.SetMultiPageFormData(
sessionEnrolOnCompetencyAssessment,
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
);
+
+ var retirementDate = selfAssessmentService.GetSelfAssessmentById(selfAssessmentID).RetirementDate;
+ if (CheckRetirementDate(retirementDate))
+ {
+ return RedirectToAction("ConfirmRetiringSelfAssessment", "Supervisor", new { supervisorDelegateId });
+ }
+
return RedirectToAction(
"EnrolDelegateCompleteBy",
"Supervisor",
@@ -794,6 +808,60 @@ public IActionResult EnrolSetCompetencyAssessment(int supervisorDelegateId, int
);
}
+ [Route("/Supervisor/Staff/{supervisorDelegateId}/ProfileAssessment/Enrol/Confirm")]
+ public IActionResult ConfirmRetiringSelfAssessment(int supervisorDelegateId)
+ {
+ var sessionEnrolOnRoleProfile = multiPageFormService.GetMultiPageFormData(
+ MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
+ TempData
+ ).GetAwaiter().GetResult();
+
+ var retirementDate = selfAssessmentService.GetSelfAssessmentById((int)sessionEnrolOnRoleProfile.SelfAssessmentID).RetirementDate;
+ if (!CheckRetirementDate((retirementDate)))
+ {
+ return RedirectToAction("StatusCode", "LearningSolutions", new { code = 410 });
+ }
+ var model = new RetiringSelfAssessmentViewModel()
+ {
+ SelfAssessmentID = (int)sessionEnrolOnRoleProfile.SelfAssessmentID,
+ SupervisorDelegateID = supervisorDelegateId,
+ RetirementDate = retirementDate,
+ ActionConfirmed = sessionEnrolOnRoleProfile.ActionConfirmed
+ };
+ return View("ConfirmRetiringSelfAssessment", model);
+ }
+
+ [HttpPost]
+ [Route("/Supervisor/Staff/{supervisorDelegateId}/ProfileAssessment/Enrol/Confirm")]
+ public IActionResult ConfirmRetiringSelfAssessment(RetiringSelfAssessmentViewModel retiringSelfAssessment)
+ {
+ var sessionEnrolOnRoleProfile = multiPageFormService.GetMultiPageFormData(
+ MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
+ TempData
+ ).GetAwaiter().GetResult();
+
+ sessionEnrolOnRoleProfile.SelfAssessmentID = retiringSelfAssessment.SelfAssessmentID;
+ sessionEnrolOnRoleProfile.ActionConfirmed = retiringSelfAssessment.ActionConfirmed;
+ multiPageFormService.SetMultiPageFormData(
+ sessionEnrolOnRoleProfile,
+ MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
+ TempData
+ );
+
+ if (ModelState.IsValid && retiringSelfAssessment.ActionConfirmed)
+ {
+ return RedirectToAction(
+ "EnrolDelegateCompleteBy",
+ "Supervisor",
+ new { supervisorDelegateId = retiringSelfAssessment.SupervisorDelegateID }
+ );
+ }
+ else
+ {
+ return View("ConfirmRetiringSelfAssessment", retiringSelfAssessment);
+ }
+ }
+
[Route("/Supervisor/Staff/{supervisorDelegateId}/ProfileAssessment/Enrol/CompleteBy")]
[ResponseCache(CacheProfileName = "Never")]
[TypeFilter(
@@ -811,6 +879,12 @@ public IActionResult EnrolDelegateCompleteBy(int supervisorDelegateId, int? day,
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
);
+
+ var retirementDate = selfAssessmentService.GetSelfAssessmentById((int)sessionEnrolOnCompetencyAssessment.SelfAssessmentID).RetirementDate;
+ if (CheckRetirementDate(retirementDate) && !sessionEnrolOnCompetencyAssessment.ActionConfirmed)
+ {
+ return RedirectToAction("ConfirmRetiringSelfAssessment", "Supervisor", new { supervisorDelegateId });
+ }
var supervisorDelegate =
supervisorService.GetSupervisorDelegateDetailsById(supervisorDelegateId, GetAdminId(), 0);
var competencyAssessment = supervisorService.GetCompetencyAssessmentById((int)sessionEnrolOnCompetencyAssessment.SelfAssessmentID);
@@ -818,7 +892,8 @@ public IActionResult EnrolDelegateCompleteBy(int supervisorDelegateId, int? day,
{
SupervisorDelegateDetail = supervisorDelegate,
CompetencyAssessment = competencyAssessment,
- CompleteByDate = sessionEnrolOnCompetencyAssessment.CompleteByDate
+ CompleteByDate = sessionEnrolOnCompetencyAssessment.CompleteByDate,
+ ActionConfirmed = sessionEnrolOnCompetencyAssessment.ActionConfirmed
};
if (day != null && month != null && year != null)
{
@@ -853,6 +928,12 @@ public IActionResult EnrolDelegateSetCompleteBy(int supervisorDelegateId, int da
TempData
);
}
+
+ sessionEnrolOnCompetencyAssessment.CompleteByDate = new DateTime(year, month, day);
+ }
+ else
+ {
+ sessionEnrolOnCompetencyAssessment.CompleteByDate = null;
}
var supervisorRoles =
@@ -964,6 +1045,13 @@ public IActionResult EnrolDelegateSummary(int supervisorDelegateId)
MultiPageFormDataFeature.EnrolDelegateOnProfileAssessment,
TempData
);
+
+ var retirementDate = selfAssessmentService.GetSelfAssessmentById((int)sessionEnrolOnCompetencyAssessment.SelfAssessmentID).RetirementDate;
+ if (CheckRetirementDate(retirementDate) && !sessionEnrolOnCompetencyAssessment.ActionConfirmed)
+ {
+ return RedirectToAction("ConfirmRetiringSelfAssessment", "Supervisor", new { supervisorDelegateId });
+ }
+
var supervisorDelegate =
supervisorService.GetSupervisorDelegateDetailsById(supervisorDelegateId, GetAdminId(), 0);
var competencyAssessment = supervisorService.GetCompetencyAssessmentById((int)sessionEnrolOnCompetencyAssessment.SelfAssessmentID);
@@ -991,6 +1079,7 @@ public IActionResult EnrolDelegateSummary(int supervisorDelegateId)
ViewBag.completeByMonth = TempData["completeByMonth"];
ViewBag.completeByYear = TempData["completeByYear"];
ViewBag.navigatedFrom = TempData["navigatedFrom"];
+ ViewBag.actionConfirmed = sessionEnrolOnCompetencyAssessment.ActionConfirmed;
return View("EnrolDelegateSummary", model);
}
@@ -1514,5 +1603,14 @@ private static string RenderRazorViewToString(Controller controller, string view
return sw.GetStringBuilder().ToString();
}
}
+ private bool CheckRetirementDate(DateTime? date)
+ {
+ if (date == null)
+ return false;
+
+ DateTime retirementOffsetDate = DateTime.Today.AddDays(14);
+ DateTime today = DateTime.Today;
+ return (date >= today && date <= retirementOffsetDate);
+ }
}
}
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/Support/RequestSupportTicketController.cs b/DigitalLearningSolutions.Web/Controllers/Support/RequestSupportTicketController.cs
index 3f58a7ec7f..a289b279aa 100644
--- a/DigitalLearningSolutions.Web/Controllers/Support/RequestSupportTicketController.cs
+++ b/DigitalLearningSolutions.Web/Controllers/Support/RequestSupportTicketController.cs
@@ -84,7 +84,7 @@ public IActionResult TypeofRequest(DlsSubApplication dlsSubApplication)
}
[HttpPost]
- [Route("/{dlsSubApplication}/RequestSupport/setRequestType")]
+ [Route("/{dlsSubApplication}/RequestSupport/TypeofRequest")]
public IActionResult setRequestType(DlsSubApplication dlsSubApplication, RequestTypeViewModel RequestTypemodel, int requestType)
{
var requestTypes = requestSupportTicketService.GetRequestTypes();
@@ -117,31 +117,36 @@ public IActionResult RequestSummary(DlsSubApplication dlsSubApplication, Request
).GetAwaiter().GetResult(); ;
var model = new RequestSummaryViewModel(data);
data.setRequestSubjectDetails(model);
+ if (!ModelState.IsValid)
+ {
+ ModelState.Clear();
+ }
return View("RequestSummary", model);
}
[HttpPost]
- [Route("/{dlsSubApplication}/RequestSupport/SetRequestSummary")]
+ [Route("/{dlsSubApplication}/RequestSupport/RequestSummary")]
public IActionResult SetRequestSummary(DlsSubApplication dlsSubApplication, RequestSummaryViewModel requestDetailsmodel)
{
- if (requestDetailsmodel.RequestSubject == null)
- {
- ModelState.AddModelError("RequestSubject", "Please enter request summary");
- return View("RequestSummary", requestDetailsmodel);
- }
- if (requestDetailsmodel.RequestDescription == null)
+ var data = multiPageFormService.GetMultiPageFormData(
+ MultiPageFormDataFeature.AddCustomWebForm("RequestSupportTicketCWF"),
+ TempData
+ ).GetAwaiter().GetResult();
+ requestDetailsmodel.RequestType = data.RequestType;
+
+ // Check if RequestDescription is null or contains any default empty tags ("