diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index ec0ed2d5f..027e35695 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -21,7 +21,7 @@ jobs: - name: Use Node 12.19 with Yarn uses: actions/setup-node@v4 with: - node-version: '14' + node-version: '18' npm: '6.14.8' - name: Typescript install WebUI diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Configuration/WebSettings.cs b/AdminUI/LearningHub.Nhs.AdminUI/Configuration/WebSettings.cs index 508e85497..f8e3c1565 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Configuration/WebSettings.cs +++ b/AdminUI/LearningHub.Nhs.AdminUI/Configuration/WebSettings.cs @@ -77,6 +77,16 @@ public class WebSettings /// public string AzureFileStorageConnectionString { get; set; } + /// + /// Gets or sets the AzureSourceFileStorageConnectionString. + /// + public string AzureSourceArchiveStorageConnectionString { get; set; } + + /// + /// Gets or sets the AzurePurgedFileStorageConnectionString. + /// + public string AzureContentArchiveStorageConnectionString { get; set; } + /// /// Gets or sets the azure file storage resource share name. /// diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Controllers/ResourceController.cs b/AdminUI/LearningHub.Nhs.AdminUI/Controllers/ResourceController.cs index 31f53b2a4..2338e457c 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Controllers/ResourceController.cs +++ b/AdminUI/LearningHub.Nhs.AdminUI/Controllers/ResourceController.cs @@ -54,6 +54,11 @@ public class ResourceController : BaseController /// private IResourceService resourceService; + /// + /// Defines the _fileService. + /// + private IFileService fileService; + /// /// Initializes a new instance of the class. /// @@ -61,6 +66,7 @@ public class ResourceController : BaseController /// The config. /// The logger. /// The resourceService. + /// The fileService. /// /// The websettings. /// The featureManager. public ResourceController( @@ -68,6 +74,7 @@ public ResourceController( IOptions config, ILogger logger, IResourceService resourceService, + IFileService fileService, IOptions websettings, IFeatureManager featureManager) : base(hostingEnvironment) @@ -76,6 +83,7 @@ public ResourceController( this.websettings = websettings; this.config = config.Value; this.resourceService = resourceService; + this.fileService = fileService; this.featureManager = featureManager; } @@ -298,11 +306,17 @@ public async Task TransferResourceOwnership(int resourceId, strin [HttpPost] public async Task Unpublish(int resourceVersionId, string details) { + var associatedFile = await this.resourceService.GetResourceVersionExtendedViewModelAsync(resourceVersionId); var vr = await this.resourceService.UnpublishResourceVersionAsync(resourceVersionId, details); await this.resourceService.CreateResourceVersionEvent(resourceVersionId, Nhs.Models.Enums.ResourceVersionEventTypeEnum.UnpublishedByAdmin, "Unpublish using Admin UI", 0); if (vr.IsValid) { + if (associatedFile.ScormDetails != null || associatedFile.HtmlDetails != null) + { + _ = Task.Run(async () => { await this.fileService.PurgeResourceFile(associatedFile, null); }); + } + return this.Json(new { success = true, diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Interfaces/IFileService.cs b/AdminUI/LearningHub.Nhs.AdminUI/Interfaces/IFileService.cs index 3d8a80b92..ef7ccfcde 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Interfaces/IFileService.cs +++ b/AdminUI/LearningHub.Nhs.AdminUI/Interfaces/IFileService.cs @@ -1,38 +1,48 @@ namespace LearningHub.Nhs.AdminUI.Interfaces { - using System.IO; - using System.Threading.Tasks; - using Azure.Storage.Files.Shares.Models; + using System.Collections.Generic; + using System.IO; + using System.Threading.Tasks; + using Azure.Storage.Files.Shares.Models; + using LearningHub.Nhs.Models.Resource; - /// - /// Defines the . - /// - public interface IFileService - { /// - /// The DeleteChunkDirectory. + /// Defines the . /// - /// Directory ref. - /// Chunks. - /// A representing the result of the asynchronous operation. - Task DeleteChunkDirectory(string directoryRef, int chunks); + public interface IFileService + { + /// + /// The DeleteChunkDirectory. + /// + /// Directory ref. + /// Chunks. + /// A representing the result of the asynchronous operation. + Task DeleteChunkDirectory(string directoryRef, int chunks); - /// - /// The DownloadFileAsync. - /// - /// File path. - /// File name. - /// A representing the result of the asynchronous operation. - Task DownloadFileAsync(string filePath, string fileName); + /// + /// The DownloadFileAsync. + /// + /// File path. + /// File name. + /// A representing the result of the asynchronous operation. + Task DownloadFileAsync(string filePath, string fileName); - /// - /// The ProcessFile. - /// - /// The fileBytes. - /// The fileName. - /// The directoryRef. - /// The originalFileName. - /// The . - Task ProcessFile(Stream fileBytes, string fileName, string directoryRef = "", string originalFileName = null); - } + /// + /// The ProcessFile. + /// + /// The fileBytes. + /// The fileName. + /// The directoryRef. + /// The originalFileName. + /// The . + Task ProcessFile(Stream fileBytes, string fileName, string directoryRef = "", string originalFileName = null); + + /// + /// The PurgeResourceFile. + /// + /// The vm.. + /// . + /// The . + Task PurgeResourceFile(ResourceVersionExtendedViewModel vm = null, List filePaths = null); + } } diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Services/FileService.cs b/AdminUI/LearningHub.Nhs.AdminUI/Services/FileService.cs index 7998020b6..60a4e0781 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Services/FileService.cs +++ b/AdminUI/LearningHub.Nhs.AdminUI/Services/FileService.cs @@ -1,122 +1,393 @@ namespace LearningHub.Nhs.AdminUI.Services { - using System; - using System.IO; - using System.Threading.Tasks; - using Azure.Storage.Files.Shares; - using Azure.Storage.Files.Shares.Models; - using LearningHub.Nhs.AdminUI.Configuration; - using LearningHub.Nhs.AdminUI.Interfaces; - using Microsoft.AspNetCore.StaticFiles; - using Microsoft.Extensions.Options; - - /// - /// Defines the . - /// - public class FileService : IFileService - { - private readonly WebSettings settings; - private ShareClient shareClient; + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using Azure.Storage.Files.Shares; + using Azure.Storage.Files.Shares.Models; + using LearningHub.Nhs.AdminUI.Configuration; + using LearningHub.Nhs.AdminUI.Interfaces; + using LearningHub.Nhs.Models.Resource; + using Microsoft.AspNetCore.StaticFiles; + using Microsoft.Extensions.Options; /// - /// Initializes a new instance of the class. + /// Defines the . /// - /// Settings. - public FileService(IOptions settings) + public class FileService : IFileService { - this.settings = settings.Value; - } + private readonly WebSettings settings; + private ShareClient shareClient; + private ShareClient archiveShareClient; + private ShareClient contentArchiveShareClient; - private ShareClient ShareClient - { - get - { - if (this.shareClient == null) + /// + /// Initializes a new instance of the class. + /// + /// Settings. + public FileService(IOptions settings) + { + this.settings = settings.Value; + } + + private ShareClient ShareClient { - var options = new ShareClientOptions(); - options.Retry.MaxRetries = 3; - options.Retry.Delay = TimeSpan.FromSeconds(10); + get + { + if (this.shareClient == null) + { + var options = new ShareClientOptions(); + options.Retry.MaxRetries = 3; + options.Retry.Delay = TimeSpan.FromSeconds(10); - this.shareClient = new ShareClient(this.settings.AzureFileStorageConnectionString, this.settings.AzureFileStorageResourceShareName, options); + this.shareClient = new ShareClient(this.settings.AzureFileStorageConnectionString, this.settings.AzureFileStorageResourceShareName, options); - if (!this.shareClient.Exists()) - { - throw new Exception($"Unable to access azure file storage resource {this.settings.AzureFileStorageResourceShareName}"); - } + if (!this.shareClient.Exists()) + { + throw new Exception($"Unable to access azure file storage resource {this.settings.AzureFileStorageResourceShareName}"); + } + } + + return this.shareClient; + } } - return this.shareClient; - } - } + private ShareClient OutputArchiveShareClient + { + get + { + if (this.contentArchiveShareClient == null) + { + var options = new ShareClientOptions(); + options.Retry.MaxRetries = 3; + options.Retry.Delay = TimeSpan.FromSeconds(10); - /// - /// The DeleteChunkDirectory. - /// - /// The directoryRef. - /// The chunks. - /// The . - public async Task DeleteChunkDirectory(string directoryRef, int chunks) - { - var directory = this.ShareClient.GetDirectoryClient(directoryRef); + this.contentArchiveShareClient = new ShareClient(this.settings.AzureContentArchiveStorageConnectionString, this.settings.AzureFileStorageResourceShareName, options); + + if (!this.contentArchiveShareClient.Exists()) + { + throw new Exception($"Unable to access azure content archive file storage resource {this.settings.AzureFileStorageResourceShareName}"); + } + } + + return this.contentArchiveShareClient; + } + } - if (await directory.ExistsAsync()) - { - for (int i = 0; i < chunks; i++) + private ShareClient InputArchiveShareClient { - var file = directory.GetFileClient("Chunk_" + i.ToString()); - await file.DeleteIfExistsAsync(); + get + { + if (this.archiveShareClient == null) + { + var options = new ShareClientOptions(); + options.Retry.MaxRetries = 3; + options.Retry.Delay = TimeSpan.FromSeconds(10); + + this.archiveShareClient = new ShareClient(this.settings.AzureSourceArchiveStorageConnectionString, this.settings.AzureFileStorageResourceShareName, options); + + if (!this.archiveShareClient.Exists()) + { + throw new Exception($"Unable to access azure file storage resource {this.settings.AzureFileStorageResourceShareName}"); + } + } + + return this.archiveShareClient; + } } - await directory.DeleteAsync(); - } - } + /// + /// The DeleteChunkDirectory. + /// + /// The directoryRef. + /// The chunks. + /// The . + public async Task DeleteChunkDirectory(string directoryRef, int chunks) + { + var directory = this.ShareClient.GetDirectoryClient(directoryRef); - /// - /// The DownloadFileAsync. - /// - /// The filePath. - /// The fileName. - /// The . - public async Task DownloadFileAsync(string filePath, string fileName) - { - var directory = this.ShareClient.GetDirectoryClient(filePath); + if (await directory.ExistsAsync()) + { + for (int i = 0; i < chunks; i++) + { + var file = directory.GetFileClient("Chunk_" + i.ToString()); + await file.DeleteIfExistsAsync(); + } - if (await directory.ExistsAsync()) - { - var file = directory.GetFileClient(fileName); + await directory.DeleteAsync(); + } + } - if (await file.ExistsAsync()) + /// + /// The DownloadFileAsync. + /// + /// The filePath. + /// The fileName. + /// The . + public async Task DownloadFileAsync(string filePath, string fileName) { - return await file.DownloadAsync(); + var directory = this.ShareClient.GetDirectoryClient(filePath); + var sourceDirectory = this.InputArchiveShareClient.GetDirectoryClient(filePath); + + if (await directory.ExistsAsync()) + { + var file = directory.GetFileClient(fileName); + + if (await file.ExistsAsync()) + { + return await file.DownloadAsync(); + } + } + else if (await sourceDirectory.ExistsAsync()) + { + var file = sourceDirectory.GetFileClient(fileName); + + if (await file.ExistsAsync()) + { + return await file.DownloadAsync(); + } + } + + return null; } - } - return null; - } + /// + public async Task ProcessFile(Stream fileBytes, string fileName, string directoryRef = "", string originalFileName = null) + { + if (directoryRef == string.Empty) + { + directoryRef = Guid.NewGuid().ToString(); + } - /// - public async Task ProcessFile(Stream fileBytes, string fileName, string directoryRef = "", string originalFileName = null) - { - if (directoryRef == string.Empty) - { - directoryRef = Guid.NewGuid().ToString(); - } + var directory = this.ShareClient.GetDirectoryClient(directoryRef); + + await directory.CreateIfNotExistsAsync(); - var directory = this.ShareClient.GetDirectoryClient(directoryRef); + var fileClient = directory.GetFileClient(fileName); - await directory.CreateIfNotExistsAsync(); + if (!new FileExtensionContentTypeProvider().TryGetContentType(originalFileName ?? fileName, out string contentType)) + { + contentType = "application/octet-stream"; + } - var fileClient = directory.GetFileClient(fileName); + await fileClient.CreateAsync(fileBytes.Length, httpHeaders: new ShareFileHttpHeaders { ContentType = contentType }); + await fileClient.UploadAsync(fileBytes); - if (!new FileExtensionContentTypeProvider().TryGetContentType(originalFileName ?? fileName, out string contentType)) - { - contentType = "application/octet-stream"; - } + return directoryRef; + } + + /// + /// The PurgeResourceFile. + /// + /// The vm.. + /// . + /// The . + public async Task PurgeResourceFile(ResourceVersionExtendedViewModel vm = null, List filePaths = null) + { + if (filePaths != null + && filePaths.Any()) + { + await this.MoveInPutDirectoryToArchive(filePaths); + return; + } - await fileClient.CreateAsync(fileBytes.Length, httpHeaders: new ShareFileHttpHeaders { ContentType = contentType }); - await fileClient.UploadAsync(fileBytes); + if (vm != null) + { + var allContentPath = new List(); + var allFilePath = new List(); + if (vm.ScormDetails != null && !string.IsNullOrWhiteSpace(vm.ScormDetails.ContentFilePath)) + { + allContentPath.Add(vm.ScormDetails.ContentFilePath); + } + else if (vm.HtmlDetails != null && !string.IsNullOrWhiteSpace(vm.HtmlDetails.ContentFilePath)) + { + allContentPath.Add(vm.HtmlDetails.ContentFilePath); + } - return directoryRef; + // audio and video to be added + await this.MoveInPutDirectoryToArchive(allFilePath); + await this.MoveOutPutDirectoryToArchive(allContentPath); + } + } + + private static async Task WaitForCopyAsync(ShareFileClient fileClient) + { + // Wait for the copy operation to complete + ShareFileProperties copyInfo; + do + { + await Task.Delay(500); // Delay before checking the status again + copyInfo = await fileClient.GetPropertiesAsync().ConfigureAwait(false); + } + while (copyInfo.CopyStatus == CopyStatus.Pending); + + if (copyInfo.CopyStatus != CopyStatus.Success) + { + throw new InvalidOperationException($"Copy failed: {copyInfo.CopyStatus}"); + } + } + + private async Task MoveOutPutDirectoryToArchive(List allDirectoryRef) + { + try + { + if (allDirectoryRef.Any()) + { + foreach (var directoryRef in allDirectoryRef) + { + var directory = this.ShareClient.GetDirectoryClient(directoryRef); + var archiveDirectory = this.OutputArchiveShareClient.GetDirectoryClient(directoryRef); + + await foreach (var fileItem in directory.GetFilesAndDirectoriesAsync()) + { + var sourceFileClient = directory.GetFileClient(fileItem.Name); + + archiveDirectory.CreateIfNotExists(); + if (fileItem.IsDirectory) + { + if (await archiveDirectory.GetSubdirectoryClient(fileItem.Name).ExistsAsync() == false) + { + archiveDirectory.CreateSubdirectory(fileItem.Name); + } + + await this.MigrateSubdirectory(sourceFileClient.Path); + } + else + { + var destinationFileClient = archiveDirectory.GetFileClient(fileItem.Name); + var uri = sourceFileClient.GenerateSasUri(Azure.Storage.Sas.ShareFileSasPermissions.Read, DateTime.UtcNow.AddHours(24)); + + await destinationFileClient.StartCopyAsync(uri); + + await WaitForCopyAsync(destinationFileClient); + } + } + + if (await directory.ExistsAsync()) + { + foreach (var fileItem in directory.GetFilesAndDirectories()) + { + var sourceFileClient = directory.GetFileClient(fileItem.Name); + if (fileItem.IsDirectory) + { + await this.DeleteSubdirectory(sourceFileClient.Path); + } + else + { + await sourceFileClient.DeleteIfExistsAsync(); + } + } + + await directory.DeleteIfExistsAsync(); + } + } + } + } + catch (Exception ex) + { + } + } + + private async Task MigrateSubdirectory(string pathDirectory) + { + var sourceDirectory = this.ShareClient.GetDirectoryClient(pathDirectory); + var archiveDirectory = this.OutputArchiveShareClient.GetDirectoryClient(pathDirectory); + + await foreach (var fileDirectory in sourceDirectory.GetFilesAndDirectoriesAsync()) + { + if (fileDirectory.IsDirectory) + { + if (await archiveDirectory.GetSubdirectoryClient(fileDirectory.Name).ExistsAsync() == false) + { + archiveDirectory.CreateSubdirectory(fileDirectory.Name); + } + + var sourceFileClient = sourceDirectory.GetFileClient(fileDirectory.Name); + await this.MigrateSubdirectory(sourceFileClient.Path); + } + else + { + var sourceFileClient = sourceDirectory.GetFileClient(fileDirectory.Name); + var destinationFileClient = archiveDirectory.GetFileClient(fileDirectory.Name); + var uri = sourceFileClient.GenerateSasUri(Azure.Storage.Sas.ShareFileSasPermissions.Read, DateTime.UtcNow.AddHours(24)); + await destinationFileClient.StartCopyAsync(uri); + await WaitForCopyAsync(destinationFileClient); + } + } + } + + private async Task DeleteSubdirectory(string pathDirectory) + { + var sourceDirectory = this.ShareClient.GetDirectoryClient(pathDirectory); + + await foreach (var fileDirectory in sourceDirectory.GetFilesAndDirectoriesAsync()) + { + if (fileDirectory.IsDirectory) + { + var sourceFileClient = sourceDirectory.GetFileClient(fileDirectory.Name); + await this.DeleteSubdirectory(sourceFileClient.Path); + await sourceFileClient.DeleteIfExistsAsync(); + } + else + { + var sourceFileClient = sourceDirectory.GetFileClient(fileDirectory.Name); + await sourceFileClient.DeleteIfExistsAsync(); + } + } + + await sourceDirectory.DeleteIfExistsAsync(); + } + + private async Task MoveInPutDirectoryToArchive(List allDirectoryRef) + { + if (allDirectoryRef.Any()) + { + foreach (var directoryRef in allDirectoryRef) + { + var directory = this.ShareClient.GetDirectoryClient(directoryRef); + var archiveDirectory = this.InputArchiveShareClient.GetDirectoryClient(directoryRef); + if (!directory.Exists()) + { + continue; + } + + if (directory.Exists()) + { + var inputCheck = directory.GetFilesAndDirectories(); + if (inputCheck.Count() > 1) + { + await this.MoveOutPutDirectoryToArchive(new List { directoryRef }); + continue; + } + } + + await foreach (var fileItem in directory.GetFilesAndDirectoriesAsync()) + { + var sourceFileClient = directory.GetFileClient(fileItem.Name); + + archiveDirectory.CreateIfNotExists(); + var destinationFileClient = archiveDirectory.GetFileClient(fileItem.Name); + var uri = sourceFileClient.GenerateSasUri(Azure.Storage.Sas.ShareFileSasPermissions.Read, DateTime.UtcNow.AddHours(24)); + + await destinationFileClient.StartCopyAsync(uri); + + await WaitForCopyAsync(destinationFileClient); + } + + if (await directory.ExistsAsync()) + { + foreach (var fileItem in directory.GetFilesAndDirectories()) + { + var sourceFileClient = directory.GetFileClient(fileItem.Name); + await sourceFileClient.DeleteIfExistsAsync(); + } + + await directory.DeleteAsync(); + } + } + } + } } - } } \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml b/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml index dddfe43ea..e9aa0ce69 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml +++ b/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml @@ -165,7 +165,7 @@
-

You can enter a maximum of 50 characters

+

You can enter a maximum of 50 characters per keyword.

@{ var i = 0; @@ -334,29 +334,31 @@ }); $('#add-keyword').on('click', function () { - var $keywordInput = $('#add-keyword-input'); - var value = $keywordInput.val(); - if (!value) { - return; - } - value = value.trim(); - if (keywords.indexOf(value) === -1) { - keywords.push(value); - $('#Keywords').val(keywords); - var tag = $('

' + value + '

'); - tag.find('.fa-times').on('click', removeKeyword); - $('.keyword-list').append(tag); - $keywordInput.val(""); - // reindex the keyword inputs - $('.keyword-value').each(function (i, x) { - $(x).attr('name', "Keywords[" + i + "]"); - }); - } - if (keywords.length > 4) { - $('#add-keyword').attr('disabled', 'disabled'); - $('#add-keyword-input').attr('disabled', 'disabled'); + var $keywordInput = $('#add-keyword-input'); + var value = $keywordInput.val(); + if (!value) { + return; + } + + // Split the input value by commas and trim each keyword + var values = value.split(',').map(function (item) { + return item.trim(); + }); + + values.forEach(function (value) { + if (value && keywords.indexOf(value) === -1) { + keywords.push(value); + $('#Keywords').val(keywords); + var tag = $('

' + value + '

'); + tag.find('.fa-times').on('click', removeKeyword); + $('.keyword-list').append(tag); + + // reindex the keyword inputs + $('.keyword-value').each(function (i, x) { + $(x).attr('name', "Keywords[" + i + "]"); + }); } - }); + }); // url var oldUrl = ""; diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Views/User/_UserLearningRecord.cshtml b/AdminUI/LearningHub.Nhs.AdminUI/Views/User/_UserLearningRecord.cshtml index 7dafcfd9b..2057d1a2b 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Views/User/_UserLearningRecord.cshtml +++ b/AdminUI/LearningHub.Nhs.AdminUI/Views/User/_UserLearningRecord.cshtml @@ -36,7 +36,7 @@ - @if (userLearningRecord.IsCurrentResourceVersion && userLearningRecord.ResourceReferenceId > 0 && userLearningRecord.VersionStatusId != (int?)VersionStatusEnum.Unpublished) + @if (userLearningRecord.ResourceReferenceId > 0 && userLearningRecord.VersionStatusId != (int?)VersionStatusEnum.Unpublished) { @userLearningRecord.Title
diff --git a/AdminUI/LearningHub.Nhs.AdminUI/appsettings.json b/AdminUI/LearningHub.Nhs.AdminUI/appsettings.json index b9d74d569..b3a5c0f9c 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/appsettings.json +++ b/AdminUI/LearningHub.Nhs.AdminUI/appsettings.json @@ -26,6 +26,8 @@ "ClientId": "", "AuthTimeout": 20, "AzureFileStorageConnectionString": "", + "AzureSourceArchiveStorageConnectionString": "", + "AzureContentArchiveStorageConnectionString": "", "AzureFileStorageResourceShareName": "resourcesdev", "AzureMediaAadClientId": "", "AzureMediaAadSecret": "", diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs index 7f0c1feee..048f1a426 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs @@ -1,6 +1,7 @@ namespace LearningHub.Nhs.WebUI.Controllers.Api { using System; + using System.Collections.Generic; using System.Threading.Tasks; using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Resource; @@ -557,5 +558,32 @@ public async Task DeleteResourceProviderAsync(int resourceVersionI var validationResult = await this.resourceService.DeleteAllResourceVersionProviderAsync(resourceVersionId); return this.Ok(validationResult); } + + /// + /// The ArchiveResourceFile. + /// + /// filePaths. + /// A representing the asynchronous operation. + [HttpPost] + [Route("ArchiveResourceFile")] + public ActionResult ArchiveResourceFile(List filePaths) + { + _ = Task.Run(async () => { await this.fileService.PurgeResourceFile(null, filePaths); }); + return this.Ok(); + } + + /// + /// The GetObsoleteResourceFile. + /// + /// The resourceVersionId. + /// . + /// The . + [HttpGet] + [Route("GetObsoleteResourceFile/{resourceVersionId}/{deletedResource}")] + public async Task> GetObsoleteResourceFile(int resourceVersionId, bool deletedResource) + { + var result = await this.resourceService.GetObsoleteResourceFile(resourceVersionId, deletedResource); + return result; + } } } diff --git a/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs b/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs index c583859bb..a791bd5d1 100644 --- a/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/MyLearningController.cs @@ -448,6 +448,7 @@ public async Task DownloadCertificate(int resourceReferenceId, in var nodePathNodes = await this.hierarchyService.GetNodePathNodes(resource.NodePathId); var currentUser = await this.userService.GetUserByUserIdAsync((userId == 0) ? this.CurrentUserId : (int)userId); var userEmployment = await this.userService.GetUserEmploymentByIdAsync(currentUser.PrimaryUserEmploymentId ?? 0); + var resourceItemUrl = this.Settings.LearningHubWebUiUrl.Trim() + "Resource/" + resourceReferenceId + "/Item"; if (activity.Item2.CertificateUrl != null) { var file = await this.fileService.DownloadFileAsync(this.filePath, activity.Item2.CertificateUrl); @@ -458,7 +459,7 @@ public async Task DownloadCertificate(int resourceReferenceId, in } } - certificateDetails = new CertificateDetails { AccessCount = activity.Item1, ProfessionalRegistrationNumber = userEmployment?.MedicalCouncilNo, NodeViewModels = nodePathNodes, UserViewModel = currentUser, ResourceItemViewModel = resource, ActivityDetailedItemViewModel = new ActivityDetailedItemViewModel(activity.Item2), DownloadCertificate = true, CertificateBase64Image = base64Image }; + certificateDetails = new CertificateDetails { AccessCount = activity.Item1, ProfessionalRegistrationNumber = userEmployment?.MedicalCouncilNo, NodeViewModels = nodePathNodes, UserViewModel = currentUser, ResourceItemViewModel = resource, ActivityDetailedItemViewModel = new ActivityDetailedItemViewModel(activity.Item2), DownloadCertificate = true, CertificateBase64Image = base64Image, PdfResoureItemUrl = resourceItemUrl }; var renderedViewHTML = new List(); certificateDetails.PageNo++; renderedViewHTML.Add(RenderRazorViewToString(this, "LearningCertificate", certificateDetails)); diff --git a/LearningHub.Nhs.WebUI/Controllers/ResourceController.cs b/LearningHub.Nhs.WebUI/Controllers/ResourceController.cs index cf674fab5..789287844 100644 --- a/LearningHub.Nhs.WebUI/Controllers/ResourceController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/ResourceController.cs @@ -180,7 +180,7 @@ public async Task Index(int resourceReferenceId, bool? acceptSens } // For article/image resources, immediately record the resource activity for this user. - if ((resource.ResourceTypeEnum == ResourceTypeEnum.Article || resource.ResourceTypeEnum == ResourceTypeEnum.Image) && (!resource.SensitiveContent)) + if ((resource.ResourceTypeEnum == ResourceTypeEnum.Article || resource.ResourceTypeEnum == ResourceTypeEnum.Image) && (!resource.SensitiveContent) && this.User.Identity.IsAuthenticated) { var activity = new CreateResourceActivityViewModel() { diff --git a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs index d23e0881e..a6b4652ed 100644 --- a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs @@ -256,7 +256,7 @@ public static bool CanViewPercentage(this ActivityDetailedItemViewModel activity /// The bool. public static bool CanViewProgress(this ActivityDetailedItemViewModel activityDetailedItemViewModel) { - if ((activityDetailedItemViewModel.ResourceType == ResourceTypeEnum.Video || activityDetailedItemViewModel.ResourceType == ResourceTypeEnum.Audio) && activityDetailedItemViewModel.ActivityStatus == ActivityStatusEnum.InProgress) + if ((activityDetailedItemViewModel.ResourceType == ResourceTypeEnum.Video || activityDetailedItemViewModel.ResourceType == ResourceTypeEnum.Audio) && activityDetailedItemViewModel.ActivityStatus == ActivityStatusEnum.InProgress && activityDetailedItemViewModel.IsCurrentResourceVersion) { return true; } diff --git a/LearningHub.Nhs.WebUI/Models/Learning/CertificateDetails.cs b/LearningHub.Nhs.WebUI/Models/Learning/CertificateDetails.cs index f4b74519c..f2ee9043b 100644 --- a/LearningHub.Nhs.WebUI/Models/Learning/CertificateDetails.cs +++ b/LearningHub.Nhs.WebUI/Models/Learning/CertificateDetails.cs @@ -55,5 +55,10 @@ public class CertificateDetails /// Gets or sets PageNo. /// public int PageNo { get; set; } + + /// + /// Gets or sets ResourseItem Url for certificate PDF Download. + /// + public string PdfResoureItemUrl { get; set; } } } diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/Content.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/Content.vue index a74f8443a..154d78e98 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/Content.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/Content.vue @@ -502,6 +502,8 @@ showError: false, errorMessage: '', contributeResourceAVFlag: true, + filePathBeforeFileChange: [] as string[], + filePathAfterFileChange: [] as string[] } }, computed: { @@ -837,6 +839,19 @@ if (uploadResult.resourceType === ResourceType.SCORM) { this.$store.commit('populateScormDetails', uploadResult.resourceVersionId); } + + if (this.filePathBeforeFileChange.length > 0) { + this.getResourceFilePath('completed'); + if (this.filePathBeforeFileChange.length > 0 && this.filePathAfterFileChange.length > 0) { + let filePaths = this.filePathBeforeFileChange.filter(item => !this.filePathAfterFileChange.includes(item)); + if (filePaths.length > 0) { + resourceData.archiveResourceFile(filePaths); + this.filePathBeforeFileChange.length = 0; + this.filePathAfterFileChange.length = 0; + } + } + } + } else { this.fileUploadServerError = 'There was a problem uploading this file to the Learning Hub. Please try again and if it still does not upload, contact the support team.'; this.$store.commit('setSaveStatus', ''); @@ -894,6 +909,7 @@ }, fileChangedScorm() { (this.$refs.fileUploadScorm as any).click(); + this.getResourceFilePath('initialised'); }, childResourceFileChanged(newFile: File) { this.uploadingFile = newFile; @@ -943,6 +959,7 @@ fileChanged() { this.fileUploadRef.value = null; this.fileUploadRef.click(); + this.getResourceFilePath('initialised'); }, childFileUploadError(errorType: FileErrorTypeEnum, customError: string) { this.fileErrorType = errorType; @@ -1015,6 +1032,21 @@ break; } return errorTitle; + }, + async getResourceFilePath(fileChangeStatus: string) { + let resource = this.$store.state.resourceDetail; + if (resource != null && resource.resourceVersionId > 0 &&(resource.resourceType != this.resourceType.CASE || resource.resourceType != this.resourceType.ASSESSMENT)) + { + await resourceData.getObsoleteResourceFile(resource.resourceVersionId).then(response => { + if (fileChangeStatus == 'initialised') { + this.filePathBeforeFileChange = response; + } + else if (fileChangeStatus == 'completed') { + this.filePathAfterFileChange = response; + } + }); + } + } }, validations: { diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue index e210dd4a3..622e1ad4d 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue @@ -90,11 +90,11 @@ To help learners find this resource, type one or more relevant keywords separated by commas and click 'Add'.
- +
@@ -529,7 +529,7 @@ if (!this.keywords.find(_keyword => allTrimmedKeyword.includes(_keyword.keyword.toLowerCase()))) { for (var i = 0; i < allTrimmedKeyword.length; i++) { let item = allTrimmedKeyword[i]; - if (item.length > 0) { + if (item.length > 0 && item.length <= 50) { let newkeywordObj = new KeywordModel(); newkeywordObj.keyword = item; newkeywordObj.resourceVersionId = this.resourceDetail.resourceVersionId; diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/data/resource.ts b/LearningHub.Nhs.WebUI/Scripts/vuesrc/data/resource.ts index b6ba8dbaf..9127917d3 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/data/resource.ts +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/data/resource.ts @@ -545,6 +545,29 @@ const getMKPlayerKey = async function (): Promise { }); }; +const getObsoleteResourceFile = async function (id: number): Promise { + return await AxiosWrapper.axios.get('/api/Resource/GetObsoleteResourceFile/' + id + '/' + true + `?timestamp=${new Date().getTime()}`) + .then(response => { + console.log(response.data); + return response.data; + }) + .catch(e => { + console.log('getObsoleteResourceFiles:' + e); + throw e; + }); +}; + +const archiveResourceFile = async function (filepaths: string[]): Promise { + const params = {filePaths:filepaths}; + return await AxiosWrapper.axios.post('/api/Resource/DuplicateBlocks', params).then(() => { + return true + }).catch(e => { + console.log('archiveResourceFile:' + e); + throw e; + }); +}; + + export const resourceData = { getContributeConfiguration, getContributeSettings, @@ -586,5 +609,7 @@ export const resourceData = { getContributeAVResourceFlag, getDisplayAVResourceFlag, getAVUnavailableView, + getObsoleteResourceFile, + archiveResourceFile, getMKPlayerKey }; \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/mycontributions/gridcomponent.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/mycontributions/gridcomponent.vue index 422e88a9c..78bd5d506 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/mycontributions/gridcomponent.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/mycontributions/gridcomponent.vue @@ -44,7 +44,7 @@ diff --git a/LearningHub.Nhs.WebUI/Views/Bookmark/Toggle.cshtml b/LearningHub.Nhs.WebUI/Views/Bookmark/Toggle.cshtml index d06b16fdd..8f77215ae 100644 --- a/LearningHub.Nhs.WebUI/Views/Bookmark/Toggle.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Bookmark/Toggle.cshtml @@ -1,136 +1,142 @@ @using System.Text.RegularExpressions @model LearningHub.Nhs.WebUI.Models.Bookmark.EditBookmarkViewModel @{ - var bookmarkType = (Model.BookmarkTypeId == 1) ? "Folder" : "Bookmark"; - var titleAction = Model.Bookmarked ? "Delete" : "Add"; - var buttonAction = Model.Bookmarked ? "Delete" : "Continue"; + var bookmarkType = (Model.BookmarkTypeId == 1) ? "Folder" : "Bookmark"; + var titleAction = Model.Bookmarked ? "Delete" : "Add"; + var buttonAction = Model.Bookmarked ? "Delete" : "Continue"; - ViewData["Title"] = $"{titleAction} {bookmarkType}"; + ViewData["Title"] = $"{titleAction} {bookmarkType}"; - // Prepare the parameters for the back-link component. - var backToText = "Back to: "; - var returnUrl = ViewBag.ReturnUrl.ToLower(); - var controller = string.Empty; - var action = string.Empty; - var routeParams = new Dictionary(); + // Prepare the parameters for the back-link component. + var backToText = "Back to: "; + var returnUrl = ViewBag.ReturnUrl.ToLower(); + var controller = string.Empty; + var action = string.Empty; + var routeParams = new Dictionary(); - if (returnUrl == "/") + if (returnUrl == "/") + { + backToText += "Learning Hub"; + controller = "home"; + action = "index"; + } + else if (returnUrl.Contains("/bookmark")) + { + backToText += "My bookmarks"; + controller = "bookmark"; + action = "index"; + } + else + { + var resourceMatches = Regex.Matches(returnUrl, @"^/resource/([0-9]+)(/item)?$"); + var catalogueMatches = Regex.Matches(returnUrl, @"^/catalogue/(\w+)$"); + + if (resourceMatches.Count > 0) { - backToText += "Learning Hub"; - controller = "home"; - action = "index"; + backToText += "Learning Resource"; + controller = "resource"; + action = "Index"; + routeParams = new Dictionary { { "resourceReferenceId", resourceMatches[0].Groups[1].Value } }; } - else if (returnUrl == "/bookmark") + else if (catalogueMatches.Count > 0) { - backToText += "My bookmarks"; - controller = "bookmark"; - action = "index"; + backToText += "Catalogue"; + controller = "catalogue"; + action = "Index"; + routeParams = new Dictionary { { "reference", catalogueMatches[0].Groups[1].Value } }; } else { - var resourceMatches = Regex.Matches(returnUrl, @"^/resource/([0-9]+)(/item)?$"); - var catalogueMatches = Regex.Matches(returnUrl, @"^/catalogue/(\w+)$"); - - if (resourceMatches.Count > 0) - { - backToText += "Learning Resource"; - controller = "resource"; - action = "Index"; - routeParams = new Dictionary { { "resourceReferenceId", resourceMatches[0].Groups[1].Value } }; - } - else if (catalogueMatches.Count > 0) - { - backToText += "Catalogue"; - controller = "catalogue"; - action = "Index"; - routeParams = new Dictionary { { "reference", catalogueMatches[0].Groups[1].Value } }; - } + backToText += "Learning Hub"; + controller = "home"; + action = "index"; } + } } @section styles{ - + }
-
- +
+ -
+ - + - @if (Model.Bookmarked) - { - @if (Model.BookmarkTypeId == 1) - { -

Are you sure you want to delete this folder?

-
-

All bookmarks contained within the folder will also be deleted. To save any bookmarks move them out of the folder.

-
- -
-
-
- -
-

@Model.Title

-
-
-
-
- } - else - { -

Are you sure you want to remove this bookmark?

-
-
-
- -
-

@Model.Title

-
-
-
-
- } - } - else - { -

Add @(Model.BookmarkTypeId == 1 ? "a folder" : "bookmark")

+ @if (Model.Bookmarked) + { + @if (Model.BookmarkTypeId == 1) + { +

Are you sure you want to delete this folder?

+
+

All bookmarks contained within the folder will also be deleted. To save any bookmarks move them out of the folder.

+
-
-
-
- You must enter a folder name" : "You must enter a bookmark name")" /> -
-
+
+
+
+ +
+

@Model.Title

- } - -
- -
- Cancel +
+
+
+ } + else + { +

Are you sure you want to remove this bookmark?

+
+
+
+ +
+

@Model.Title

+
+
+ } + } + else + { +

Add @(Model.BookmarkTypeId == 1 ? "a folder" : "bookmark")

+ +
+
+
+ +
+
+
+ } + +
+ +
+ Cancel +
+
- - - - - - - - -
+ + + + + + + + +
\ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/Home/_CmsVideo.cshtml b/LearningHub.Nhs.WebUI/Views/Home/_CmsVideo.cshtml index 8bcb5ada1..577ddfc93 100644 --- a/LearningHub.Nhs.WebUI/Views/Home/_CmsVideo.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Home/_CmsVideo.cshtml @@ -1,10 +1,13 @@ @using LearningHub.Nhs.Models.Content @model PageSectionDetailViewModel @{ - string mkPlayerLicence = (string) ViewData["mkPlayerLicenceKey"]; + string mkPlayerLicence = (string)ViewData["mkPlayerLicenceKey"]; + var scheme = Context?.Request?.Scheme ?? "undefined"; + var host = Context?.Request?.Host; + var path = Context?.Request?.Path ?? "undefined"; + var requestURL = $"{scheme}://{host}{path}"; } -@* *@ @* @if (Model != null) @@ -50,7 +53,7 @@ } *@
-