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/Controllers/UserGroupController.cs b/AdminUI/LearningHub.Nhs.AdminUI/Controllers/UserGroupController.cs index 3a27787f4..1be7f211c 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Controllers/UserGroupController.cs +++ b/AdminUI/LearningHub.Nhs.AdminUI/Controllers/UserGroupController.cs @@ -257,6 +257,7 @@ public async Task AddUsersToUserGroup(int userGroupId, string use var vr = await this.userGroupService.AddUsersToUserGroup(userGroupId, userIdList); if (vr.IsValid) { + this.ClearUserCachedPermissions(userIdList); return this.Json(new { success = true, @@ -527,5 +528,16 @@ public async Task UserGroupCatalogues(int id) return this.PartialView("_UserGroupCatalogues", catalogues); } + + private void ClearUserCachedPermissions(string userIdList) + { + if (!string.IsNullOrWhiteSpace(userIdList)) + { + foreach (var userId in userIdList.Split(",")) + { + _ = Task.Run(async () => { await this.userService.ClearUserCachedPermissions(int.Parse(userId)); }); + } + } + } } } \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Helpers/LearningActivityHelper.cs b/AdminUI/LearningHub.Nhs.AdminUI/Helpers/LearningActivityHelper.cs index b7646146e..caffaaa5d 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Helpers/LearningActivityHelper.cs +++ b/AdminUI/LearningHub.Nhs.AdminUI/Helpers/LearningActivityHelper.cs @@ -116,6 +116,12 @@ public static string GetResourceTypeVerb(this MyLearningDetailedItemViewModel my return "Played " + GetDurationText(myLearningDetailedItemViewModel.ActivityDurationSeconds * 1000); case ResourceTypeEnum.WebLink: return "Visited"; + case ResourceTypeEnum.Html: + return "Viewed"; + case ResourceTypeEnum.Case: + return "Accessed"; + case ResourceTypeEnum.Assessment: + return "Accessed"; default: return string.Empty; } 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/LearningHub.Nhs.AdminUI.csproj b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj index 7bef84029..21ec69f5e 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj +++ b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj @@ -89,7 +89,7 @@ - + diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/ckeditorwithhint.vue b/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/ckeditorwithhint.vue index a31005950..824b025cd 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/ckeditorwithhint.vue +++ b/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/ckeditorwithhint.vue @@ -23,7 +23,10 @@ }, data() { return { - editorConfig: { toolbar: CKEditorToolbar.default }, + editorConfig: { + toolbar: CKEditorToolbar.default, + versionCheck: false + }, description: this.initialValue, hint: `You have ${this.maxLength} characters remaining`, valid: getRemainingCharactersFromHtml(this.maxLength, this.initialValue) >= 0, diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content-structure/contentStructure.vue b/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content-structure/contentStructure.vue index c16eb9d59..6b9041142 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content-structure/contentStructure.vue +++ b/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content-structure/contentStructure.vue @@ -169,7 +169,10 @@ export default Vue.extend({ isReady: false, EditModeEnum: EditModeEnum, HierarchyEditStatusEnum: HierarchyEditStatusEnum, - editorConfig: { toolbar: CKEditorToolbar.default }, + editorConfig: { + toolbar: CKEditorToolbar.default, + versionCheck: false + }, deleteFolderName: '', editFolderStructureButtonText: '', editFolderStructureButtonDisabled: true, diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/pageImageSection.vue b/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/pageImageSection.vue index 432da81ae..ee6f0334f 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/pageImageSection.vue +++ b/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/pageImageSection.vue @@ -257,6 +257,7 @@ SectionLayoutType: SectionLayoutType, editorConfig: { toolbar: CKEditorToolbar.landingPages, + versionCheck: false, stylesSet: 'landing-pages-image-text' }, diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/pageVideoSection.vue b/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/pageVideoSection.vue index 74e0ab031..77d38adb6 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/pageVideoSection.vue +++ b/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/pageVideoSection.vue @@ -263,6 +263,7 @@ Vue.use(Vuelidate as any); SectionLayoutType: SectionLayoutType, editorConfig: { toolbar: CKEditorToolbar.landingPages, + versionCheck: false, stylesSet: 'landing-pages-video-text' }, 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..e0e81edcf 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml +++ b/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml @@ -3,423 +3,442 @@ @using LearningHub.Nhs.Models.Enums @using Newtonsoft.Json @{ - var page = Model.CatalogueNodeVersionId == 0 ? CatalogueNavPage.Add : CatalogueNavPage.Edit; - ViewData["Title"] = page == CatalogueNavPage.Add ? "Add catalogue" : "Edit catalogue"; - var baseUrl = _settings.Value.LearningHubUrl + "catalogue/"; - var url = baseUrl + (Model.Url ?? ""); - var aToZ = (int)CatalogueOrder.AlphabeticalAscending; - var keywords = Model.Keywords ?? new List(); - var Providers = Model.Providers; - var CatalogueNodeVersionProviderId = Model.CatalogueNodeVersionProvider?.ProviderId ?? 0; - var CatalogueNodeVersionProvider = Model.CatalogueNodeVersionProvider; - var action = page.ToString(); - var keywordsJson = Html.Raw(JsonConvert.SerializeObject(keywords)); - var imageBaseUrl = "/file/download/CatalogueImageDirectory/"; - var lastModifiedDate = Model.LastModifiedDate?.ToString("dd MMM yyyy"); + var page = Model.CatalogueNodeVersionId == 0 ? CatalogueNavPage.Add : CatalogueNavPage.Edit; + ViewData["Title"] = page == CatalogueNavPage.Add ? "Add catalogue" : "Edit catalogue"; + var baseUrl = _settings.Value.LearningHubUrl + "catalogue/"; + var url = baseUrl + (Model.Url ?? ""); + var aToZ = (int)CatalogueOrder.AlphabeticalAscending; + var keywords = Model.Keywords ?? new List(); + var Providers = Model.Providers; + var CatalogueNodeVersionProviderId = Model.CatalogueNodeVersionProvider?.ProviderId ?? 0; + var CatalogueNodeVersionProvider = Model.CatalogueNodeVersionProvider; + var action = page.ToString(); + var keywordsJson = Html.Raw(JsonConvert.SerializeObject(keywords)); + var imageBaseUrl = "/file/download/CatalogueImageDirectory/"; + var lastModifiedDate = Model.LastModifiedDate?.ToString("dd MMM yyyy"); } @section SideMenu { - @{ - await Html.RenderPartialAsync("_NavSection"); - } + @{ + await Html.RenderPartialAsync("_NavSection"); + } } @if (!string.IsNullOrEmpty(ViewBag.ErrorMessage)) { - + } @if (page == CatalogueNavPage.Edit) { -
-
-

@Model.Name

-
+
+
+

@Model.Name

+
} @{ - await Html.RenderPartialAsync("_CatalogueNav.cshtml", new CatalogueNavViewModel { Page = page, CatalogueId = Model.CatalogueNodeVersionId }); + await Html.RenderPartialAsync("_CatalogueNav.cshtml", new CatalogueNavViewModel { Page = page, CatalogueId = Model.CatalogueNodeVersionId }); }
-
- - - - -
-
-
- @if (page == CatalogueNavPage.Edit) - { - var idString = Model.CatalogueNodeVersionId.ToString(); - var paddedIdString = idString.Length > 3 ? idString : idString.PadLeft(3, '0'); - ID: @paddedIdString - } - else - { - NEW - } -
- @if (page == CatalogueNavPage.Add || Model.Hidden) - { - Hidden - } - @if (lastModifiedDate != null) - { -

Last modified on: @lastModifiedDate

- } -
+ + + + + +
+
+
+ @if (page == CatalogueNavPage.Edit) + { + var idString = Model.CatalogueNodeVersionId.ToString(); + var paddedIdString = idString.Length > 3 ? idString : idString.PadLeft(3, '0'); + ID: @paddedIdString + } + else + { + NEW + }
- - @if (!ViewData.ModelState.IsValid) + @if (page == CatalogueNavPage.Add || Model.Hidden) + { + Hidden + } + @if (lastModifiedDate != null) { -
This form failed to save because there are errors below.
+

Last modified on: @lastModifiedDate

} +
+
-
-
- -

You can enter a maximum of 255 characters including spaces

- - -
-
-
-
- - @if (page == CatalogueNavPage.Add) - { - - } - else - { - - } - -
-
- @if (Model.Status == VersionStatusEnum.Published && !Model.Hidden) - { - - } - else - { -

- } -
-
-
-
- -
-
-
-
- - -
-
-
-
- -
-
-
-
- + @if (!ViewData.ModelState.IsValid) + { +
This form failed to save because there are errors below.
+ } + +
+
+ +

You can enter a maximum of 255 characters including spaces

+ + +
+
+
+
+ + @if (page == CatalogueNavPage.Add) + { + + } + else + { + + } + +
+
+ @if (Model.Status == VersionStatusEnum.Published && !Model.Hidden) + { + + } + else + { +

+ } +
+
+
+
+ +
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + + Only the first 3,000 characters of the description will be used by search + +
+
+
+
+ +

+ Add one or more relevant keywords to help learners find this catalogue, ensuring that the keyword is specifically related to the catalogue rather than the resources associated with it. A maximum of 5 keywords can be added. Enter one keyword and add it before entering another. +

+
+
+ +
+
+
-
-
- - - Only the first 3,000 characters of the description will be used by search - -
+
+
+ + +
-
-
- -

- Add one or more relevant keywords to help learners find this catalogue, ensuring that the keyword is specifically related to the catalogue rather than the resources associated with it. A maximum of 5 keywords can be added. Enter one keyword and add it before entering another. -

-
-
- -
- -
-
-
-
-
- - -
-
+
+
+

You can enter a maximum of 50 characters per keyword.

+
+ @{ + var i = 0; + } + @foreach (var keyword in keywords) + { +
+

@keyword

+ +
-
-

You can enter a maximum of 50 characters

-
- @{ - var i = 0; - } - @foreach (var keyword in keywords) - { -
-

@keyword

- - -
- } -
-
+ }
+
+
-
+
-
-
- -
-
+
+
+ +
+
-
-
-
- @if (!Model.RestrictedAccess) - { - - } - else - { - - } - Unrestricted access (default) -
-
- @if (Model.RestrictedAccess) - { - - } - else - { - - } - Restricted access -
-
-
- @if (!Model.HasUserGroup) +
+
+
+ @if (!Model.RestrictedAccess) { -
-

There are no user groups associated with this catalogue. You can add some in the User Groups tab.

-
+ } + else + { + + } + Unrestricted access (default) +
+
+ @if (Model.RestrictedAccess) + { + + } + else + { + + } + Restricted access +
+
+ @if (!Model.HasUserGroup) + { +
+

There are no user groups associated with this catalogue. You can add some in the User Groups tab.

+
+ } +
-
- -
-
- -

When applicable please select the provider of this content. This will allow a contributor to flag content from a specific provider.

-

This will enable learners to search for content produced by organisations and help separate learning resources from community contributions.

-

Developed with;

-
-
+
-
-
- @if (Providers != null && Providers.Count() > 0) - { -
- @foreach (var provider in Providers) - { - - @provider.Name -
- } - - Not applicable -
- } -
-
+
+
+ +

When applicable please select the provider of this content. This will allow a contributor to flag content from a specific provider.

+

This will enable learners to search for content produced by organisations and help separate learning resources from community contributions.

+

Developed with;

-
-
- @if (page == CatalogueNavPage.Add) - { - - } - else - { - - } +
+ +
+
+ @if (Providers != null && Providers.Count() > 0) + { +
+ @foreach (var provider in Providers) + { + + @provider.Name +
+ } + + Not applicable
+ }
- -
+
+
+
+
+ @if (page == CatalogueNavPage.Add) + { + + } + else + { + + } +
+
+ +
@section Scripts { - - + + } \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Views/User/_UserLearningRecord.cshtml b/AdminUI/LearningHub.Nhs.AdminUI/Views/User/_UserLearningRecord.cshtml index 7dafcfd9b..b60466822 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Views/User/_UserLearningRecord.cshtml +++ b/AdminUI/LearningHub.Nhs.AdminUI/Views/User/_UserLearningRecord.cshtml @@ -46,7 +46,8 @@ else { - @userLearningRecord.Title + @userLearningRecord.Title
+ Last @LearningActivityHelper.GetResourceTypeVerb(userLearningRecord).ToLower() on : @userLearningRecord.ActivityDate.DateTime.ToString("dd MMM yyyy") at @userLearningRecord.ActivityDate.DateTime.ToShortTimeString()
} 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/Configuration/FindwiseSettings.cs b/LearningHub.Nhs.WebUI/Configuration/FindwiseSettings.cs index dcac6798d..683d5b4c9 100644 --- a/LearningHub.Nhs.WebUI/Configuration/FindwiseSettings.cs +++ b/LearningHub.Nhs.WebUI/Configuration/FindwiseSettings.cs @@ -14,5 +14,10 @@ public class FindwiseSettings /// Gets or sets the CatalogueSearchPageSize. /// public int CatalogueSearchPageSize { get; set; } + + /// + /// Gets or sets the AllCatalogueSearchPageSize. + /// + public int AllCatalogueSearchPageSize { get; set; } } } diff --git a/LearningHub.Nhs.WebUI/Configuration/Settings.cs b/LearningHub.Nhs.WebUI/Configuration/Settings.cs index d107ac2d5..9764d293e 100644 --- a/LearningHub.Nhs.WebUI/Configuration/Settings.cs +++ b/LearningHub.Nhs.WebUI/Configuration/Settings.cs @@ -245,5 +245,10 @@ public Settings() /// Gets or sets the MediaKindSettings. /// public MediaKindSettings MediaKindSettings { get; set; } = new MediaKindSettings(); + + /// + /// Gets or sets AllCataloguePageSize. + /// + public int AllCataloguePageSize { get; set; } } } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/ContributeController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/ContributeController.cs index 6bfbcca6c..572cd69c9 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/ContributeController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/ContributeController.cs @@ -349,11 +349,18 @@ public async Task PublishResourceVersionAsync([FromBody] PublishVi { if (associatedResource.ResourceType != ResourceTypeEnum.Scorm && associatedResource.ResourceType != ResourceTypeEnum.Html) { + try + { var obsoleteFiles = await this.resourceService.GetObsoleteResourceFile(publishViewModel.ResourceVersionId); if (obsoleteFiles != null && obsoleteFiles.Any()) { - await this.fileService.PurgeResourceFile(null, obsoleteFiles); + _ = Task.Run(async () => { await this.fileService.PurgeResourceFile(null, obsoleteFiles); }); } + } + catch (Exception ex) + { + this.Logger.LogError($"File Archive Error: {ex.Message}", $"ResourceVersionId -{publishViewModel.ResourceVersionId}"); + } } } @@ -707,8 +714,8 @@ private async Task RemoveBlockCollectionFiles(int resourceVersionId, BlockCollec { foreach (var oldblock in existingImages) { - var entry = newBlocks.FirstOrDefault(x => x.BlockType == BlockType.Media && x.MediaBlock != null && x.MediaBlock.MediaType == MediaType.Image && x.MediaBlock.Image != null && (x.MediaBlock?.Image?.File?.FileId == oldblock.MediaBlock?.Image?.File?.FileId || x.MediaBlock?.Image?.File?.FilePath == oldblock.MediaBlock?.Image?.File?.FilePath)); - if (entry == null) + var entry = newBlocks.FirstOrDefault(x => x.BlockType == BlockType.Media && x.MediaBlock != null && x.MediaBlock.MediaType == MediaType.Image && x.MediaBlock.Image != null && (x.MediaBlock?.Image?.File?.FileId == oldblock.MediaBlock?.Image?.File?.FileId || x.MediaBlock?.Image?.File?.FilePath == oldblock.MediaBlock?.Image?.File?.FilePath)); + if (entry == null) { filePaths.Add(oldblock?.MediaBlock?.Image?.File?.FilePath); } @@ -790,8 +797,10 @@ private async Task RemoveBlockCollectionFiles(int resourceVersionId, BlockCollec _ = Task.Run(async () => { await this.fileService.PurgeResourceFile(null, deleteList); }); } } - catch + catch (Exception ex) { + var param = new object[] { resourceVersionId, existingResource, newResource }; + this.Logger.LogError($"BlockCollection Archive Error: {ex.Message}", param); } } diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs index 7f0c1feee..e5e8ab35e 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs @@ -1,6 +1,8 @@ namespace LearningHub.Nhs.WebUI.Controllers.Api { using System; + using System.Collections.Generic; + using System.Linq; using System.Threading.Tasks; using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Resource; @@ -104,7 +106,6 @@ public async Task DownloadResourceAndRecordActivity(int resourceV ActivityStatus = ActivityStatusEnum.Completed, }; await this.activityService.CreateResourceActivityAsync(activity); - return this.File(file.Content, file.ContentType, fileName); } else @@ -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(IEnumerable filePaths) + { + _ = Task.Run(async () => { await this.fileService.PurgeResourceFile(null, filePaths.ToList()); }); + 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/Api/ScormController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/ScormController.cs index ba9ed57f3..6a8f7d699 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/ScormController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/ScormController.cs @@ -335,10 +335,10 @@ private async Task GetSco(int resourceReferenceId) // 1. StudentDataMasteryScore, StudentDataMaxTimeAllowed, StudentDataTimeLimitAction, Credit sco.Credit = "credit"; sco.CommentsFromLms = ScormContansts.CommentsFromLMS; - sco.LaunchData = rv.ScormDetails.ScormManifest.LaunchData ?? ScormContansts.LaunchData; - sco.StudentDataMasteryScore = rv.ScormDetails.ScormManifest.MasteryScore ?? ScormContansts.StudentDataMasteryScore; - sco.StudentDataMaxTimeAllowed = rv.ScormDetails.ScormManifest.MaxTimeAllowed ?? ScormContansts.StudentDataMaxTimeAllowed; - sco.StudentDataTimeLimitAction = rv.ScormDetails.ScormManifest.TimeLimitAction ?? ScormContansts.StudentDataTimeLimitAction; + sco.LaunchData = rv?.ScormDetails?.ScormManifest?.LaunchData ?? ScormContansts.LaunchData; + sco.StudentDataMasteryScore = rv?.ScormDetails?.ScormManifest?.MasteryScore ?? ScormContansts.StudentDataMasteryScore; + sco.StudentDataMaxTimeAllowed = rv?.ScormDetails?.ScormManifest?.MaxTimeAllowed ?? ScormContansts.StudentDataMaxTimeAllowed; + sco.StudentDataTimeLimitAction = rv?.ScormDetails?.ScormManifest?.TimeLimitAction ?? ScormContansts.StudentDataTimeLimitAction; // 2. LessonLocation, ScoreRaw, SuspendData, Entry // If previous Scorm Activity was "incomplete" then offer pick-up from previous lesson location. diff --git a/LearningHub.Nhs.WebUI/Controllers/BookmarkController.cs b/LearningHub.Nhs.WebUI/Controllers/BookmarkController.cs index b38881542..0d699fe4d 100644 --- a/LearningHub.Nhs.WebUI/Controllers/BookmarkController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/BookmarkController.cs @@ -276,6 +276,16 @@ public async Task BookmarkToggle(EditBookmarkViewModel bookmark, { if (!this.ModelState.IsValid) { + this.ModelState.Remove(nameof(bookmark.Title)); + if (bookmark.BookmarkTypeId == 1) + { + this.ModelState.AddModelError(nameof(bookmark.Title), "You must enter a folder name"); + } + else + { + this.ModelState.AddModelError(nameof(bookmark.Title), "You must enter a bookmark name"); + } + this.ViewBag.ReturnUrl = returnUrl; return this.View("Toggle", bookmark); } diff --git a/LearningHub.Nhs.WebUI/Controllers/CatalogueController.cs b/LearningHub.Nhs.WebUI/Controllers/CatalogueController.cs index 67b4a5cb9..efa8cd4f2 100644 --- a/LearningHub.Nhs.WebUI/Controllers/CatalogueController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/CatalogueController.cs @@ -123,6 +123,7 @@ public async Task Index(int pageIndex = 1, string term = null) BookmarkId = t.BookmarkId, NodeId = int.Parse(t.Id), BadgeUrl = t.BadgeUrl, + Providers = t.Providers, }).ToList(); } else @@ -190,8 +191,6 @@ public async Task IndexAsync(string reference, string tab, int? n CatalogueAccessRequestViewModel catalogueAccessRequest = null; if (this.ViewBag.UserAuthenticated) { - var cacheKey = $"{this.CurrentUserId}:AllRolesWithPermissions"; - await this.cacheService.RemoveAsync(cacheKey); userGroups = await this.userGroupService.GetRoleUserGroupDetailAsync(); catalogueAccessRequest = await this.catalogueService.GetLatestCatalogueAccessRequestAsync(catalogue.NodeId); } @@ -553,5 +552,70 @@ public async Task RequestPreviewAccess(CatalogueRequestAccessView return this.View("RequestPreviewAccess", viewModel); } } + + /// + /// Get all catelogues, filter and pagination based on alphabets. + /// + /// filterChar. + /// rk. + [Route("/allcatalogue")] + [Route("/allcatalogue/{filterChar}")] + public async Task GetAllCatalogue(string filterChar = "a") + { + var pageSize = this.settings.AllCataloguePageSize; + var catalogues = await this.catalogueService.GetAllCatalogueAsync(filterChar, pageSize); + return this.View("allcatalogue", catalogues); + } + + /// + /// AllCatalogues Search. + /// + /// pageIndex. + /// Search term. + /// IActionResult. + [Route("/allcataloguesearch")] + public async Task GetAllCatalogueSearch(int pageIndex = 1, string term = null) + { + var catalogues = new AllCatalogueSearchResponseViewModel(); + var searchString = term?.Trim() ?? string.Empty; + var allCatalogueSearchPageSize = this.settings.FindwiseSettings.AllCatalogueSearchPageSize; + + if (!string.IsNullOrWhiteSpace(term)) + { + var termCatalogues = await this.searchService.GetAllCatalogueSearchResultAsync( + new AllCatalogueSearchRequestModel + { + SearchText = searchString, + PageIndex = pageIndex - 1, + PageSize = allCatalogueSearchPageSize, + }); + + catalogues.TotalCount = termCatalogues.TotalHits; + catalogues.Catalogues = termCatalogues.DocumentModel.Select(t => new AllCatalogueViewModel + { + Url = t.Url, + Name = t.Name, + CardImageUrl = t.CardImageUrl, + BannerUrl = t.BannerUrl, + Description = t.Description, + RestrictedAccess = t.RestrictedAccess, + HasAccess = t.HasAccess, + IsBookmarked = t.IsBookmarked, + BookmarkId = t.BookmarkId, + NodeId = int.Parse(t.Id), + BadgeUrl = t.BadgeUrl, + Providers = t.Providers, + }).ToList(); + } + else + { + catalogues.TotalCount = 0; + catalogues.Catalogues = new List(); + } + + this.ViewBag.PageIndex = pageIndex; + this.ViewBag.PageSize = allCatalogueSearchPageSize; + return this.View("AllCatalogueSearch", catalogues); + } } } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Controllers/HomeController.cs b/LearningHub.Nhs.WebUI/Controllers/HomeController.cs index de5c71912..87d01e668 100644 --- a/LearningHub.Nhs.WebUI/Controllers/HomeController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/HomeController.cs @@ -203,11 +203,17 @@ public async Task Index(string myLearningDashboard = "my-in-progr this.Logger.LogInformation("User is authenticated: User is {fullname} and userId is: {lhuserid}", this.User.Identity.GetCurrentName(), this.User.Identity.GetCurrentUserId()); if (this.User.IsInRole("Administrator") || this.User.IsInRole("BlueUser") || this.User.IsInRole("ReadOnly") || this.User.IsInRole("BasicUser")) { + var learningTask = this.dashboardService.GetMyAccessLearningsAsync(myLearningDashboard, 1); + var resourcesTask = this.dashboardService.GetResourcesAsync(resourceDashboard, 1); + var cataloguesTask = this.dashboardService.GetCataloguesAsync(catalogueDashboard, 1); + + await Task.WhenAll(learningTask, resourcesTask, cataloguesTask); + var model = new DashboardViewModel() { - MyLearnings = await this.dashboardService.GetMyAccessLearningsAsync(myLearningDashboard, 1), - Resources = await this.dashboardService.GetResourcesAsync(resourceDashboard, 1), - Catalogues = await this.dashboardService.GetCataloguesAsync(catalogueDashboard, 1), + MyLearnings = await learningTask, + Resources = await resourcesTask, + Catalogues = await cataloguesTask, }; if (!string.IsNullOrEmpty(this.Request.Query["preview"]) && Convert.ToBoolean(this.Request.Query["preview"])) @@ -271,9 +277,13 @@ public async Task LoadPage(string dashBoardTray = "my-learning", } else { - model.MyLearnings = await this.dashboardService.GetMyAccessLearningsAsync(myLearningDashBoard, dashBoardTray == "my-learning" ? pageNumber : 1); - model.Resources = await this.dashboardService.GetResourcesAsync(resourceDashBoard, dashBoardTray == "resources" ? pageNumber : 1); - model.Catalogues = await this.dashboardService.GetCataloguesAsync(catalogueDashBoard, dashBoardTray == "catalogues" ? pageNumber : 1); + var learningTask = this.dashboardService.GetMyAccessLearningsAsync(myLearningDashBoard, dashBoardTray == "my-learning" ? pageNumber : 1); + var resourcesTask = this.dashboardService.GetResourcesAsync(resourceDashBoard, dashBoardTray == "resources" ? pageNumber : 1); + var cataloguesTask = this.dashboardService.GetCataloguesAsync(catalogueDashBoard, dashBoardTray == "catalogues" ? pageNumber : 1); + await Task.WhenAll(learningTask, resourcesTask, cataloguesTask); + model.MyLearnings = await learningTask; + model.Resources = await resourcesTask; + model.Catalogues = await cataloguesTask; return this.View("Dashboard", model); } } 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..7775ec6cd 100644 --- a/LearningHub.Nhs.WebUI/Controllers/ResourceController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/ResourceController.cs @@ -123,7 +123,7 @@ public async Task Index(int resourceReferenceId, bool? acceptSens var resource = await this.resourceService.GetItemByIdAsync(resourceReferenceId); - if (resource.Id == 0 || (resource.Catalogue != null && resource.Catalogue.Hidden)) + if ((resource == null && resource.Id == 0) || (resource.Catalogue != null && resource.Catalogue.Hidden)) { this.ViewBag.SupportFormUrl = this.Settings.SupportUrls.SupportForm; return this.View("Unavailable"); @@ -147,7 +147,7 @@ public async Task Index(int resourceReferenceId, bool? acceptSens var hasCatalogueAccess = false; if (resource.Catalogue.RestrictedAccess && this.User.Identity.IsAuthenticated) { - var userGroups = await this.userGroupService.GetRoleUserGroupDetailForUserAsync(this.CurrentUserId); + var userGroups = await this.userGroupService.GetRoleUserGroupDetailAsync(); hasCatalogueAccess = userGroups.Any(x => x.CatalogueNodeId == resource.Catalogue.NodeId && (x.RoleEnum == RoleEnum.LocalAdmin || x.RoleEnum == RoleEnum.Editor || x.RoleEnum == RoleEnum.Reader)) || this.User.IsInRole("Administrator"); @@ -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() { @@ -437,12 +437,13 @@ public async Task UnpublishConfirm(ResourceUnpublishConfirmViewMo /// View HTML resource content. /// /// Resource reference id. + /// Resource version id. /// Html resource content relative path. /// The file content. [HttpGet] [Authorize] - [Route("resource/html/{resourceReferenceId}/{*path}")] - public async Task HtmlResourceContent(int resourceReferenceId, string path) + [Route("resource/html/{resourceReferenceId}/{CurrentResourceVersionId}/{*path}")] + public async Task HtmlResourceContent(int resourceReferenceId, int currentResourceVersionId, string path) { if (resourceReferenceId == 0 || string.IsNullOrWhiteSpace(path)) { @@ -452,8 +453,14 @@ public async Task HtmlResourceContent(int resourceReferenceId, st var userId = this.User.Identity.GetCurrentUserId(); var cacheKey = $"HtmlContent:{userId}:{resourceReferenceId}"; var (cacheExists, cacheValue) = await this.cacheService.TryGetAsync(cacheKey); + var oldresourceVersionId = 0; + if (cacheExists) + { + var cachesplits = cacheValue.Split(":"); + oldresourceVersionId = int.Parse(cachesplits[0]); + } - if (!cacheExists) + if (!cacheExists || (oldresourceVersionId != currentResourceVersionId)) { var resource = await this.resourceService.GetItemByIdAsync(resourceReferenceId); diff --git a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs index d23e0881e..8d17307cf 100644 --- a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs @@ -115,6 +115,12 @@ public static string GetResourceTypeVerb(this ActivityDetailedItemViewModel acti return "Played " + GetDurationText(activityDetailedItemViewModel.ActivityDurationSeconds * 1000); case ResourceTypeEnum.WebLink: return "Visited"; + case ResourceTypeEnum.Html: + return "Viewed"; + case ResourceTypeEnum.Case: + return "Accessed"; + case ResourceTypeEnum.Assessment: + return "Accessed"; default: return string.Empty; } @@ -256,7 +262,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/Interfaces/ICatalogueService.cs b/LearningHub.Nhs.WebUI/Interfaces/ICatalogueService.cs index 30ba46a54..635eee151 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/ICatalogueService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/ICatalogueService.cs @@ -138,5 +138,13 @@ public interface ICatalogueService /// The user - user group id. /// The validation result. Task RemoveUserFromRestrictedAccessUserGroup(int userUserGroupId); + + /// + /// The GetAllCatalogueAsync. + /// + /// The letter. + /// The pageSize. + /// The allcatalogue result based on letters. + Task GetAllCatalogueAsync(string filterChar, int pageSize); } } diff --git a/LearningHub.Nhs.WebUI/Interfaces/ISearchService.cs b/LearningHub.Nhs.WebUI/Interfaces/ISearchService.cs index 4f8ed36c7..b01be5bce 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/ISearchService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/ISearchService.cs @@ -77,5 +77,12 @@ public interface ISearchService /// catalogue search request model. /// The . Task CreateCatalogueSearchTermEventAsync(CatalogueSearchRequestModel catalogueSearchRequestModel); + + /// + /// Get AllCatalogue Search Result Async. + /// + /// The catalogue Search Request Model. + /// The . + Task GetAllCatalogueSearchResultAsync(AllCatalogueSearchRequestModel catalogueSearchRequestModel); } } diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index acea5791b..9e46194b1 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -108,7 +108,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/Models/Bookmark/EditBookmarkViewModel.cs b/LearningHub.Nhs.WebUI/Models/Bookmark/EditBookmarkViewModel.cs index 8472ceb4a..18801abeb 100644 --- a/LearningHub.Nhs.WebUI/Models/Bookmark/EditBookmarkViewModel.cs +++ b/LearningHub.Nhs.WebUI/Models/Bookmark/EditBookmarkViewModel.cs @@ -30,7 +30,7 @@ public class EditBookmarkViewModel /// /// Gets or sets Title. /// - [Required(AllowEmptyStrings = false, ErrorMessage = "You must edit bookmark name")] + [Required(AllowEmptyStrings = false, ErrorMessage = "You must enter a bookmark name")] [MinLength(2, ErrorMessage = "The bookmark name must be at least 2 characters")] [MaxLength(60, ErrorMessage = "The bookmark name must be no more than 60 characters")] public string Title { get; set; } 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/Models/NavigationModel.cs b/LearningHub.Nhs.WebUI/Models/NavigationModel.cs index ca4dee523..950343bde 100644 --- a/LearningHub.Nhs.WebUI/Models/NavigationModel.cs +++ b/LearningHub.Nhs.WebUI/Models/NavigationModel.cs @@ -69,5 +69,10 @@ public class NavigationModel /// Gets or sets a value indicating whether to show my account. /// public bool ShowMyAccount { get; set; } + + /// + /// Gets or sets a value indicating whether to show Browse Catalogues. + /// + public bool ShowBrowseCatalogues { get; set; } } } diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/ckeditorwithhint.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/ckeditorwithhint.vue index ba9d1610b..80415a53d 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/ckeditorwithhint.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/ckeditorwithhint.vue @@ -23,7 +23,10 @@ }, data() { return { - editorConfig: { toolbar: CKEditorToolbar.default }, + editorConfig: { + toolbar: CKEditorToolbar.default, + versionCheck: false + }, description: this.initialValue, hint: `You have ${this.maxLength} characters remaining`, valid: getRemainingCharactersFromHtml(this.maxLength, this.initialValue) >= 0, diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/content-structure-admin/contentStructure.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/content-structure-admin/contentStructure.vue index 3e20cfaa2..c632aeff0 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/content-structure-admin/contentStructure.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/content-structure-admin/contentStructure.vue @@ -176,7 +176,10 @@ isReady: false, EditModeEnum: EditModeEnum, HierarchyEditStatusEnum: HierarchyEditStatusEnum, - editorConfig: { toolbar: CKEditorToolbar.default }, + editorConfig: { + toolbar: CKEditorToolbar.default, + versionCheck: false + }, deleteFolderName: '', editFolderStructureButtonText: '', editFolderStructureButtonDisabled: true, diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeTextBlock.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeTextBlock.vue index f548aa11e..fc49403dd 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeTextBlock.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeTextBlock.vue @@ -32,7 +32,7 @@ data() { return { textBlockContent: this.textBlock.content, - ckEditorConfig: { toolbar: CKEditorToolbar.default }, + ckEditorConfig: { toolbar: CKEditorToolbar.default, versionCheck: false }, prevHeight: undefined, }; }, diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/components/KeyWordsEditor.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/components/KeyWordsEditor.vue index 7435ee6d6..bfc00c5aa 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/components/KeyWordsEditor.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/components/KeyWordsEditor.vue @@ -1,27 +1,29 @@