diff --git a/DigitalLearningSolutions.Data.Tests/DataServices/CentresDataServiceTests.cs b/DigitalLearningSolutions.Data.Tests/DataServices/CentresDataServiceTests.cs index e91138413c..68061c057c 100644 --- a/DigitalLearningSolutions.Data.Tests/DataServices/CentresDataServiceTests.cs +++ b/DigitalLearningSolutions.Data.Tests/DataServices/CentresDataServiceTests.cs @@ -227,6 +227,38 @@ public void UpdateCentreWebsiteDetails_updates_centre() } } + [Test] + public void UpdateCentreDetails_updates_centre() + { + using var transaction = new TransactionScope(); + try + { + // Given + const string notifyEmail = "test@centre.com"; + const string bannerText = "Test banner text"; + var signature = new byte[100]; + var logo = new byte[200]; + + + // When + centresDataService.UpdateCentreDetails(2, notifyEmail, bannerText, signature, logo); + var updatedCentre = centresDataService.GetCentreDetailsById(2)!; + + // Then + using (new AssertionScope()) + { + updatedCentre.NotifyEmail.Should().BeEquivalentTo(notifyEmail); + updatedCentre.BannerText.Should().BeEquivalentTo(bannerText); + updatedCentre.SignatureImage.Should().BeEquivalentTo(signature); + updatedCentre.CentreLogo.Should().BeEquivalentTo(logo); + } + } + finally + { + transaction.Dispose(); + } + } + [Test] public void GetCentreAutoRegisterValues_should_return_correct_values() { diff --git a/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs b/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs index 1e2a46c8a2..8a11b1db7d 100644 --- a/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs @@ -37,6 +37,14 @@ void UpdateCentreWebsiteDetails( string? otherInformation ); + void UpdateCentreDetails( + int centreId, + string? notifyEmail, + string bannerText, + byte[]? centreSignature, + byte[]? centreLogo + ); + (string firstName, string lastName, string email) GetCentreManagerDetails(int centreId); string[] GetCentreIpPrefixes(int centreId); (bool autoRegistered, string? autoRegisterManagerEmail) GetCentreAutoRegisterValues(int centreId); @@ -215,6 +223,32 @@ public void UpdateCentreWebsiteDetails( ); } + public void UpdateCentreDetails( + int centreId, + string? notifyEmail, + string bannerText, + byte[]? centreSignature, + byte[]? centreLogo + ) + { + connection.Execute( + @"UPDATE Centres SET + NotifyEmail = @notifyEmail, + BannerText = @bannerText, + SignatureImage = @centreSignature, + CentreLogo = @centreLogo + WHERE CentreId = @centreId", + new + { + notifyEmail, + bannerText, + centreSignature, + centreLogo, + centreId + } + ); + } + public (string firstName, string lastName, string email) GetCentreManagerDetails(int centreId) { var info = connection.QueryFirstOrDefault<(string, string, string)>( diff --git a/DigitalLearningSolutions.Data/Services/ImageResizeService.cs b/DigitalLearningSolutions.Data/Services/ImageResizeService.cs index 7df2d49595..ccb33204a4 100644 --- a/DigitalLearningSolutions.Data/Services/ImageResizeService.cs +++ b/DigitalLearningSolutions.Data/Services/ImageResizeService.cs @@ -6,10 +6,13 @@ using System.Drawing.Imaging; using System.IO; using Microsoft.AspNetCore.Http; + using Org.BouncyCastle.Crypto.Tls; public interface IImageResizeService { public byte[] ResizeProfilePicture(IFormFile formProfileImage); + + public byte[] ResizeCentreImage(IFormFile formCentreImage); } public class ImageResizeService : IImageResizeService @@ -22,6 +25,14 @@ public byte[] ResizeProfilePicture(IFormFile formProfileImage) return SquareImageFromMemoryStream(memoryStream, 300); } + public byte[] ResizeCentreImage(IFormFile formCentreImage) + { + using var memoryStream = new MemoryStream(); + formCentreImage.CopyTo(memoryStream); + + return ResizedImageFromMemoryStream(memoryStream, 500); + } + private byte[] SquareImageFromMemoryStream(MemoryStream memoryStream, int targetSideLengthPx) { using var image = Image.FromStream(memoryStream); @@ -35,6 +46,17 @@ private byte[] SquareImageFromMemoryStream(MemoryStream memoryStream, int target return result.ToArray(); } + private byte[] ResizedImageFromMemoryStream(MemoryStream memoryStream, int maxSideLengthPx) + { + using var image = Image.FromStream(memoryStream); + + using var resizedImage = ResizeImageByMaxSideLength(image, maxSideLengthPx); + + using var result = new MemoryStream(); + resizedImage.Save(result, ImageFormat.Jpeg); + return result.ToArray(); + } + private Image CropImageToCentredSquare(Image image) { var minSideLength = Math.Min(image.Height, image.Width); @@ -67,8 +89,25 @@ private Image CropImageToCentredSquare(Image image) private Image ResizeSquareImage(Image image, int sideLengthPx) { - var destRect = new Rectangle(0, 0, sideLengthPx, sideLengthPx); - var returnImage = new Bitmap(sideLengthPx, sideLengthPx); + return ResizeImageToDimensions(image, sideLengthPx, sideLengthPx); + } + + private Image ResizeImageByMaxSideLength(Image image, int maxSideLengthPx) + { + var longestSideLengthPx = Math.Max(image.Width, image.Height); + // No need to resize if image is smaller than the max size + var ratio = Math.Min((float)maxSideLengthPx / (float)longestSideLengthPx, 1); + + var newWidth = (int)(image.Width * ratio); + var newHeight = (int)(image.Height * ratio); + + return ResizeImageToDimensions(image, newWidth, newHeight); + } + + private Image ResizeImageToDimensions(Image image, int widthPx, int heightPx) + { + var destRect = new Rectangle(0, 0, widthPx, heightPx); + var returnImage = new Bitmap(widthPx, heightPx); returnImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); diff --git a/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs b/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs index 45af58c39c..dca5ef543f 100644 --- a/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs +++ b/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs @@ -31,6 +31,7 @@ public void Page_has_no_accessibility_errors(string url, string pageTitle) [InlineData("/TrackingSystem/Centre/Ranking", "Centre ranking")] [InlineData("/TrackingSystem/Centre/ContractDetails", "Contract details")] [InlineData("/TrackingSystem/CentreConfiguration", "Centre configuration")] + [InlineData("/TrackingSystem/CentreConfiguration/EditCentreDetails", "Edit centre details")] [InlineData("/TrackingSystem/CentreConfiguration/EditCentreManagerDetails", "Edit centre manager details")] [InlineData( "/TrackingSystem/CentreConfiguration/EditCentreWebsiteDetails", diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/MyAccount/MyAccountControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/MyAccount/MyAccountControllerTests.cs index 84ee34cae7..88dcc19996 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/MyAccount/MyAccountControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/MyAccount/MyAccountControllerTests.cs @@ -124,7 +124,7 @@ public void EditDetailsPostSave_for_admin_user_with_missing_delegate_answers_doe } [Test] - public void EditDetailsPostSave_with_profile_image_fails_validation() + public void EditDetailsPostSave_without_previewing_profile_image_fails_validation() { // Given var myAccountController = new MyAccountController( diff --git a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CentreConfiguration/CentreConfigurationControllerTests.cs b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CentreConfiguration/CentreConfigurationControllerTests.cs index ea1365266e..78c1c286bd 100644 --- a/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CentreConfiguration/CentreConfigurationControllerTests.cs +++ b/DigitalLearningSolutions.Web.Tests/Controllers/TrackingSystem/CentreConfiguration/CentreConfigurationControllerTests.cs @@ -3,12 +3,17 @@ using System.Globalization; using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Models.External.Maps; + using DigitalLearningSolutions.Data.Services; using DigitalLearningSolutions.Web.Controllers.TrackingSystem.CentreConfiguration; using DigitalLearningSolutions.Web.Helpers.ExternalApis; using DigitalLearningSolutions.Web.Tests.ControllerHelpers; using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.CentreConfiguration; using FakeItEasy; + using FluentAssertions; using FluentAssertions.AspNetCore.Mvc; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; using NUnit.Framework; @@ -16,15 +21,18 @@ public class CentreConfigurationControllerTests { private readonly ICentresDataService centresDataService = A.Fake(); private readonly IMapsApiHelper mapsApiHelper = A.Fake(); + private readonly ILogger logger = A.Fake>(); + + private readonly IImageResizeService imageResizeService = A.Fake(); private CentreConfigurationController controller = null!; [SetUp] public void Setup() { controller = - new CentreConfigurationController(centresDataService, mapsApiHelper, logger) + new CentreConfigurationController(centresDataService, mapsApiHelper, logger, imageResizeService) .WithDefaultContext() .WithMockUser(true); @@ -45,6 +53,15 @@ public void Setup() ).DoesNothing(); } + [TearDown] + public void Cleanup() + { + Fake.ClearRecordedCalls(centresDataService); + Fake.ClearRecordedCalls(mapsApiHelper); + Fake.ClearRecordedCalls(logger); + Fake.ClearRecordedCalls(imageResizeService); + } + [Test] public void EditCentreWebsiteDetails_should_show_validation_error_to_user_when_given_invalid_postcode() { @@ -81,6 +98,187 @@ public void EditCentreWebsiteDetails_should_redirect_to_error_page_when_API_issu result.Should().BeRedirectToActionResult().WithActionName("Error").WithControllerName("LearningSolutions"); } + [Test] + public void EditCentreDetailsPostSave_without_previewing_signature_image_fails_validation() + { + // Given + var model = new EditCentreDetailsViewModel + { + NotifyEmail = "email@test.com", + BannerText = "Banner text", + CentreSignatureFile = A.Fake() + }; + + // When + var result = controller.EditCentreDetails(model, "save"); + + // Then + A.CallTo( + () => centresDataService.UpdateCentreDetails( + A._, + A._, + A._, + A._, + A._ + ) + ) + .MustNotHaveHappened(); + result.As().Model.Should().BeEquivalentTo(model); + controller.ModelState[nameof(EditCentreDetailsViewModel.CentreSignatureFile)].ValidationState.Should() + .Be(ModelValidationState.Invalid); + } + + [Test] + public void EditCentreDetailsPostSave_without_previewing_logo_image_fails_validation() + { + // Given + var model = new EditCentreDetailsViewModel + { + NotifyEmail = "email@test.com", + BannerText = "Banner text", + CentreLogoFile = A.Fake() + }; + + // When + var result = controller.EditCentreDetails(model, "save"); + + // Then + A.CallTo( + () => centresDataService.UpdateCentreDetails( + A._, + A._, + A._, + A._, + A._ + ) + ) + .MustNotHaveHappened(); + result.As().Model.Should().BeEquivalentTo(model); + controller.ModelState[nameof(EditCentreDetailsViewModel.CentreLogoFile)].ValidationState.Should() + .Be(ModelValidationState.Invalid); + } + + [Test] + public void EditCentreDetailsPost_returns_error_with_unexpected_action() + { + // Given + const string action = "unexpectedString"; + var model = new EditCentreDetailsViewModel(); + + // When + var result = controller.EditCentreDetails(model, action); + + // Then + result.Should().BeStatusCodeResult().WithStatusCode(500); + } + + [Test] + public void EditCentreDetailsPost_updates_centre_and_redirects_with_successful_save() + { + // Given + const string action = "save"; + var model = new EditCentreDetailsViewModel + { + BannerText = "Test banner text" + }; + + // When + var result = controller.EditCentreDetails(model, action); + + // Then + result.Should().BeRedirectToActionResult().WithActionName("Index"); + A.CallTo(() => centresDataService.UpdateCentreDetails(2, null, model.BannerText, null, null)) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public void EditCentreDetailsPost_previewSignature_calls_imageResizeService() + { + // Given + const string action = "previewSignature"; + var model = new EditCentreDetailsViewModel + { + BannerText = "Test banner text", + CentreSignature = new byte[100], + CentreSignatureFile = A.Fake() + }; + var newImage = new byte [200]; + A.CallTo(() => imageResizeService.ResizeCentreImage(A._)).Returns(newImage); + + // When + var result = controller.EditCentreDetails(model, action); + + // Then + result.Should().BeViewResult(); + A.CallTo(() => imageResizeService.ResizeCentreImage(A._)).MustHaveHappenedOnceExactly(); + var returnModel = (result as ViewResult)!.Model as EditCentreDetailsViewModel; + returnModel!.CentreSignature.Should().BeEquivalentTo(newImage); + } + + [Test] + public void EditCentreDetailsPost_previewLogo_calls_imageResizeService() + { + // Given + const string action = "previewLogo"; + var model = new EditCentreDetailsViewModel + { + BannerText = "Test banner text", + CentreLogo = new byte[100], + CentreLogoFile = A.Fake() + }; + var newImage = new byte [200]; + A.CallTo(() => imageResizeService.ResizeCentreImage(A._)).Returns(newImage); + + // When + var result = controller.EditCentreDetails(model, action); + + // Then + result.Should().BeViewResult(); + A.CallTo(() => imageResizeService.ResizeCentreImage(A._)).MustHaveHappenedOnceExactly(); + var returnModel = (result as ViewResult)!.Model as EditCentreDetailsViewModel; + returnModel!.CentreLogo.Should().BeEquivalentTo(newImage); + } + + [Test] + public void EditCentreDetailsPost_removeSignature_removes_signature() + { + // Given + const string action = "removeSignature"; + var model = new EditCentreDetailsViewModel + { + BannerText = "Test banner text", + CentreSignature = new byte[100] + }; + + // When + var result = controller.EditCentreDetails(model, action); + + // Then + result.Should().BeViewResult(); + var returnModel = (result as ViewResult)!.Model as EditCentreDetailsViewModel; + returnModel!.CentreSignature.Should().BeNull(); + } + + [Test] + public void EditCentreDetailsPost_removeLogo_removes_logo() + { + // Given + const string action = "removeLogo"; + var model = new EditCentreDetailsViewModel + { + BannerText = "Test banner text", + CentreLogo = new byte[100] + }; + + // When + var result = controller.EditCentreDetails(model, action); + + // Then + result.Should().BeViewResult(); + var returnModel = (result as ViewResult)!.Model as EditCentreDetailsViewModel; + returnModel!.CentreLogo.Should().BeNull(); + } + [Test] public void EditCentreWebsiteDetails_should_show_save_coordinates_when_postcode_is_valid() { diff --git a/DigitalLearningSolutions.Web/Controllers/MyAccountController.cs b/DigitalLearningSolutions.Web/Controllers/MyAccountController.cs index 2102941367..9982ce950c 100644 --- a/DigitalLearningSolutions.Web/Controllers/MyAccountController.cs +++ b/DigitalLearningSolutions.Web/Controllers/MyAccountController.cs @@ -5,6 +5,7 @@ using DigitalLearningSolutions.Data.DataServices; using DigitalLearningSolutions.Data.Models.User; using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.ViewModels.Common; using DigitalLearningSolutions.Web.ViewModels.MyAccount; @@ -131,11 +132,7 @@ private IActionResult EditDetailsPostSave(EditDetailsViewModel model) private IActionResult EditDetailsPostPreviewImage(EditDetailsViewModel model) { // We don't want to display validation errors on other fields in this case - foreach (var key in ModelState.Keys.Where(k => k != nameof(EditDetailsViewModel.ProfileImageFile))) - { - ModelState[key].Errors.Clear(); - ModelState[key].ValidationState = ModelValidationState.Valid; - } + ModelState.ClearErrorsForAllFieldsExcept(nameof(EditDetailsViewModel.ProfileImageFile)); if (!ModelState.IsValid) { @@ -154,11 +151,7 @@ private IActionResult EditDetailsPostPreviewImage(EditDetailsViewModel model) private IActionResult EditDetailsPostRemoveImage(EditDetailsViewModel model) { // We don't want to display validation errors on other fields in this case - foreach (var key in ModelState.Keys) - { - ModelState[key].Errors.Clear(); - ModelState[key].ValidationState = ModelValidationState.Valid; - } + ModelState.ClearAllErrors(); ModelState.Remove(nameof(EditDetailsViewModel.ProfileImage)); model.ProfileImage = null; diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CentreConfiguration/CentreConfigurationController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CentreConfiguration/CentreConfigurationController.cs index 9ea3988d29..7f1538fd93 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CentreConfiguration/CentreConfigurationController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CentreConfiguration/CentreConfigurationController.cs @@ -1,6 +1,8 @@ namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.CentreConfiguration { using DigitalLearningSolutions.Data.DataServices; + using DigitalLearningSolutions.Data.Services; + using DigitalLearningSolutions.Web.Extensions; using DigitalLearningSolutions.Web.Helpers; using DigitalLearningSolutions.Web.Helpers.ExternalApis; using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.CentreConfiguration; @@ -17,16 +19,19 @@ public class CentreConfigurationController : Controller private readonly ICentresDataService centresDataService; private readonly ILogger logger; private readonly IMapsApiHelper mapsApiHelper; + private readonly IImageResizeService imageResizeService; public CentreConfigurationController( ICentresDataService centresDataService, IMapsApiHelper mapsApiHelper, - ILogger logger + ILogger logger, + IImageResizeService imageResizeService ) { this.centresDataService = centresDataService; this.mapsApiHelper = mapsApiHelper; this.logger = logger; + this.imageResizeService = imageResizeService; } public IActionResult Index() @@ -132,5 +137,119 @@ public IActionResult EditCentreWebsiteDetails(EditCentreWebsiteDetailsViewModel return RedirectToAction("Index"); } + + [HttpGet] + [Route("EditCentreDetails")] + public IActionResult EditCentreDetails() + { + var centreId = User.GetCentreId(); + + var centreDetails = centresDataService.GetCentreDetailsById(centreId)!; + + var model = new EditCentreDetailsViewModel(centreDetails); + + return View(model); + } + + [HttpPost] + [Route("EditCentreDetails")] + public IActionResult EditCentreDetails(EditCentreDetailsViewModel model, string action) + { + return action switch + { + "save" => EditCentreDetailsPostSave(model), + "previewSignature" => EditCentreDetailsPostPreviewSignature(model), + "removeSignature" => EditCentreDetailsPostRemoveSignature(model), + "previewLogo" => EditCentreDetailsPostPreviewLogo(model), + "removeLogo" => EditCentreDetailsPostRemoveLogo(model), + _ => new StatusCodeResult(500) + }; + } + + private IActionResult EditCentreDetailsPostSave(EditCentreDetailsViewModel model) + { + if (model.CentreSignatureFile != null) + { + ModelState.AddModelError(nameof(EditCentreDetailsViewModel.CentreSignatureFile), + "Preview your new centre signature before saving"); + } + + if (model.CentreLogoFile != null) + { + ModelState.AddModelError(nameof(EditCentreDetailsViewModel.CentreLogoFile), + "Preview your new centre logo before saving"); + } + + if (!ModelState.IsValid) + { + return View(model); + } + + var centreId = User.GetCentreId(); + + centresDataService.UpdateCentreDetails( + centreId, + model.NotifyEmail, + model.BannerText!, + model.CentreSignature, + model.CentreLogo + ); + + return RedirectToAction("Index"); + } + + private IActionResult EditCentreDetailsPostPreviewSignature(EditCentreDetailsViewModel model) + { + ModelState.ClearErrorsForAllFieldsExcept(nameof(EditCentreDetailsViewModel.CentreSignatureFile)); + + if (!ModelState.IsValid) + { + return View(model); + } + + if (model.CentreSignatureFile != null) + { + ModelState.Remove(nameof(EditCentreDetailsViewModel.CentreSignature)); + model.CentreSignature = imageResizeService.ResizeCentreImage(model.CentreSignatureFile); + } + + return View(model); + } + + private IActionResult EditCentreDetailsPostRemoveSignature(EditCentreDetailsViewModel model) + { + ModelState.ClearAllErrors(); + + ModelState.Remove(nameof(EditCentreDetailsViewModel.CentreSignature)); + model.CentreSignature = null; + return View(model); + } + + private IActionResult EditCentreDetailsPostPreviewLogo(EditCentreDetailsViewModel model) + { + ModelState.ClearErrorsForAllFieldsExcept(nameof(EditCentreDetailsViewModel.CentreLogoFile)); + + if (!ModelState.IsValid) + { + return View(model); + } + + if (model.CentreLogoFile != null) + { + ModelState.Remove(nameof(EditCentreDetailsViewModel.CentreLogo)); + model.CentreLogo = imageResizeService.ResizeCentreImage(model.CentreLogoFile); + } + + return View(model); + } + + private IActionResult EditCentreDetailsPostRemoveLogo(EditCentreDetailsViewModel model) + { + ModelState.ClearAllErrors(); + + ModelState.Remove(nameof(EditCentreDetailsViewModel.CentreLogo)); + model.CentreLogo = null; + return View(model); + } } } diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CentreConfiguration/RegistrationPromptsController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CentreConfiguration/RegistrationPromptsController.cs index 8be6579098..d7e5c3c727 100644 --- a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CentreConfiguration/RegistrationPromptsController.cs +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/CentreConfiguration/RegistrationPromptsController.cs @@ -318,12 +318,7 @@ public IActionResult RemoveRegistrationPrompt(int promptNumber, RemoveRegistrati private IActionResult EditRegistrationPromptPostSave(EditRegistrationPromptViewModel model) { - IgnoreAddNewAnswerValidation(); - - if (!ModelState.IsValid) - { - return View(model); - } + ModelState.ClearAllErrors(); customPromptsService.UpdateCustomPromptForCentre( User.GetCentreId(), @@ -370,7 +365,7 @@ private IActionResult RegistrationPromptAnswersPostRemovePrompt( bool saveToTempData = false ) { - IgnoreAddNewAnswerValidation(); + ModelState.ClearAllErrors(); var optionsString = NewlineSeparatedStringListHelper.RemoveStringFromNewlineSeparatedList(model.OptionsString!, index); @@ -387,12 +382,7 @@ private IActionResult RegistrationPromptAnswersPostRemovePrompt( private IActionResult AddRegistrationPromptConfigureAnswersPostNext(RegistrationPromptAnswersViewModel model) { - IgnoreAddNewAnswerValidation(); - - if (!ModelState.IsValid) - { - return View(model); - } + ModelState.ClearAllErrors(); UpdateTempDataWithAnswersModelValues(model); @@ -460,15 +450,6 @@ private void SetTotalAnswersLengthTooLongError(RegistrationPromptAnswersViewMode ); } - private void IgnoreAddNewAnswerValidation() - { - foreach (var key in ModelState.Keys) - { - ModelState[key].Errors.Clear(); - ModelState[key].ValidationState = ModelValidationState.Valid; - } - } - private static bool TryGetAnswerIndexFromDeleteAction(string action, out int index) { return int.TryParse(action.Remove(0, DeleteAction.Length), out index); diff --git a/DigitalLearningSolutions.Web/Extensions/ModelStateExtensions.cs b/DigitalLearningSolutions.Web/Extensions/ModelStateExtensions.cs index bf65f4d30c..0940eb9d55 100644 --- a/DigitalLearningSolutions.Web/Extensions/ModelStateExtensions.cs +++ b/DigitalLearningSolutions.Web/Extensions/ModelStateExtensions.cs @@ -1,15 +1,34 @@ -using System.Linq; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace DigitalLearningSolutions.Web.Extensions -{ - internal static class ModelStateDictionaryExtensions - { - internal static bool HasError(this ModelStateDictionary modelStateDictionary, string fieldName) - { - return !string.IsNullOrWhiteSpace(fieldName) - && modelStateDictionary.TryGetValue(fieldName, out var entry) - && (entry.Errors?.Any() ?? false); - } - } -} +using System.Linq; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace DigitalLearningSolutions.Web.Extensions +{ + using Microsoft.CodeAnalysis.CSharp.Syntax; + + internal static class ModelStateDictionaryExtensions + { + internal static bool HasError(this ModelStateDictionary modelStateDictionary, string fieldName) + { + return !string.IsNullOrWhiteSpace(fieldName) + && modelStateDictionary.TryGetValue(fieldName, out var entry) + && (entry.Errors?.Any() ?? false); + } + + internal static void ClearAllErrors(this ModelStateDictionary modelStateDictionary) + { + ClearErrorsForAllFieldsExcept(modelStateDictionary, null); + } + + internal static void ClearErrorsForAllFieldsExcept( + this ModelStateDictionary modelStateDictionary, + string? fieldName + ) + { + foreach (var key in modelStateDictionary.Keys.Where(k => k != fieldName)) + { + modelStateDictionary[key].Errors.Clear(); + modelStateDictionary[key].ValidationState = ModelValidationState.Valid; + } + } + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/CentreConfiguration/EditCentreDetailsViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/CentreConfiguration/EditCentreDetailsViewModel.cs new file mode 100644 index 0000000000..2d8b8dae1e --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/CentreConfiguration/EditCentreDetailsViewModel.cs @@ -0,0 +1,39 @@ +namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.CentreConfiguration +{ + using System.ComponentModel.DataAnnotations; + using DigitalLearningSolutions.Data.Models; + using DigitalLearningSolutions.Web.Attributes; + using Microsoft.AspNetCore.Http; + + public class EditCentreDetailsViewModel + { + public EditCentreDetailsViewModel() { } + + public EditCentreDetailsViewModel(Centre centre) + { + NotifyEmail = centre.NotifyEmail; + BannerText = centre.BannerText; + CentreSignature = centre.SignatureImage; + CentreLogo = centre.CentreLogo; + } + + [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")] + public string? NotifyEmail { get; set; } + + [Required(ErrorMessage = "Enter the centre support details")] + [MaxLength(250, ErrorMessage = "Centre support details must be 250 characters or fewer")] + public string? BannerText { get; set; } + + public byte[]? CentreSignature { get; set; } + + [AllowedExtensions(new []{".png",".tiff",".jpg",".jpeg",".bmp",".gif"})] + public IFormFile? CentreSignatureFile { get; set; } + + public byte[]? CentreLogo { get; set; } + + [AllowedExtensions(new []{".png",".tiff",".jpg",".jpeg",".bmp",".gif"})] + public IFormFile? CentreLogoFile { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/CentreConfiguration/EditCentreDetails.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/CentreConfiguration/EditCentreDetails.cshtml new file mode 100644 index 0000000000..e8aa0dc7f5 --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/CentreConfiguration/EditCentreDetails.cshtml @@ -0,0 +1,105 @@ +@inject IConfiguration Configuration +@using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.CentreConfiguration +@using Microsoft.Extensions.Configuration +@model EditCentreDetailsViewModel + + + +@{ + var errorHasOccurred = !ViewData.ModelState.IsValid; + ViewData["Title"] = errorHasOccurred ? "Error: Edit Centre Details" : "Edit Centre Details"; + ViewData["Application"] = "Tracking System"; + ViewData["HeaderPath"] = $"{Configuration["AppRootPath"]}/TrackingSystem/Centre/Dashboard"; + ViewData["HeaderPathName"] = "Tracking System"; + const string hintText = @"To change your {0}, select a new image and click the Preview button to preview it. + To remove your {0} click the remove button. + Changes will not be made until the Save button below is clicked."; +} + +@section NavMenuItems { + +} + +
+
+ @if (errorHasOccurred) { + + } + +

Edit centre details

+
+
+ +
+ +
+
+ + + +
+
+ +
+
+ + +
+
+ @if (Model.CentreSignature != null) { + Centre signature picture + } else { + Placeholder signature image + } +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ @if (Model.CentreLogo != null) { + Centre logo picture + } else { + Placeholder logo image + } +
+
+ +
+
+ + +
+
+ + +
+ + + +
+
diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/CentreConfiguration/_CentreConfigurationCentreDetails.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/CentreConfiguration/_CentreConfigurationCentreDetails.cshtml index 81e149b821..84f7b2232f 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/CentreConfiguration/_CentreConfigurationCentreDetails.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/CentreConfiguration/_CentreConfigurationCentreDetails.cshtml @@ -22,7 +22,7 @@
- Banner text + Centre support details
@Model.BannerText @@ -56,6 +56,10 @@
+ + Edit + + Preview certificate (opens in a new window)