diff --git a/DigitalLearningSolutions.Data.Tests/DataServices/UserDataServiceTests.cs b/DigitalLearningSolutions.Data.Tests/DataServices/UserDataServiceTests.cs index c08f4644c5..9933580bc0 100644 --- a/DigitalLearningSolutions.Data.Tests/DataServices/UserDataServiceTests.cs +++ b/DigitalLearningSolutions.Data.Tests/DataServices/UserDataServiceTests.cs @@ -6,6 +6,7 @@ using System.Transactions; using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Mappers; + using DigitalLearningSolutions.Data.Models.User; using DigitalLearningSolutions.Data.Tests.TestHelpers; using FluentAssertions; using FluentAssertions.Execution; @@ -55,6 +56,59 @@ public void GetDelegateUsersByUsername_Returns_delegate_user() returnedDelegateUsers.FirstOrDefault().Should().BeEquivalentTo(expectedDelegateUser); } + [Test] + public void GetAllDelegateUsersByUsername_Returns_delegate_user() + { + // Given + var expectedDelegateUser = UserTestHelper.GetDefaultDelegateUser(); + + //When + var returnedDelegateUsers = userDataService.GetAllDelegateUsersByUsername("SV1234"); + + // Then + returnedDelegateUsers.FirstOrDefault().Should().BeEquivalentTo(expectedDelegateUser); + } + + [Test] + public void GetAllDelegateUsersByUsername_includes_inactive_users() + { + //When + var returnedDelegateUsers = userDataService.GetAllDelegateUsersByUsername("OS35"); + + // Then + returnedDelegateUsers.FirstOrDefault()!.Id.Should().Be(89094); + } + + [Test] + public void GetAllDelegateUsersByUsername_search_includes_CandidateNumber() + { + //When + var returnedDelegateUsers = userDataService.GetAllDelegateUsersByUsername("ND107"); + + // Then + returnedDelegateUsers.FirstOrDefault()!.Id.Should().Be(78051); + } + + [Test] + public void GetAllDelegateUsersByUsername_search_includes_EmailAddress() + { + //When + var returnedDelegateUsers = userDataService.GetAllDelegateUsersByUsername("saudnhb@.5lpyk"); + + // Then + returnedDelegateUsers.FirstOrDefault()!.Id.Should().Be(78051); + } + + [Test] + public void GetAllDelegateUsersByUsername_searches_AliasID() + { + //When + var returnedDelegateUsers = userDataService.GetAllDelegateUsersByUsername("aldn y"); + + // Then + returnedDelegateUsers.FirstOrDefault()!.Id.Should().Be(78051); + } + [Test] public void GetAdminUserById_Returns_admin_user() { diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService.cs index 350d2c62b4..01f626ef28 100644 --- a/DigitalLearningSolutions.Data/DataServices/UserDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/UserDataService.cs @@ -15,6 +15,7 @@ public interface IUserDataService public List GetDelegateUserCardsByCentreId(int centreId); public AdminUser? GetAdminUserByUsername(string username); public List GetDelegateUsersByUsername(string username); + public List GetAllDelegateUsersByUsername(string username); public AdminUser? GetAdminUserByEmailAddress(string emailAddress); public List GetDelegateUsersByEmailAddress(string emailAddress); public List GetUnapprovedDelegateUsersByCentreId(int centreId); @@ -278,6 +279,29 @@ FROM Candidates AS cd return users; } + public List GetAllDelegateUsersByUsername(string username) + { + List users = connection.Query( + @"SELECT + cd.CandidateID AS Id, + cd.CandidateNumber, + cd.CentreID, + ct.CentreName, + ct.Active AS CentreActive, + cd.EmailAddress, + cd.FirstName, + cd.LastName, + cd.Password, + cd.Approved + FROM Candidates AS cd + INNER JOIN Centres AS ct ON ct.CentreID = cd.CentreID + WHERE cd.CandidateNumber = @username OR cd.EmailAddress = @username OR cd.AliasID = @username", + new { username } + ).ToList(); + + return users; + } + public AdminUser? GetAdminUserByEmailAddress(string emailAddress) { return connection.Query( diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/Register/RegisterDelegateByCentreControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/Register/RegisterDelegateByCentreControllerTests.cs new file mode 100644 index 0000000000..e1e7fcf4ad --- /dev/null +++ b/DigitalLearningSolutions.Web.Tests/Controllers/Register/RegisterDelegateByCentreControllerTests.cs @@ -0,0 +1,143 @@ +namespace DigitalLearningSolutions.Web.Tests.Controllers.Register +{ + using System.Collections.Generic; + using DigitalLearningSolutions.Data.DataServices; + using DigitalLearningSolutions.Data.Models.User; + using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Data.Tests.TestHelpers; + using DigitalLearningSolutions.Web.Controllers.Register; + using DigitalLearningSolutions.Web.Extensions; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.Models; + using DigitalLearningSolutions.Web.Tests.ControllerHelpers; + using DigitalLearningSolutions.Web.ViewModels.Register; + using FakeItEasy; + using FluentAssertions.AspNetCore.Mvc; + using NUnit.Framework; + + public class RegisterDelegateByCentreControllerTests + { + private RegisterDelegateByCentreController controller = null!; + private CustomPromptHelper customPromptHelper = null!; + private IJobGroupsDataService jobGroupsDataService = null!; + private IUserDataService userDataService = null!; + private IUserService userService = null!; + + [SetUp] + public void Setup() + { + jobGroupsDataService = A.Fake(); + userService = A.Fake(); + userDataService = A.Fake(); + customPromptHelper = A.Fake(); + controller = new RegisterDelegateByCentreController( + jobGroupsDataService, + userService, + customPromptHelper, + userDataService + ) + .WithDefaultContext() + .WithMockTempData(); + } + + [Test] + public void PersonalInformationPost_with_duplicate_email_for_centre_fails_validation() + { + // Given + var duplicateUser = UserTestHelper.GetDefaultDelegateUser(); + var model = new PersonalInformationViewModel + { + FirstName = "Test", + LastName = "User", + Centre = duplicateUser.CentreId, + Email = duplicateUser.EmailAddress, + Alias = "testUser" + }; + A.CallTo(() => userService.GetUsersByEmailAddress(duplicateUser.EmailAddress!)) + .Returns((null, new List { duplicateUser })); + + // When + var result = controller.PersonalInformation(model); + + // Then + A.CallTo(() => userService.GetUsersByEmailAddress(duplicateUser.EmailAddress!)).MustHaveHappened(); + result.Should().BeViewResult().WithDefaultViewName(); + } + + [Test] + public void PersonalInformationPost_with_duplicate_email_for_different_centre_is_allowed() + { + // Given + controller.TempData.Set(new DelegateRegistrationByCentreData()); + var duplicateUser = UserTestHelper.GetDefaultDelegateUser(); + var model = new PersonalInformationViewModel + { + FirstName = "Test", + LastName = "User", + Centre = duplicateUser.CentreId + 1, + Email = duplicateUser.EmailAddress, + Alias = "testUser" + }; + A.CallTo(() => userService.GetUsersByEmailAddress(duplicateUser.EmailAddress!)) + .Returns((null, new List { duplicateUser })); + + // When + var result = controller.PersonalInformation(model); + + // Then + A.CallTo(() => userService.GetUsersByEmailAddress(duplicateUser.EmailAddress!)).MustHaveHappened(); + result.Should().BeRedirectToActionResult().WithActionName("LearnerInformation"); + } + + [Test] + public void PersonalInformationPost_with_duplicate_alias_for_centre_fails_validation() + { + // Given + const string duplicateAlias = "alias1"; + var duplicateUser = UserTestHelper.GetDefaultDelegateUser(); + var model = new PersonalInformationViewModel + { + FirstName = "Test", + LastName = "User", + Centre = duplicateUser.CentreId, + Email = "unique@email", + Alias = duplicateAlias + }; + A.CallTo(() => userDataService.GetAllDelegateUsersByUsername(duplicateAlias)) + .Returns(new List { duplicateUser }); + + // When + var result = controller.PersonalInformation(model); + + // Then + A.CallTo(() => userDataService.GetAllDelegateUsersByUsername(duplicateAlias)).MustHaveHappened(); + result.Should().BeViewResult().WithDefaultViewName(); + } + + [Test] + public void PersonalInformationPost_with_duplicate_alias_for_different_centre_is_allowed() + { + // Given + const string duplicateAlias = "alias1"; + controller.TempData.Set(new DelegateRegistrationByCentreData()); + var duplicateUser = UserTestHelper.GetDefaultDelegateUser(); + var model = new PersonalInformationViewModel + { + FirstName = "Test", + LastName = "User", + Centre = duplicateUser.CentreId + 1, + Email = duplicateUser.EmailAddress, + Alias = duplicateAlias + }; + A.CallTo(() => userDataService.GetAllDelegateUsersByUsername(duplicateAlias)) + .Returns(new List { duplicateUser }); + + // When + var result = controller.PersonalInformation(model); + + // Then + A.CallTo(() => userDataService.GetAllDelegateUsersByUsername(duplicateAlias)).MustHaveHappened(); + result.Should().BeRedirectToActionResult().WithActionName("LearnerInformation"); + } + } +} diff --git a/DigitalLearningSolutions.Web/Controllers/Register/RegisterController.cs b/DigitalLearningSolutions.Web/Controllers/Register/RegisterController.cs index 5142ffbb04..38f19538a3 100644 --- a/DigitalLearningSolutions.Web/Controllers/Register/RegisterController.cs +++ b/DigitalLearningSolutions.Web/Controllers/Register/RegisterController.cs @@ -251,11 +251,7 @@ public IActionResult Confirmation() private void SetDelegateRegistrationData(int? centreId) { - var delegateRegistrationData = new DelegateRegistrationData - { - IsCentreSpecificRegistration = centreId.HasValue, - Centre = centreId - }; + var delegateRegistrationData = new DelegateRegistrationData(centreId); var id = delegateRegistrationData.Id; Response.Cookies.Append( diff --git a/DigitalLearningSolutions.Web/Controllers/Register/RegisterDelegateByCentreController.cs b/DigitalLearningSolutions.Web/Controllers/Register/RegisterDelegateByCentreController.cs new file mode 100644 index 0000000000..f89d325351 --- /dev/null +++ b/DigitalLearningSolutions.Web/Controllers/Register/RegisterDelegateByCentreController.cs @@ -0,0 +1,207 @@ +namespace DigitalLearningSolutions.Web.Controllers.Register +{ + using System; + using System.Collections.Generic; + using System.Linq; + using DigitalLearningSolutions.Data.DataServices; + using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Web.Extensions; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.Models; + using DigitalLearningSolutions.Web.ServiceFilter; + using DigitalLearningSolutions.Web.ViewModels.Common; + using DigitalLearningSolutions.Web.ViewModels.Register; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + + [Authorize(Policy = CustomPolicies.UserCentreAdmin)] + [Route("/TrackingSystem/Delegates/Register/{action}")] + public class RegisterDelegateByCentreController : Controller + { + private const string CookieName = "DelegateRegistrationByCentreData"; + private readonly CustomPromptHelper customPromptHelper; + private readonly IJobGroupsDataService jobGroupsDataService; + private readonly IUserDataService userDataService; + private readonly IUserService userService; + + public RegisterDelegateByCentreController( + IJobGroupsDataService jobGroupsDataService, + IUserService userService, + CustomPromptHelper customPromptHelper, + IUserDataService userDataService + ) + { + this.jobGroupsDataService = jobGroupsDataService; + this.userService = userService; + this.customPromptHelper = customPromptHelper; + this.userDataService = userDataService; + } + + [Route("/TrackingSystem/Delegates/Register")] + public IActionResult Index() + { + var centreId = User.GetCentreId(); + + SetCentreDelegateRegistrationData(centreId); + + return RedirectToAction("PersonalInformation"); + } + + [ServiceFilter(typeof(RedirectEmptySessionData))] + [HttpGet] + public IActionResult PersonalInformation() + { + var data = TempData.Peek()!; + + var model = new PersonalInformationViewModel(data); + + ValidatePersonalInformation(model); + + return View(model); + } + + [ServiceFilter(typeof(RedirectEmptySessionData))] + [HttpPost] + public IActionResult PersonalInformation(PersonalInformationViewModel model) + { + var data = TempData.Peek()!; + + ValidatePersonalInformation(model); + + if (!ModelState.IsValid) + { + return View(model); + } + + data.SetPersonalInformation(model); + TempData.Set(data); + + return RedirectToAction("LearnerInformation"); + } + + [ServiceFilter(typeof(RedirectEmptySessionData))] + [HttpGet] + public IActionResult LearnerInformation() + { + var data = TempData.Peek()!; + + var model = new LearnerInformationViewModel(data); + + PopulateLearnerInformationExtraFields(model, data); + + return View(model); + } + + [ServiceFilter(typeof(RedirectEmptySessionData))] + [HttpPost] + public IActionResult LearnerInformation(LearnerInformationViewModel model) + { + var data = TempData.Peek()!; + + var centreId = data.Centre!.Value; + + customPromptHelper.ValidateCustomPrompts( + centreId, + model.Answer1, + model.Answer2, + model.Answer3, + model.Answer4, + model.Answer5, + model.Answer6, + ModelState + ); + + if (!ModelState.IsValid) + { + PopulateLearnerInformationExtraFields(model, data); + return View(model); + } + + data.SetLearnerInformation(model); + TempData.Set(data); + + return new OkResult(); + } + + private void SetCentreDelegateRegistrationData(int centreId) + { + var centreDelegateRegistrationData = new DelegateRegistrationByCentreData(centreId); + var id = centreDelegateRegistrationData.Id; + + Response.Cookies.Append( + CookieName, + id.ToString(), + new CookieOptions + { + Expires = DateTimeOffset.UtcNow.AddDays(30) + } + ); + + TempData.Set(centreDelegateRegistrationData); + } + + private void ValidatePersonalInformation(PersonalInformationViewModel model) + { + if (model.Email == null) + { + return; + } + + var duplicateUsers = userService.GetUsersByEmailAddress(model.Email).delegateUsers + .Where(u => u.CentreId == model.Centre); + + if (duplicateUsers.Count() != 0) + { + ModelState.AddModelError( + nameof(PersonalInformationViewModel.Email), + "A user with this email address is already registered at this centre" + ); + } + + if (model.Alias == null) + { + return; + } + + duplicateUsers = userDataService.GetAllDelegateUsersByUsername(model.Alias) + .Where(u => u.CentreId == model.Centre); + + if (duplicateUsers.Count() != 0) + { + ModelState.AddModelError( + nameof(PersonalInformationViewModel.Alias), + "A user with this alias is already registered at this centre" + ); + } + } + + private IEnumerable GetEditCustomFieldsFromModel( + LearnerInformationViewModel model, + int centreId + ) + { + return customPromptHelper.GetEditCustomFieldViewModelsForCentre( + centreId, + model.Answer1, + model.Answer2, + model.Answer3, + model.Answer4, + model.Answer5, + model.Answer6 + ); + } + + private void PopulateLearnerInformationExtraFields( + LearnerInformationViewModel model, + RegistrationData data + ) + { + model.CustomFields = GetEditCustomFieldsFromModel(model, data.Centre!.Value); + model.JobGroupOptions = SelectListHelper.MapOptionsToSelectListItems( + jobGroupsDataService.GetJobGroupsAlphabetical(), + model.JobGroup + ); + } + } +} diff --git a/DigitalLearningSolutions.Web/Models/DelegateRegistrationByCentreData.cs b/DigitalLearningSolutions.Web/Models/DelegateRegistrationByCentreData.cs new file mode 100644 index 0000000000..d0e0841322 --- /dev/null +++ b/DigitalLearningSolutions.Web/Models/DelegateRegistrationByCentreData.cs @@ -0,0 +1,9 @@ +namespace DigitalLearningSolutions.Web.Models +{ + public class DelegateRegistrationByCentreData : DelegateRegistrationData + { + public DelegateRegistrationByCentreData() { } + public DelegateRegistrationByCentreData(int centreId) : base(centreId) { } + public string? Alias { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/Models/DelegateRegistrationData.cs b/DigitalLearningSolutions.Web/Models/DelegateRegistrationData.cs index 53bb3fa599..e98bf9e70f 100644 --- a/DigitalLearningSolutions.Web/Models/DelegateRegistrationData.cs +++ b/DigitalLearningSolutions.Web/Models/DelegateRegistrationData.cs @@ -4,6 +4,13 @@ public class DelegateRegistrationData : RegistrationData { + public DelegateRegistrationData() { } + + public DelegateRegistrationData(int? centreId) : base(centreId) + { + IsCentreSpecificRegistration = centreId.HasValue; + } + public bool IsCentreSpecificRegistration { get; set; } public string? Answer1 { get; set; } @@ -12,7 +19,7 @@ public class DelegateRegistrationData : RegistrationData public string? Answer4 { get; set; } public string? Answer5 { get; set; } public string? Answer6 { get; set; } - + public override void SetLearnerInformation(LearnerInformationViewModel model) { JobGroup = model.JobGroup; diff --git a/DigitalLearningSolutions.Web/Startup.cs b/DigitalLearningSolutions.Web/Startup.cs index 76ea9be90a..8731f348a7 100644 --- a/DigitalLearningSolutions.Web/Startup.cs +++ b/DigitalLearningSolutions.Web/Startup.cs @@ -186,6 +186,7 @@ private static void RegisterWebServiceFilters(IServiceCollection services) { services.AddScoped>(); services.AddScoped>(); + services.AddScoped>(); services.AddScoped>(); services.AddScoped>(); services.AddScoped>>(); diff --git a/DigitalLearningSolutions.Web/ViewModels/Register/PersonalInformationViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Register/PersonalInformationViewModel.cs index 0f4dd5f0b8..8fc2b82f10 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Register/PersonalInformationViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Register/PersonalInformationViewModel.cs @@ -22,16 +22,20 @@ public PersonalInformationViewModel(DelegateRegistrationData data) : this((Regis { IsCentreSpecificRegistration = data.IsCentreSpecificRegistration; } + public PersonalInformationViewModel(DelegateRegistrationByCentreData data) : this((DelegateRegistrationData)data) + { + Alias = data.Alias; + } - [Required(ErrorMessage = "Enter your first name")] + [Required(ErrorMessage = "Enter a first name")] [MaxLength(250, ErrorMessage = "First name must be 250 characters or fewer")] public string? FirstName { get; set; } - [Required(ErrorMessage = "Enter your last name")] + [Required(ErrorMessage = "Enter a last name")] [MaxLength(250, ErrorMessage = "Last name must be 250 characters or fewer")] public string? LastName { get; set; } - [Required(ErrorMessage = "Enter your email address")] + [Required(ErrorMessage = "Enter an email address")] [MaxLength(250, ErrorMessage = "Email address must be 250 characters or fewer")] [EmailAddress(ErrorMessage = "Enter an email address in the correct format, like name@example.com")] [NoWhitespace("Email address must not contain any whitespace characters")] @@ -40,6 +44,9 @@ public PersonalInformationViewModel(DelegateRegistrationData data) : this((Regis [Required(ErrorMessage = "Select a centre")] public int? Centre { get; set; } + [MaxLength(250, ErrorMessage = "Alias must be 250 characters or fewer")] + public string? Alias { get; set; } + public bool IsCentreSpecificRegistration { get; set; } public string? CentreName { get; set; } diff --git a/DigitalLearningSolutions.Web/Views/RegisterDelegateByCentre/LearnerInformation.cshtml b/DigitalLearningSolutions.Web/Views/RegisterDelegateByCentre/LearnerInformation.cshtml new file mode 100644 index 0000000000..2ed2e1e498 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/RegisterDelegateByCentre/LearnerInformation.cshtml @@ -0,0 +1,67 @@ +@inject IConfiguration Configuration +@using DigitalLearningSolutions.Web.ViewModels.Register +@using Microsoft.Extensions.Configuration +@model LearnerInformationViewModel + +@{ + var errorHasOccurred = !ViewData.ModelState.IsValid; + ViewData["Title"] = errorHasOccurred ? "Error: Register Delegate - Learner information" : "Register Delegate - Learner information"; + ViewData["Application"] = "Tracking System"; + ViewData["HeaderPath"] = $"{Configuration["AppRootPath"]}/TrackingSystem/Centre/Dashboard"; + ViewData["HeaderPathName"] = "Tracking System"; +} + +@section NavMenuItems { + +} + +
+
+ @if (errorHasOccurred) { + + } + +

Learner information

+ +
+ + + + @foreach (var customField in Model.CustomFields) { + if (customField.Options.Any()) { + + } else { + + } + } + + + + + +
+
diff --git a/DigitalLearningSolutions.Web/Views/RegisterDelegateByCentre/PersonalInformation.cshtml b/DigitalLearningSolutions.Web/Views/RegisterDelegateByCentre/PersonalInformation.cshtml new file mode 100644 index 0000000000..6bd762572e --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/RegisterDelegateByCentre/PersonalInformation.cshtml @@ -0,0 +1,74 @@ +@inject IConfiguration Configuration +@using DigitalLearningSolutions.Web.ViewModels.Register +@using Microsoft.Extensions.Configuration +@model PersonalInformationViewModel +@{ + var errorHasOccurred = !ViewData.ModelState.IsValid; + ViewData["Title"] = errorHasOccurred ? "Error: Register Delegate" : "Register Delegate"; + ViewData["Application"] = "Tracking System"; + ViewData["HeaderPath"] = $"{Configuration["AppRootPath"]}/TrackingSystem/Centre/Dashboard"; + ViewData["HeaderPathName"] = "Tracking System"; +} + +@section NavMenuItems { + +} + +
+
+
+ @if (errorHasOccurred) { + + } + +
+

Register

+ +

+ Please enter delegate's personal details to start the registration process. +

+ +
+ + + + + + + + + + + + + +
+
diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/AllDelegates/Index.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/AllDelegates/Index.cshtml index bd90a1dd30..927e3b254f 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/AllDelegates/Index.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Delegates/AllDelegates/Index.cshtml @@ -30,7 +30,7 @@

Delegates