diff --git a/.gitignore b/.gitignore index 4119589ad..5e8ca2030 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,7 @@ obj /WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Database/LearningHub.Nhs.Migration.Staging.Database.dbmdl /WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Database/LearningHub.Nhs.Migration.Staging.Database.jfm /LearningHub.Nhs.WebUI.AutomatedUiTests/appsettings.Development.json +/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.Development.json +/OpenAPI/LearningHub.Nhs.OpenApi/web.config +/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj.user +/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj.user 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/LearningHub.Nhs.AdminUI.csproj b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj index f222dc100..4ea6a2e6a 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/Views/Catalogue/Edit.cshtml b/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml index e0e81edcf..19ed8d399 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml +++ b/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml @@ -162,6 +162,11 @@ +
+
+ +
+
@@ -341,6 +346,9 @@ } else { $('#add-keyword').removeAttr('disabled'); } + + $('#keyword-error-span').hide(); + $('#keyword-error-span').html(''); }); $('#add-keyword').on('click', function () { @@ -352,9 +360,11 @@ // Split the input value by commas and trim each keyword var values = value.split(',').map(function (item) { - return item.trim(); + return item.trim().toLowerCase(); }); + var duplicateKeywords = []; + $('#keyword-error-span').hide(); values.forEach(function (value) { if (value && keywords.indexOf(value) === -1) { keywords.push(value); @@ -368,9 +378,15 @@ $(x).attr('name', "Keywords[" + i + "]"); }); } + else + { + duplicateKeywords.push(value); + $('#keyword-error-span').show(); + $('#keyword-error-span').html('The keyword(s) have already been added : ' + duplicateKeywords.join(', ')) + } }); - $keywordInput.val(""); + $keywordInput.val(""); if (keywords.length > 4) { $('#add-keyword').attr('disabled', 'disabled'); $('#add-keyword-input').attr('disabled', 'disabled'); @@ -441,4 +457,4 @@ -} \ No newline at end of file +} 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 0616474ff..e5e8ab35e 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs @@ -2,6 +2,7 @@ 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; @@ -565,9 +566,9 @@ public async Task DeleteResourceProviderAsync(int resourceVersionI /// A representing the asynchronous operation. [HttpPost] [Route("ArchiveResourceFile")] - public ActionResult ArchiveResourceFile(List filePaths) + public ActionResult ArchiveResourceFile(IEnumerable filePaths) { - _ = Task.Run(async () => { await this.fileService.PurgeResourceFile(null, filePaths); }); + _ = Task.Run(async () => { await this.fileService.PurgeResourceFile(null, filePaths.ToList()); }); return this.Ok(); } diff --git a/LearningHub.Nhs.WebUI/Controllers/CatalogueController.cs b/LearningHub.Nhs.WebUI/Controllers/CatalogueController.cs index fb4d3def3..477aabfbf 100644 --- a/LearningHub.Nhs.WebUI/Controllers/CatalogueController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/CatalogueController.cs @@ -110,6 +110,7 @@ public async Task Index(int pageIndex = 1, string term = null) }); catalogues.TotalCount = termCatalogues.TotalHits; + catalogues.GroupId = Guid.NewGuid(); catalogues.Catalogues = termCatalogues.DocumentModel.Select(t => new DashboardCatalogueViewModel { Url = t.Url, @@ -123,6 +124,8 @@ public async Task Index(int pageIndex = 1, string term = null) BookmarkId = t.BookmarkId, NodeId = int.Parse(t.Id), BadgeUrl = t.BadgeUrl, + Providers = t.Providers, + ClickPayload = t.Click.Payload, }).ToList(); } else @@ -551,5 +554,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/LearningSessionsController.cs b/LearningHub.Nhs.WebUI/Controllers/LearningSessionsController.cs index 4f77c00bb..755c14d62 100644 --- a/LearningHub.Nhs.WebUI/Controllers/LearningSessionsController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/LearningSessionsController.cs @@ -68,6 +68,7 @@ public async Task Scorm(int id) /// filePath. /// bool. //// [ResponseCache(VaryByQueryKeys = new[] { "*" }, Duration = 0, NoStore = true)] // disable caching + //// Removed Request.Headers["Referer"] Referer URL checking based on issue reported in TD-4283 [AllowAnonymous] [Route("ScormContent/{*filePath}")] public async Task ScormContent(string filePath) @@ -79,12 +80,6 @@ public async Task ScormContent(string filePath) try { - var referringUrl = this.Request.Headers["Referer"].ToString(); - if (string.IsNullOrEmpty(referringUrl)) - { - throw new UnauthorizedAccessException("Referer URL is required."); - } - if (!this.User.Identity.IsAuthenticated) { throw new UnauthorizedAccessException("User is not authenticated."); diff --git a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs index 2cebfd11c..a87517b97 100644 --- a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs @@ -214,8 +214,9 @@ public async Task RecordCatalogueNavigation(SearchRequestViewMode /// time of search. /// user query. /// search query. + /// the title. [HttpGet("record-resource-click")] - public void RecordResourceClick(string url, int nodePathId, int itemIndex, int pageIndex, int totalNumberOfHits, string searchText, int resourceReferenceId, Guid groupId, string searchId, long timeOfSearch, string userQuery, string query) + public void RecordResourceClick(string url, int nodePathId, int itemIndex, int pageIndex, int totalNumberOfHits, string searchText, int resourceReferenceId, Guid groupId, string searchId, long timeOfSearch, string userQuery, string query, string title) { var searchActionResourceModel = new SearchActionResourceModel { @@ -230,6 +231,7 @@ public void RecordResourceClick(string url, int nodePathId, int itemIndex, int p TimeOfSearch = timeOfSearch, UserQuery = userQuery, Query = query, + Title = title, }; this.searchService.CreateResourceSearchActionAsync(searchActionResourceModel); @@ -251,9 +253,10 @@ public void RecordResourceClick(string url, int nodePathId, int itemIndex, int p /// time of search. /// user query. /// search query. + /// the name. /// A representing the asynchronous operation. [HttpGet("record-catalogue-click")] - public async Task RecordCatalogueClick(string url, int nodePathId, int itemIndex, int pageIndex, int totalNumberOfHits, string searchText, int catalogueId, Guid groupId, string searchId, long timeOfSearch, string userQuery, string query) + public async Task RecordCatalogueClick(string url, int nodePathId, int itemIndex, int pageIndex, int totalNumberOfHits, string searchText, int catalogueId, Guid groupId, string searchId, long timeOfSearch, string userQuery, string query, string name) { SearchActionCatalogueModel searchActionCatalogueModel = new SearchActionCatalogueModel { @@ -268,6 +271,7 @@ public async Task RecordCatalogueClick(string url, int nodePathId TimeOfSearch = timeOfSearch, UserQuery = userQuery, Query = query, + Name = name, }; await this.searchService.CreateCatalogueSearchActionAsync(searchActionCatalogueModel); 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 6a9ff6ca0..78ab26256 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/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/contribute-resource/ContributeAssessmentSettings.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeAssessmentSettings.vue index 5a20df8ad..b828110a1 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeAssessmentSettings.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/ContributeAssessmentSettings.vue @@ -65,22 +65,21 @@
-
Provide guidance for the learner at the end of this assessment.
- - Provide guidance for the learner at the end of this assessment.
+ +

Tip

You can offer guidance to the learner at the end of the assessment such as next steps or recommendations on other learning resources to try.
- + @@ -132,6 +131,7 @@ endGuidance: "", initialGuidance: "", guidanceValid: true, + IsVisible: false, } }, watch: { @@ -140,7 +140,7 @@ { this.assessmentDetails.endGuidance.addBlock(BlockTypeEnum.Text); } - this.assessmentDetails.endGuidance.blocks[0].textBlock.content = this.endGuidance; + this.assessmentDetails.endGuidance.blocks[0].textBlock.content = this.endGuidance; }, ["assessmentDetails.passMark"](value){ this.assessmentDetails.passMark = this.capNumberFieldBy(value, 100)}, ["assessmentDetails.maximumAttempts"](value){ this.assessmentDetails.maximumAttempts = this.capNumberFieldBy(value, 10)}, @@ -157,6 +157,14 @@ } this.assessmentDetails.assessmentSettingsAreValid = settingsAreValid; + + if (this.endGuidance != "") { + this.IsVisible = true; + } + else { + this.IsVisible = false; + } + return settingsAreValid; }, }, @@ -170,9 +178,23 @@ { this.endGuidance = description; } + + if (this.endGuidance != "") { + this.IsVisible = true; + } + else { + this.IsVisible = false; + } }, setGuidanceValidity(valid: boolean) { - this.guidanceValid = valid; + if (this.endGuidance == "") { + this.guidanceValid = false; + this.IsVisible = false; + } + else { + this.guidanceValid = valid; + this.IsVisible = true; + } } } }); 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 bfc00c5aa..97e4ed42f 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/components/KeyWordsEditor.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/components/KeyWordsEditor.vue @@ -4,7 +4,7 @@
- This keyword has already been added. + The keyword(s) have already been added : {{formattedkeywordErrorMessage}}
@@ -60,6 +60,7 @@ newKeyword: '', keywordError: false, keywordLengthExceeded: false, + keywordErrorMessage: [] } }, computed: { @@ -69,19 +70,23 @@ newKeywordTrimmed(): string { return this.newKeyword?.trim().replace(/ +(?= )/g, '').toLowerCase(); }, + formattedkeywordErrorMessage(): string { + return this.keywordErrorMessage.join(', '); + }, }, methods: { keywordChange() { this.keywordError = false; this.keywordLengthExceeded = false; + this.keywordErrorMessage = []; }, async addKeyword() { if (this.newKeyword && this.newKeywordTrimmed.length > 0) { + this.keywordChange(); let allTrimmedKeyword = this.newKeywordTrimmed.toLowerCase().split(','); allTrimmedKeyword = allTrimmedKeyword.filter(e => String(e).trim()); - if (!this.resourceDetails.resourceKeywords.find(_keyword => allTrimmedKeyword.includes(_keyword.keyword.toLowerCase()))) { for (var i = 0; i < allTrimmedKeyword.length; i++) { - let item = allTrimmedKeyword[i]; + let item = allTrimmedKeyword[i].trim(); if (item.length > 0 && item.length <= 50) { let newKeywordObj = new KeywordModel({ keyword: item, @@ -90,8 +95,11 @@ newKeywordObj = await resourceData.addKeyword(this.resourceVersionId, newKeywordObj); if (newKeywordObj.id > 0) { this.resourceDetails.resourceKeywords.push(newKeywordObj); - this.keywordError = false; this.newKeyword = ''; + } else if (newKeywordObj.id == 0) { + this.newKeyword = ''; + this.keywordError = true; + this.keywordErrorMessage.push(item); } else { this.keywordError = true; @@ -103,10 +111,6 @@ this.keywordLengthExceeded = true; } } - } - else { - this.keywordError = true; - } } }, async deleteKeyword(keywordId: number) { diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/Content.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/Content.vue index 154d78e98..00d740bb2 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/Content.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/Content.vue @@ -822,7 +822,7 @@ this.fileUploadRef.value = null; (this.$refs.fileUploader as any).uploadResourceFile(this.file); }, - fileUploadComplete(uploadResult: FileUploadResult) { + async fileUploadComplete(uploadResult: FileUploadResult) { if (!uploadResult.invalid) { if (uploadResult.resourceType != ResourceType.SCORM) { this.$store.commit("setResourceType", uploadResult.resourceType); @@ -841,7 +841,7 @@ } if (this.filePathBeforeFileChange.length > 0) { - this.getResourceFilePath('completed'); + await 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) { @@ -1040,6 +1040,7 @@ await resourceData.getObsoleteResourceFile(resource.resourceVersionId).then(response => { if (fileChangeStatus == 'initialised') { this.filePathBeforeFileChange = response; + this.filePathAfterFileChange.length = 0; } else if (fileChangeStatus == 'completed') { this.filePathAfterFileChange = response; diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue index 3741814d3..8304843e1 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/ContentCommon.vue @@ -84,7 +84,7 @@
- This keyword has already been added. + The keyword(s) have already been added : {{formattedkeywordErrorMessage}}
@@ -292,6 +292,7 @@ ResourceType, resourceProviderId: null, keywordLengthExceeded: false, + keywordErrorMessage:[] }; }, computed: { @@ -340,6 +341,9 @@ return this.$store.state.userProviders.length > 0; } }, + formattedkeywordErrorMessage(): string { + return this.keywordErrorMessage.join(', '); + }, }, created() { this.setInitialValues(); @@ -485,6 +489,7 @@ keywordChange() { this.keywordError = false; this.keywordLengthExceeded = false; + this.keywordErrorMessage = []; }, resetSelectedLicence() { this.resourceLicenceId = 0; @@ -532,11 +537,11 @@ }, async addKeyword() { if (this.newKeyword && this.newKeywordTrimmed.length > 0) { + this.keywordChange(); let allTrimmedKeyword = this.newKeywordTrimmed.toLowerCase().split(','); allTrimmedKeyword = allTrimmedKeyword.filter(e => String(e).trim()); - if (!this.keywords.find(_keyword => allTrimmedKeyword.includes(_keyword.keyword.toLowerCase()))) { for (var i = 0; i < allTrimmedKeyword.length; i++) { - let item = allTrimmedKeyword[i]; + let item = allTrimmedKeyword[i].trim(); if (item.length > 0 && item.length <= 50) { let newkeywordObj = new KeywordModel(); newkeywordObj.keyword = item; @@ -548,22 +553,22 @@ if (this.resourceDetail.resourceVersionId == 0) { this.$store.commit('setResourceVersionId', newkeywordObj.resourceVersionId) } - this.keywordError = false; this.newKeyword = ''; - } else { + } else if (newkeywordObj.id == 0) { + this.newKeyword = ''; this.keywordError = true; - break; + this.keywordErrorMessage.push(item); } + else { + this.keywordError = true; + break; + } } else { this.keywordLengthExceeded = true; break; } } - } - else { - this.keywordError = true; - } } else { this.newKeyword = ''; diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/data/resource.ts b/LearningHub.Nhs.WebUI/Scripts/vuesrc/data/resource.ts index 9127917d3..817ffcf2f 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/data/resource.ts +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/data/resource.ts @@ -558,8 +558,8 @@ const getObsoleteResourceFile = async function (id: number): Promise { }; const archiveResourceFile = async function (filepaths: string[]): Promise { - const params = {filePaths:filepaths}; - return await AxiosWrapper.axios.post('/api/Resource/DuplicateBlocks', params).then(() => { + + return await AxiosWrapper.axios.post('/api/Resource/ArchiveResourceFile', filepaths).then(() => { return true }).catch(e => { console.log('archiveResourceFile:' + e); diff --git a/LearningHub.Nhs.WebUI/Services/CatalogueService.cs b/LearningHub.Nhs.WebUI/Services/CatalogueService.cs index fc95fe746..615363cfd 100644 --- a/LearningHub.Nhs.WebUI/Services/CatalogueService.cs +++ b/LearningHub.Nhs.WebUI/Services/CatalogueService.cs @@ -602,5 +602,33 @@ public async Task RemoveUserFromRestrictedAccessUse return apiResponse.ValidationResult; } + + /// + /// GetAllCatalogueAsync. + /// + /// The filterChar. + /// the pageSize. + /// A representing the result of the asynchronous operation. + public async Task GetAllCatalogueAsync(string filterChar, int pageSize) + { + AllCatalogueResponseViewModel viewmodel = new AllCatalogueResponseViewModel { }; + var client = await this.LearningHubHttpClient.GetClientAsync(); + + var request = $"catalogue/allcatalogues/{pageSize}/{filterChar}"; + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + viewmodel = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || + response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return viewmodel; + } } } diff --git a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs index 954b20ca7..95e74022e 100644 --- a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs +++ b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs @@ -80,6 +80,7 @@ public NavigationModel NotAuthenticated() ShowRegister = false, ShowSignOut = false, ShowMyAccount = false, + ShowBrowseCatalogues = false, }; } @@ -104,6 +105,7 @@ private NavigationModel AuthenticatedAdministrator(string controllerName) ShowRegister = false, ShowSignOut = true, ShowMyAccount = true, + ShowBrowseCatalogues = true, }; } @@ -128,6 +130,7 @@ private NavigationModel AuthenticatedBlueUser(string controllerName) ShowRegister = false, ShowSignOut = true, ShowMyAccount = true, + ShowBrowseCatalogues = true, }; } @@ -151,6 +154,7 @@ private NavigationModel AuthenticatedGuest() ShowRegister = false, ShowSignOut = true, ShowMyAccount = false, + ShowBrowseCatalogues = false, }; } @@ -175,6 +179,7 @@ private async Task AuthenticatedReadOnly(string controllerName) ShowRegister = false, ShowSignOut = true, ShowMyAccount = false, + ShowBrowseCatalogues = true, }; } @@ -198,6 +203,7 @@ private async Task AuthenticatedBasicUserOnly() ShowRegister = false, ShowSignOut = true, ShowMyAccount = true, + ShowBrowseCatalogues = true, }; } @@ -221,6 +227,7 @@ private NavigationModel InLoginWizard() ShowRegister = false, ShowSignOut = true, ShowMyAccount = false, + ShowBrowseCatalogues = false, }; } } diff --git a/LearningHub.Nhs.WebUI/Services/SearchService.cs b/LearningHub.Nhs.WebUI/Services/SearchService.cs index 3fc38d8a2..8e22e0eeb 100644 --- a/LearningHub.Nhs.WebUI/Services/SearchService.cs +++ b/LearningHub.Nhs.WebUI/Services/SearchService.cs @@ -587,6 +587,53 @@ public async Task CreateCatalogueSearchTermEventAsync(CatalogueSearchReques } } + /// + /// GetAllCatalogueSearchResultAsync. + /// + /// catalogueSearchRequestModel. + /// The . + public async Task GetAllCatalogueSearchResultAsync(AllCatalogueSearchRequestModel catalogueSearchRequestModel) + { + SearchAllCatalogueViewModel searchViewModel = new SearchAllCatalogueViewModel(); + + try + { + var client = await this.LearningHubHttpClient.GetClientAsync(); + + catalogueSearchRequestModel.SearchText = this.DecodeProblemCharacters(catalogueSearchRequestModel.SearchText); + + var json = JsonConvert.SerializeObject(catalogueSearchRequestModel); + var stringContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json"); + + var request = $"Search/GetAllCatalogueSearchResult"; + var response = await client.PostAsync(request, stringContent).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadAsStringAsync(); + searchViewModel = JsonConvert.DeserializeObject(result); + + if (searchViewModel.DocumentModel != null + && searchViewModel.DocumentModel.Count != 0) + { + searchViewModel.DocumentModel.ForEach(x => x.Description = this.RemoveHtmlTags(x.Description)); + } + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return searchViewModel; + } + catch (Exception ex) + { + searchViewModel.ErrorOnAPI = true; + this.Logger.LogError(string.Format("Error occurred in GetAllCatalogueSearchResultAsync: {0}", ex.Message)); + return searchViewModel; + } + } + /// /// The RemoveHtmlTags. /// diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/catalogue.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/catalogue.scss index 22ffd1a18..dd64e5a88 100644 --- a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/catalogue.scss +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/catalogue.scss @@ -173,4 +173,15 @@ textarea { font-weight: 700; line-height: 24px; word-wrap: break-word -} \ No newline at end of file +} +.allCatalogue-lettercard { + background: $nhsuk-blue !important; + padding: 12px 8px; + margin-left: 16px; + width: 52px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + color: $nhsuk-white !important; +} diff --git a/LearningHub.Nhs.WebUI/Views/Catalogue/AllCatalogue.cshtml b/LearningHub.Nhs.WebUI/Views/Catalogue/AllCatalogue.cshtml new file mode 100644 index 000000000..702e4277c --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Catalogue/AllCatalogue.cshtml @@ -0,0 +1,144 @@ +@using LearningHub.Nhs.WebUI.Extensions +@using Microsoft.AspNetCore.WebUtilities +@model LearningHub.Nhs.Models.Catalogue.AllCatalogueResponseViewModel; + +@{ + ViewData["Title"] = "All Catalogues"; + string cardStyle = "card-provider-details--blank"; +} + +@section styles { + + +} +
+
+

+ A-Z of catalogues +

+ +
+
+
+ @await Html.PartialAsync("_AllCatalogueSearchBar", string.Empty) +
+
+ +
+ +
+
+
+

@Model.FilterChar

+ +
+
+ +
    + + @foreach (var item in Model.Catalogues) + { +
  • + +
    + +
    + @if (!string.IsNullOrWhiteSpace(item.CardImageUrl)) + { + @item.Name + } + else if (!string.IsNullOrWhiteSpace(item.BannerUrl)) + { + @item.Name + } + else + { +
    + } +
    + @if (item.Providers?.Count > 0) + { +
    + @ProviderHelper.GetProviderString(item.Providers.FirstOrDefault().Name) +
    + } + else + { +
    + } + +
    + +

    + @item.Name +

    + +
    +
    + @Html.Raw(item.Description) +
    + +
    +
    +
    + @if (item.RestrictedAccess) + { +
    @((item.HasAccess || this.User.IsInRole("Administrator")) ? "Access Granted" : "Access restricted")
    + } +
    + +
    +
    + @if (item.Providers?.Count > 0) + { + var provider = item.Providers.First(); + @provider.Name catalogue badge + } + else if (!string.IsNullOrEmpty(item.BadgeUrl)) + { + Provider's catalogue badge + } +
    +
    +
    +
    +
    + +
  • + } +
+ + @await Html.PartialAsync("_AllCataloguePagination", Model) + +
+
\ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/Catalogue/AllCatalogueSearch.cshtml b/LearningHub.Nhs.WebUI/Views/Catalogue/AllCatalogueSearch.cshtml new file mode 100644 index 000000000..dab2977f4 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Catalogue/AllCatalogueSearch.cshtml @@ -0,0 +1,151 @@ +@using LearningHub.Nhs.WebUI.Extensions +@using Microsoft.AspNetCore.WebUtilities +@model LearningHub.Nhs.Models.Catalogue.AllCatalogueSearchResponseViewModel; + +@{ + ViewData["Title"] = "All Catalogues Search"; + + var queryParams = QueryHelpers.ParseQuery(Context.Request.QueryString.ToString()); + var hasSearchTerm = queryParams.ContainsKey("term"); + var searchTerm = hasSearchTerm ? queryParams["term"].ToString().Trim() : null; + string cardStyle = "card-provider-details--blank"; + var pageSize = this.ViewBag.PageSize; +} + +@section styles { + + +} +
+ @if (hasSearchTerm) + { + var parms = new Dictionary { { "term", searchTerm } }; + + } +

+ Search results @(!string.IsNullOrEmpty(searchTerm) ? "for " + searchTerm : string.Empty) +

+ +
+
+
+ @await Html.PartialAsync("_AllCatalogueSearchBar", searchTerm) +
+
+
+ @if (Model.TotalCount > 0) + { +

+ @($"{Model.TotalCount} catalogue results") +

+ } + +
    + + @foreach (var item in Model.Catalogues) + { +
  • + +
    + +
    + @if (!string.IsNullOrWhiteSpace(item.CardImageUrl)) + { + @item.Name + } + else if (!string.IsNullOrWhiteSpace(item.BannerUrl)) + { + @item.Name + } + else + { +
    + } +
    + @if (item.Providers?.Count > 0) + { +
    + @ProviderHelper.GetProviderString(item.Providers.FirstOrDefault().Name) +
    + } + else + { +
    + } + +
    + +

    + @item.Name +

    + +
    +
    + @Html.Raw(item.Description) +
    + +
    +
    +
    + @if (item.RestrictedAccess) + { +
    @((item.HasAccess || this.User.IsInRole("Administrator")) ? "Access Granted" : "Access restricted")
    + } +
    + +
    +
    + + @if (item.Providers?.Count > 0) + { + var provider = item.Providers.First(); + @provider.Name catalogue badge + } + else if (!string.IsNullOrEmpty(item.BadgeUrl)) + { + Provider's catalogue badge + } +
    +
    +
    +
    +
    + +
  • + } +
+ @if (Model.TotalCount > pageSize) + { + var currentPage = this.ViewBag.PageIndex; + int totalPage = (Model.TotalCount / pageSize) + (Model.TotalCount % pageSize == 0 ? 0 : 1); + var searchQueryParam = hasSearchTerm ? $"&term={searchTerm}" : string.Empty; + var prevUrl = $"/allcataloguesearch?pageindex={currentPage - 1}{searchQueryParam}"; + var nextUrl = $"/allcataloguesearch?pageindex={currentPage + 1}{searchQueryParam}"; + + @await Html.PartialAsync("_Pagination", new PaginationViewModel(currentPage, totalPage, prevUrl, nextUrl)) + } + + @if (Model.TotalCount == 0) + { +
+
+

No results found @(!string.IsNullOrEmpty(searchTerm) ? "for " + searchTerm : string.Empty)

+

You could try:

+
    +
  • checking your spelling
  • +
  • searching again using other words
  • +
+
+
+ } +
\ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/Catalogue/Catalogues.cshtml b/LearningHub.Nhs.WebUI/Views/Catalogue/Catalogues.cshtml index 6eda29434..b976651b2 100644 --- a/LearningHub.Nhs.WebUI/Views/Catalogue/Catalogues.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Catalogue/Catalogues.cshtml @@ -1,13 +1,30 @@ -@using LearningHub.Nhs.WebUI.Extensions -@using Microsoft.AspNetCore.WebUtilities +@using System.Web; +@using LearningHub.Nhs.WebUI.Extensions; +@using Microsoft.AspNetCore.WebUtilities; +@using LearningHub.Nhs.Models.Search.SearchClick; + @model LearningHub.Nhs.Models.Dashboard.DashboardCatalogueResponseViewModel; @{ - ViewData["Title"] = "Learning Hub - Catalogues"; + ViewData["Title"] = "Learning Hub - Catalogues"; + + var queryParams = QueryHelpers.ParseQuery(Context.Request.QueryString.ToString().ToLower()); + var hasSearchTerm = queryParams.ContainsKey("term"); + var searhTerm = hasSearchTerm ? queryParams["term"].ToString() : null; + string cardStyle = "card-provider-details--blank"; + + string GetCatalogueUrl(string catalogueUrl, SearchClickPayloadModel list, int catalogueId) + { + string encodedCatalogueUrl = HttpUtility.UrlEncode("/Catalogue/" + catalogueUrl); + string searchSignalQueryEncoded = HttpUtility.UrlEncode(HttpUtility.UrlDecode(list?.SearchSignal?.Query)); + string groupId = HttpUtility.UrlEncode(Model.GroupId.ToString()); + var url = $@"/search/record-catalogue-click?url={encodedCatalogueUrl}&itemIndex={list?.HitNumber} +&pageIndex={this.ViewBag.PageIndex}&totalNumberOfHits={list?.SearchSignal?.Stats.TotalHits}&searchText={searhTerm}&catalogueId={catalogueId} +&GroupId={groupId}&searchId={list?.SearchSignal.SearchId}&timeOfSearch={list?.SearchSignal.TimeOfSearch}&userQuery={HttpUtility.UrlEncode(list?.SearchSignal?.UserQuery)} +&query={searchSignalQueryEncoded}&name={list?.DocumentFields?.Name}"; + return url; + } - var queryParams = QueryHelpers.ParseQuery(Context.Request.QueryString.ToString().ToLower()); - var hasSearchTerm = queryParams.ContainsKey("term"); - var searhTerm = hasSearchTerm ? queryParams["term"].ToString() : null; } @section styles{ @@ -55,11 +72,21 @@
}
+ @if (item.Providers?.Count > 0) + { +
+ @ProviderHelper.GetProviderString(item.Providers.FirstOrDefault().Name) +
+ } + else + { +
+ }

- @item.Name + @item.Name

@@ -88,7 +115,12 @@
- @if (!string.IsNullOrEmpty(item.BadgeUrl)) + @if (item.Providers?.Count > 0) + { + var provider = item.Providers.First(); + @provider.Name catalogue badge + } + else if (!string.IsNullOrEmpty(item.BadgeUrl)) { Provider's catalogue badge } diff --git a/LearningHub.Nhs.WebUI/Views/Catalogue/_AllCataloguePagination.cshtml b/LearningHub.Nhs.WebUI/Views/Catalogue/_AllCataloguePagination.cshtml new file mode 100644 index 000000000..216facb96 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Catalogue/_AllCataloguePagination.cshtml @@ -0,0 +1,36 @@ +@model LearningHub.Nhs.Models.Catalogue.AllCatalogueResponseViewModel; + +@if (Model.PrevChar != null || Model.NextChar != null) +{ + +} diff --git a/LearningHub.Nhs.WebUI/Views/Catalogue/_AllCatalogueSearchBar.cshtml b/LearningHub.Nhs.WebUI/Views/Catalogue/_AllCatalogueSearchBar.cshtml new file mode 100644 index 000000000..c41d0ce35 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Catalogue/_AllCatalogueSearchBar.cshtml @@ -0,0 +1,13 @@ +@model string + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/Home/_CatalogueTray.cshtml b/LearningHub.Nhs.WebUI/Views/Home/_CatalogueTray.cshtml index 9b3bf931d..fd8932610 100644 --- a/LearningHub.Nhs.WebUI/Views/Home/_CatalogueTray.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Home/_CatalogueTray.cshtml @@ -73,7 +73,11 @@
-
+
+ @if (Model.Catalogues.TotalCount > 0 ) + { + View all catalogues + }
diff --git a/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml index 1d113e79c..f740f04cf 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml @@ -10,11 +10,10 @@
- -

- Search results for @Model.SearchString -

- +

+ Search results @(!string.IsNullOrEmpty(Model.SearchString) ? "for " + Model.SearchString : string.Empty) +

+
@await Html.PartialAsync("_SearchBar", @Model.SearchString) @@ -48,7 +47,7 @@ {
-

No results found for @Model.SearchString

+

No results found @(!string.IsNullOrEmpty(Model.SearchString) ? "for " + Model.SearchString : string.Empty)

You could try:

  • checking your spelling
  • diff --git a/LearningHub.Nhs.WebUI/Views/Search/_CatalogueSearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_CatalogueSearchResult.cshtml index a27a7aba0..b26b4ba70 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_CatalogueSearchResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_CatalogueSearchResult.cshtml @@ -1,24 +1,26 @@ @using System.Web; @using LearningHub.Nhs.WebUI.Extensions +@using LearningHub.Nhs.Models.Search.SearchClick; @model LearningHub.Nhs.WebUI.Models.Search.SearchResultViewModel @{ - var catalogueResult = Model.CatalogueSearchResult; - var pagingModel = Model.CatalogueResultPaging; - var searchString = HttpUtility.UrlEncode(Model.SearchString); - var searchSignal = catalogueResult.Feedback?.FeedbackAction?.Payload?.SearchSignal; - - string GetCatalogueUrl(string catalogueUrl, int nodePathId, int itemIndex, int catalogueId) - { - string encodedCatalogueUrl = HttpUtility.UrlEncode("/Catalogue/" + catalogueUrl); - string groupId = HttpUtility.UrlEncode(Model.GroupId.ToString()); - string searchSignalQueryEncoded = HttpUtility.UrlEncode(HttpUtility.UrlDecode(searchSignal?.Query)); - - var url = $@"/search/record-catalogue-click?url={encodedCatalogueUrl}&nodePathId={nodePathId}&itemIndex={itemIndex} -&pageIndex={pagingModel.CurrentPage}&totalNumberOfHits={catalogueResult.TotalHits}&searchText={searchString}&catalogueId={catalogueId} -&GroupId={groupId}&searchId={searchSignal?.SearchId}&timeOfSearch={searchSignal.TimeOfSearch}&userQuery={HttpUtility.UrlEncode(searchSignal?.UserQuery)}&query={searchSignalQueryEncoded}"; - return url; + var catalogueResult = Model.CatalogueSearchResult; + var pagingModel = Model.CatalogueResultPaging; + var searchString = HttpUtility.UrlEncode(Model.SearchString); + + string GetCatalogueUrl(string catalogueUrl, int nodePathId, int itemIndex, int catalogueId, SearchClickPayloadModel payload) + { + var searchSignal = payload?.SearchSignal; + string encodedCatalogueUrl = HttpUtility.UrlEncode("/Catalogue/" + catalogueUrl); + string groupId = HttpUtility.UrlEncode(Model.GroupId.ToString()); + string searchSignalQueryEncoded = HttpUtility.UrlEncode(HttpUtility.UrlDecode(searchSignal?.Query)); + + var url = $@"/search/record-catalogue-click?url={encodedCatalogueUrl}&nodePathId={nodePathId}&itemIndex={payload?.HitNumber} +&pageIndex={pagingModel.CurrentPage}&totalNumberOfHits={payload?.SearchSignal?.Stats?.TotalHits}&searchText={searchString}&catalogueId={catalogueId} +&GroupId={groupId}&searchId={searchSignal?.SearchId}&timeOfSearch={searchSignal?.TimeOfSearch}&userQuery={HttpUtility.UrlEncode(searchSignal?.UserQuery)} +&query={searchSignalQueryEncoded}&name={payload?.DocumentFields?.Name}"; + return url; } } @@ -44,6 +46,20 @@
  • +
    + @if (!string.IsNullOrWhiteSpace(item.CardImageUrl)) + { + @item.Name + } + else if (!string.IsNullOrWhiteSpace(item.BannerUrl)) + { + @item.Name + } + else + { +
    + } +
    @if (provider != null) {
    @@ -57,7 +73,7 @@

    - @item.Name + @item.Name

    @@ -91,7 +107,7 @@ { @provider.Name catalogue badge } - else @if (hasBadge) + else if (hasBadge) { Provider's catalogue badge } diff --git a/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml index 415e9d550..d4faffd0d 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml @@ -7,28 +7,24 @@ @using LearningHub.Nhs.Models.Search.SearchFeedback; @using LearningHub.Nhs.Models.Enums; @using LearningHub.Nhs.WebUI.Models.Search; +@using LearningHub.Nhs.Models.Search.SearchClick; @{ var resourceResult = Model.ResourceSearchResult; var pagingModel = Model.ResourceResultPaging; var index = pagingModel.CurrentPage * pagingModel.PageSize; var searchString = HttpUtility.UrlEncode(Model.SearchString); - var searchSignal = resourceResult.Feedback?.FeedbackAction?.Payload?.SearchSignal; - int qVectorIndex = searchSignal.Query?.IndexOf("q_vector") ?? -1; - var searchSignalQuery = searchSignal?.Query; - // Check if "q_vector" is found in the string. if Yes, Remove "q_vector" and everything after it - if (qVectorIndex != -1) - { - searchSignalQuery = searchSignal?.Query.Substring(0, qVectorIndex); - } - string GetUrl(int resourceReferenceId, int itemIndex, int nodePathId) + + string GetUrl(int resourceReferenceId, int itemIndex, int nodePathId, SearchClickPayloadModel payload) { + var searchSignal = payload?.SearchSignal; string groupId = HttpUtility.UrlEncode(Model.GroupId.ToString()); - string searchSignalQueryEncoded = HttpUtility.UrlEncode(HttpUtility.UrlDecode(searchSignalQuery)); + string searchSignalQueryEncoded = HttpUtility.UrlEncode(HttpUtility.UrlDecode(searchSignal?.Query)); - return $@"/search/record-resource-click?url=/Resource/{resourceReferenceId}&nodePathId={nodePathId}&itemIndex={itemIndex} -&pageIndex={pagingModel.CurrentPage}&totalNumberOfHits={resourceResult.TotalHits}&searchText={searchString}&resourceReferenceId={resourceReferenceId} -&groupId={groupId}&searchId={searchSignal?.SearchId}&timeOfSearch={searchSignal?.TimeOfSearch}&userQuery={HttpUtility.UrlEncode(searchSignal.UserQuery)}&query={searchSignalQueryEncoded}"; + return $@"/search/record-resource-click?url=/Resource/{resourceReferenceId}&nodePathId={nodePathId}&itemIndex={payload?.HitNumber} +&pageIndex={pagingModel.CurrentPage}&totalNumberOfHits={payload?.SearchSignal?.Stats?.TotalHits}&searchText={searchString}&resourceReferenceId={resourceReferenceId} +&groupId={groupId}&searchId={searchSignal?.SearchId}&timeOfSearch={searchSignal?.TimeOfSearch}&userQuery={HttpUtility.UrlEncode(searchSignal.UserQuery)} +&query={searchSignalQueryEncoded}&title={payload?.DocumentFields?.Title}"; } bool showCatalogueFieldsInResources = ViewBag.ShowCatalogueFieldsInResources == null || ViewBag.ShowCatalogueFieldsInResources == true; @@ -41,7 +37,7 @@

    - @item.Title + @item.Title

    @if (provider != null) diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml index 719fb24be..4f27477d5 100644 --- a/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml @@ -15,6 +15,15 @@ @if (Context.Request.Path.Value != "/Home/Error" && !SystemOffline()) { + @if (Model.ShowBrowseCatalogues) + { +
  • + + Browse catalogues + + +
  • + } @if (Model.ShowMyLearning) {
  • diff --git a/LearningHub.Nhs.WebUI/appsettings.json b/LearningHub.Nhs.WebUI/appsettings.json index f7d2a80cf..af3d9e063 100644 --- a/LearningHub.Nhs.WebUI/appsettings.json +++ b/LearningHub.Nhs.WebUI/appsettings.json @@ -97,7 +97,8 @@ }, "FindwiseSettings": { "ResourceSearchPageSize": 10, - "CatalogueSearchPageSize": 3 + "CatalogueSearchPageSize": 3, + "AllCatalogueSearchPageSize": 10 }, "MediaKindSettings": { "StorageAccountName": "", @@ -111,7 +112,8 @@ "MediaKindStorageConnectionString": "" }, "EnableTempDebugging": "false", - "LimitScormToAdmin": "false" + "LimitScormToAdmin": "false", + "AllCataloguePageSize": 10 }, "LearningHubAuthServiceConfig": { "Authority": "", diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj index 0047f4483..0af6830e4 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj @@ -16,7 +16,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs index 3675959a6..a4c1fbf4c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs @@ -1,7 +1,9 @@ namespace LearningHub.Nhs.OpenApi.Models.ViewModels { + using LearningHub.Nhs.Models.Entities.Activity; using System.Collections.Generic; + /// /// Class. /// @@ -23,20 +25,25 @@ public ResourceMetadataViewModel() /// . /// . /// . + /// . public ResourceMetadataViewModel( int resourceId, string title, string description, List references, string resourceType, - decimal rating) + int? majorVersion, + decimal rating, + List userSummaryActivityStatuses) { this.ResourceId = resourceId; this.Title = title; this.Description = description; this.References = references; this.ResourceType = resourceType; + this.MajorVersion = majorVersion; this.Rating = rating; + this.UserSummaryActivityStatuses = userSummaryActivityStatuses; } /// @@ -64,9 +71,20 @@ public ResourceMetadataViewModel( /// public string ResourceType { get; set; } + /// + /// Gets or sets . + /// + public int? MajorVersion { get; set; } + + /// /// Gets or sets . /// public decimal Rating { get; set; } + + /// + /// Gets or sets . + /// + public List UserSummaryActivityStatuses { get; set; } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs index cf31bdf54..41d1b197b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs @@ -1,3 +1,6 @@ +using LearningHub.Nhs.Models.Entities.Activity; +using System.Collections.Generic; + namespace LearningHub.Nhs.OpenApi.Models.ViewModels { /// @@ -14,8 +17,10 @@ public class ResourceReferenceWithResourceDetailsViewModel /// . /// . /// . + /// /// . /// . + /// public ResourceReferenceWithResourceDetailsViewModel( int resourceId, int refId, @@ -23,17 +28,21 @@ public ResourceReferenceWithResourceDetailsViewModel( string description, CatalogueViewModel catalogueViewModel, string resourceType, + int? majorVersion, decimal rating, - string link) + string link, + List userSummaryActivityStatuses) { this.ResourceId = resourceId; this.RefId = refId; this.Title = title; this.Description = description; this.Catalogue = catalogueViewModel; + this.MajorVersion = majorVersion; this.ResourceType = resourceType; this.Rating = rating; this.Link = link; + this.UserSummaryActivityStatuses = userSummaryActivityStatuses; } /// @@ -66,14 +75,27 @@ public ResourceReferenceWithResourceDetailsViewModel( /// public string ResourceType { get; } + + /// + /// Gets . + /// + public int? MajorVersion { get; } + /// /// Gets . /// + /// + public decimal Rating { get; } /// /// Gets . /// public string Link { get; } + + /// + /// Gets . + /// + public List UserSummaryActivityStatuses { get; } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs index 50eff71f6..52b3dee08 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs @@ -2,6 +2,7 @@ namespace LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories { using System.Collections.Generic; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; /// @@ -23,5 +24,19 @@ public interface IResourceRepository /// Resource references. public Task> GetResourceReferencesByOriginalResourceReferenceIds( IEnumerable originalResourceReferenceIds); + + /// + /// Gets resource activity for resourceReferenceIds and userIds. + /// + /// . + /// + /// ResourceActivityDTO. + Task> GetResourceActivityPerResourceMajorVersion(IEnumerable? resourceReferenceIds, IEnumerable? userIds); + + /// + /// GetAchievedCertificatedResourceIds + /// + /// . + public Task> GetAchievedCertificatedResourceIds(int currentUserId); } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs index 020a4f1f5..c1ebf721b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs @@ -295,6 +295,11 @@ public LearningHubDbContext(LearningHubDbContextOptions options) /// public virtual DbSet FileChunkDetail { get; set; } + /// + /// Gets or sets the ResourceActivityDto. These are not entities. They are returned from the [activity].[GetResourceActivityPerResourceMajorVersion] stored proc.. + /// + public virtual DbSet ResourceActivityDTO { get; set; } + /// /// Gets or sets the RecentlyAddedResources. These are not entities. They are returned from the [resources].[GetRecentlyAddedResources] stored proc.. /// @@ -312,14 +317,14 @@ public LearningHubDbContext(LearningHubDbContextOptions options) public virtual DbSet DashboardResourceDto { get; set; } /// - /// Gets or sets the ScormContentDetailsViewModel. + /// Gets or sets the ExternalContentDetailsViewModel. /// - public virtual DbSet ScormContentDetailsViewModel { get; set; } + public virtual DbSet ExternalContentDetailsViewModel { get; set; } /// - /// Gets or sets the ScormContentServerViewModel. + /// Gets or sets the ContentServerViewModel. /// - public virtual DbSet ScormContentServerViewModel { get; set; } + public virtual DbSet ContentServerViewModel { get; set; } /// /// Gets or sets the DashboardCatalogueDto @@ -520,12 +525,12 @@ public LearningHubDbContext(LearningHubDbContextOptions options) /// /// Gets or sets the whole slide image annotation. /// - public virtual DbSet WholeSlideImageAnnotation { get; set; } + public virtual DbSet ImageAnnotation { get; set; } /// /// Gets or sets the whole slide image annotation mark. /// - public virtual DbSet WholeSlideImageAnnotationMark { get; set; } + public virtual DbSet ImageAnnotationMark { get; set; } /// /// Gets or sets the media block. diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs index 668d318b1..8db1cd876 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs @@ -110,8 +110,8 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs index 1bdbb69c8..42d2e309f 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs @@ -17,8 +17,6 @@ protected override void InternalMap(EntityTypeBuilder entity) { entity.ToTable("PageSectionDetail", "content"); - entity.Property(e => e.AssetPositionId).HasDefaultValueSql("((2))"); - entity.Property(e => e.BackgroundColour).HasMaxLength(20); entity.Property(e => e.Description).HasMaxLength(512); @@ -31,7 +29,7 @@ protected override void InternalMap(EntityTypeBuilder entity) entity.Property(e => e.TextColour).HasMaxLength(20); - entity.Property(e => e.Title).HasMaxLength(128); + entity.Property(e => e.SectionTitle).HasMaxLength(128); entity.Property(e => e.DeletePending).IsRequired(false); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs index 62ebffd0c..0b8bb7428 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs @@ -68,10 +68,6 @@ protected void InternalMap(EntityTypeBuilder modelBuilder) modelBuilder.Property(e => e.UserId) .HasColumnName("UserId"); - modelBuilder.HasOne(d => d.User) - .WithMany(p => p.Logs) - .HasForeignKey(d => d.UserId) - .OnDelete(DeleteBehavior.ClientSetNull); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs index 18a94369a..00f7f46cb 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs @@ -7,7 +7,7 @@ /// /// The whole slide image annotation map. /// - public class WholeSlideImageAnnotationMap : BaseEntityMap + public class ImageAnnotationMap : BaseEntityMap { /// /// The internal map. @@ -15,15 +15,15 @@ public class WholeSlideImageAnnotationMap : BaseEntityMap /// The model builder. /// - protected override void InternalMap(EntityTypeBuilder modelBuilder) + protected override void InternalMap(EntityTypeBuilder modelBuilder) { - modelBuilder.ToTable("WholeSlideImageAnnotation", "resources"); + modelBuilder.ToTable("ImageAnnotation", "resources"); modelBuilder.HasOne(a => a.WholeSlideImage) - .WithMany(i => i.WholeSlideImageAnnotations) + .WithMany(i => i.ImageAnnotations) .HasForeignKey(a => a.WholeSlideImageId) .OnDelete(DeleteBehavior.Cascade) - .HasConstraintName("FK_WholeSlideImageAnnotation_WholeSlideImageId"); + .HasConstraintName("FK_ImageAnnotation_WholeSlideImageId"); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs index 2720b23db..9db0a6db6 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs @@ -7,7 +7,7 @@ /// /// The whole slide image annotation map. /// - public class WholeSlideImageAnnotationMarkMap : BaseEntityMap + public class ImageAnnotationMarkMap : BaseEntityMap { /// /// The internal map. @@ -15,15 +15,15 @@ public class WholeSlideImageAnnotationMarkMap : BaseEntityMap /// The model builder. /// - protected override void InternalMap(EntityTypeBuilder modelBuilder) + protected override void InternalMap(EntityTypeBuilder modelBuilder) { - modelBuilder.ToTable("WholeSlideImageAnnotationMark", "resources"); + modelBuilder.ToTable("ImageAnnotationMark", "resources"); - modelBuilder.HasOne(a => a.WholeSlideImageAnnotation) - .WithMany(i => i.WholeSlideImageAnnotationMarks) - .HasForeignKey(a => a.WholeSlideImageAnnotationId) + modelBuilder.HasOne(a => a.ImageAnnotation) + .WithMany(i => i.ImageAnnotationMarks) + .HasForeignKey(a => a.ImageAnnotationId) .OnDelete(DeleteBehavior.Cascade) - .HasConstraintName("FK_WholeSlideImageAnnotationMark_WholeSlideImageAnnotationId"); + .HasConstraintName("FK_ImageAnnotationMark_ImageAnnotationId"); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs index a87bee808..d96c3b4fe 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs @@ -39,6 +39,9 @@ protected override void InternalMap(EntityTypeBuilder modelBuil .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_ResourceVersion_Resource"); + modelBuilder.Property(e => e.ResourceAccessibilityEnum).HasColumnName("ResourceAccessibilityId") + .HasConversion(); + modelBuilder.Property(e => e.VersionStatusEnum).HasColumnName("VersionStatusId") .HasConversion(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs index a826c627e..79b7a239b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs @@ -1,11 +1,18 @@ namespace LearningHub.Nhs.OpenApi.Repositories.Repositories { + using System; using System.Collections.Generic; + using System.ComponentModel; + using System.Data; using System.Linq; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Dashboard; + using LearningHub.Nhs.Models.Entities; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.OpenApi.Repositories.EntityFramework; using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; + using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; /// @@ -69,5 +76,48 @@ public async Task> GetResourceReferencesByOrigina .ThenInclude(r => r.ResourceVersionRatingSummary) .ToListAsync(); } + + /// + public async Task> GetAchievedCertificatedResourceIds(int currentUserId) + { + // Use dashboard logic to ensure same resources determined has having achieved certificates + var param0 = new SqlParameter("@userId", SqlDbType.Int) { Value = currentUserId }; + var param4 = new SqlParameter("@TotalRecords", SqlDbType.Int) { Direction = ParameterDirection.Output }; + + var result = this.dbContext.DashboardResourceDto.FromSqlRaw("resources.GetAchievedCertificatedResourcesWithOptionalPagination @userId = @userId, @TotalRecords = @TotalRecords output", param0, param4).ToList(); + List achievedCertificatedResourceIds = result.Select(drd => drd.ResourceId).Distinct().ToList(); + + return achievedCertificatedResourceIds; + } + + /// + /// + /// + /// . + /// A representing the result of the asynchronous operation. + public async Task> GetResourceActivityPerResourceMajorVersion( + IEnumerable? resourceIds, IEnumerable? userIds) + { + var resourceIdsParam = resourceIds != null + ? string.Join(",", resourceIds) + : null; + + var userIdsParam = userIds != null + ? string.Join(",", userIds) + : null; + + var resourceIdsParameter = new SqlParameter("@p0", resourceIdsParam ?? (object)DBNull.Value); + var userIdsParameter = new SqlParameter("@p1", userIdsParam ?? (object)DBNull.Value); + + List resourceActivityDTOs = await dbContext.ResourceActivityDTO + .FromSqlRaw( + "[activity].[GetResourceActivityPerResourceMajorVersion] @p0, @p1", + resourceIdsParameter, + userIdsParameter) + .AsNoTracking() + .ToListAsync(); + + return resourceActivityDTOs; + } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj index 6372c89b6..55ce79f06 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj @@ -16,7 +16,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs index 8b2e46dee..f5f59cb1d 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs @@ -10,18 +10,35 @@ namespace LearningHub.Nhs.OpenApi.Services.Interface.Services /// public interface IResourceService { + /// + /// The get resource by activityStatusIds async. + /// + /// activityStatusIds. + /// c. + /// The the resourceMetaDataViewModel corresponding to the resource reference. + Task> GetResourceReferenceByActivityStatus(List activityStatusIds, int currentUserId); + /// /// The get resource by id async. /// /// The original resource reference id. + /// . /// The the resourceMetaDataViewModel corresponding to the resource reference. - Task GetResourceReferenceByOriginalId(int originalResourceReferenceId); + Task GetResourceReferenceByOriginalId(int originalResourceReferenceId, int? currentUserId); + + /// + /// The get resource references for certificates + /// + /// currentUserId. + /// The ResourceReferenceWithResourceDetailsViewModelthe resourceMetaDataViewModel corresponding to the resource reference. + Task> GetResourceReferencesForCertificates(int currentUserId); /// /// The get resources by Ids endpoint. /// /// The original resource reference Ids. + /// . /// The resourceReferenceMetaDataViewModel. - Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds); + Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds, int? currentUserId); } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs index 63155c5eb..c9b6e3031 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs @@ -14,6 +14,6 @@ public interface ISearchService /// /// . /// . - Task Search(ResourceSearchRequest query); + Task Search(ResourceSearchRequest query, int? currentUserId); } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj index 2207dfdd4..dece11f1c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -24,6 +24,7 @@ + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs index 1e3f4e563..4848cc1e6 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs @@ -2,10 +2,14 @@ namespace LearningHub.Nhs.OpenApi.Services.Services { using System; using System.Collections.Generic; + using System.Data; using System.Linq; using System.Net; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; + using LearningHub.Nhs.Models.Enums; + using LearningHub.Nhs.Models.ViewModels.Helpers; using LearningHub.Nhs.OpenApi.Models.Exceptions; using LearningHub.Nhs.OpenApi.Models.ViewModels; using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; @@ -47,9 +51,11 @@ public ResourceService(ILearningHubService learningHubService, IResourceReposito /// the get by id async. /// /// the id. + /// . /// the resource. - public async Task GetResourceReferenceByOriginalId(int originalResourceReferenceId) + public async Task GetResourceReferenceByOriginalId(int originalResourceReferenceId, int? currentUserId) { + List resourceActivities = new List() { }; var list = new List() { originalResourceReferenceId }; var resourceReferences = await this.resourceRepository.GetResourceReferencesByOriginalResourceReferenceIds(list); @@ -64,7 +70,15 @@ public async Task GetResourceRefe throw new HttpResponseException("No matching resource reference", HttpStatusCode.NotFound); } - return this.GetResourceReferenceWithResourceDetailsViewModel(resourceReference); + if (currentUserId.HasValue) + { + List resourceIds = new List() { resourceReference.ResourceId }; + List userIds = new List() { currentUserId.Value }; + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(resourceIds, userIds))?.ToList() ?? new List() { }; + } + + return this.GetResourceReferenceWithResourceDetailsViewModel(resourceReference, resourceActivities); } catch (InvalidOperationException exception) { @@ -78,8 +92,11 @@ public async Task GetResourceRefe /// /// the resource reference ids. /// the resource. - public async Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds) + public async Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds, int? currentUserId) { + List resourceActivities = new List() { }; + List majorVersionIdActivityStatusDescription = new List() { }; + var resourceReferences = await this.resourceRepository.GetResourceReferencesByOriginalResourceReferenceIds(originalResourceReferenceIds); var resourceReferencesList = resourceReferences.ToList(); var matchedIds = resourceReferencesList.Select(r => r.OriginalResourceReferenceId).ToList(); @@ -95,18 +112,85 @@ public async Task GetResourceReferencesByOrigina this.logger.LogWarning($"Multiple resource references found with OriginalResourceReferenceId {duplicateIds.First()}"); } - var matchedResources = resourceReferencesList - .Select(this.GetResourceReferenceWithResourceDetailsViewModel) - .ToList(); + if (currentUserId.HasValue) + { + List resourceIds = resourceReferencesList.Select(rrl => rrl.ResourceId).ToList(); + List userIds = new List() { currentUserId.Value }; + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(resourceIds, userIds))?.ToList() ?? new List() { }; + } + + List matchedResources = resourceReferencesList + .Select(rr => this.GetResourceReferenceWithResourceDetailsViewModel(rr, resourceActivities.Where(ra => ra.ResourceId == rr.ResourceId).ToList())) + .ToList(); return new BulkResourceReferenceViewModel(matchedResources, unmatchedIds); } - private ResourceReferenceWithResourceDetailsViewModel GetResourceReferenceWithResourceDetailsViewModel(ResourceReference resourceReference) + + /// + /// the get by id async. + /// + /// . + /// c. + /// list resource ViewModel. + public async Task> GetResourceReferenceByActivityStatus(List activityStatusIds, int currentUserId) + { + List resourceActivities = new List() { }; + List resourceReferenceWithResourceDetailsViewModelLS = new List() { }; + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(new List(){ }, new List(){ currentUserId }))?.ToList() ?? new List() { }; + + // Removing resources that have no major versions with the required activitystatus + List resourceIds = resourceActivities + .GroupBy(ra => ra.ResourceId) + .Where(group => group.Any(g => activityStatusIds.Contains(g.ActivityStatusId))) + .Select(group => group.Key) + .Distinct() + .ToList(); + + var resourceReferencesList = (await this.resourceRepository.GetResourcesFromIds(resourceIds)).SelectMany(r => r.ResourceReference).ToList(); + + resourceReferenceWithResourceDetailsViewModelLS = resourceReferencesList.Select(rr => this.GetResourceReferenceWithResourceDetailsViewModel(rr, resourceActivities)).ToList(); + + return resourceReferenceWithResourceDetailsViewModelLS; + } + + /// + /// Gets ResourceReferences ForCertificates using the ResourceReferenceWithResourceDetailsViewModel . + /// + /// user Id. + /// list resource reference ViewModel. + public async Task> GetResourceReferencesForCertificates(int currentUserId) + { + + List resourceActivities = new List() { }; + List resourceReferenceWithResourceDetailsViewModelLS = new List() { }; + List achievedCertificatedResourceIds = (await this.resourceRepository.GetAchievedCertificatedResourceIds(currentUserId)).ToList(); + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(achievedCertificatedResourceIds, new List() { currentUserId }))?.ToList() ?? new List() { }; + + var resourceList = (await this.resourceRepository.GetResourcesFromIds(achievedCertificatedResourceIds)).ToList(); + + resourceReferenceWithResourceDetailsViewModelLS = resourceList.SelectMany(r => r.ResourceReference) + .Distinct() + .Select(rr => this.GetResourceReferenceWithResourceDetailsViewModel(rr, resourceActivities)).ToList(); + + return resourceReferenceWithResourceDetailsViewModelLS; + } + + private ResourceReferenceWithResourceDetailsViewModel GetResourceReferenceWithResourceDetailsViewModel(ResourceReference resourceReference, List resourceActivities) { var hasCurrentResourceVersion = resourceReference.Resource.CurrentResourceVersion != null; var hasRating = resourceReference.Resource.CurrentResourceVersion?.ResourceVersionRatingSummary != null; + List majorVersionIdActivityStatusDescription = new List() { }; + + if (resourceActivities != null && resourceActivities.Count != 0) + { + majorVersionIdActivityStatusDescription = ActivityStatusHelper.GetMajorVersionIdActivityStatusDescriptionLSPerResource(resourceReference.Resource, resourceActivities).ToList(); + } + if (resourceReference.Resource == null) { throw new Exception("No matching resource"); @@ -135,8 +219,10 @@ private ResourceReferenceWithResourceDetailsViewModel GetResourceReferenceWithRe resourceReference.Resource.CurrentResourceVersion?.Description ?? string.Empty, resourceReference.GetCatalogue(), resourceTypeNameOrEmpty, + resourceReference.Resource?.CurrentResourceVersion?.MajorVersion ?? 0, resourceReference.Resource?.CurrentResourceVersion?.ResourceVersionRatingSummary?.AverageRating ?? 0, - this.learningHubService.GetResourceLaunchUrl(resourceReference.OriginalResourceReferenceId)); + this.learningHubService.GetResourceLaunchUrl(resourceReference.OriginalResourceReferenceId), + majorVersionIdActivityStatusDescription); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs index 656a8cb14..c5ede76fb 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs @@ -3,8 +3,11 @@ namespace LearningHub.Nhs.OpenApi.Services.Services using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; + using LearningHub.Nhs.Models.Resource; using LearningHub.Nhs.Models.Search; + using LearningHub.Nhs.Models.ViewModels.Helpers; using LearningHub.Nhs.OpenApi.Models.ServiceModels.Findwise; using LearningHub.Nhs.OpenApi.Models.ServiceModels.Resource; using LearningHub.Nhs.OpenApi.Models.ViewModels; @@ -57,7 +60,7 @@ public SearchService( } /// - public async Task Search(ResourceSearchRequest query) + public async Task Search(ResourceSearchRequest query, int? currentUserId) { var findwiseResultModel = await this.findwiseClient.Search(query); @@ -66,7 +69,7 @@ public async Task Search(ResourceSearchRequest query) return ResourceSearchResultModel.FailedWithStatus(findwiseResultModel.FindwiseRequestStatus); } - var resourceMetadataViewModels = await this.GetResourceMetadataViewModels(findwiseResultModel); + var resourceMetadataViewModels = await this.GetResourceMetadataViewModels(findwiseResultModel, currentUserId); var totalHits = findwiseResultModel.SearchResults?.Stats.TotalHits; @@ -77,8 +80,9 @@ public async Task Search(ResourceSearchRequest query) } private async Task> GetResourceMetadataViewModels( - FindwiseResultModel findwiseResultModel) + FindwiseResultModel findwiseResultModel, int? currentUserId) { + List resourceActivities = new List() { }; var documentsFound = findwiseResultModel.SearchResults?.DocumentList.Documents?.ToList() ?? new List(); var findwiseResourceIds = documentsFound.Select(d => int.Parse(d.Id)).ToList(); @@ -90,7 +94,7 @@ private async Task> GetResourceMetadataViewModel var resourcesFound = await this.resourceRepository.GetResourcesFromIds(findwiseResourceIds); - var resourceMetadataViewModels = resourcesFound.Select(this.MapToViewModel) + List resourceMetadataViewModels = resourcesFound.Select(resource => MapToViewModel(resource, resourceActivities.Where(x => x.ResourceId == resource.Id).ToList())) .OrderBySequence(findwiseResourceIds) .ToList(); @@ -105,14 +109,29 @@ private async Task> GetResourceMetadataViewModel unmatchedResourcesIdsString); } + if (currentUserId.HasValue) + { + List resourceIds = resourcesFound.Select(x => x.Id).ToList(); + List userIds = new List() { currentUserId.Value }; + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(resourceIds, userIds))?.ToList() ?? new List() { }; + } return resourceMetadataViewModels; } - private ResourceMetadataViewModel MapToViewModel(Resource resource) + private ResourceMetadataViewModel MapToViewModel(Resource resource, List resourceActivities) { var hasCurrentResourceVersion = resource.CurrentResourceVersion != null; var hasRating = resource.CurrentResourceVersion?.ResourceVersionRatingSummary != null; + List majorVersionIdActivityStatusDescription = new List() { }; + + if (resourceActivities != null && resourceActivities.Count != 0) + { + majorVersionIdActivityStatusDescription = ActivityStatusHelper.GetMajorVersionIdActivityStatusDescriptionLSPerResource(resource, resourceActivities) + .ToList(); + } + if (!hasCurrentResourceVersion) { this.logger.LogInformation( @@ -131,13 +150,17 @@ private ResourceMetadataViewModel MapToViewModel(Resource resource) this.logger.LogError($"Resource has unrecognised type: {resource.ResourceTypeEnum}"); } + return new ResourceMetadataViewModel( resource.Id, resource.CurrentResourceVersion?.Title ?? ResourceHelpers.NoResourceVersionText, resource.CurrentResourceVersion?.Description ?? string.Empty, resource.ResourceReference.Select(this.GetResourceReferenceViewModel).ToList(), resourceTypeNameOrEmpty, - resource.CurrentResourceVersion?.ResourceVersionRatingSummary?.AverageRating ?? 0.0m); + resource.CurrentResourceVersion?.MajorVersion ?? 0, + resource.CurrentResourceVersion?.ResourceVersionRatingSummary?.AverageRating ?? 0.0m, + majorVersionIdActivityStatusDescription + ); } private ResourceReferenceViewModel GetResourceReferenceViewModel( diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs index 49f901c72..d0126a840 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs @@ -18,8 +18,12 @@ namespace LearningHub.Nhs.OpenApi.Tests.Controllers using Moq; using Newtonsoft.Json; using Xunit; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using System.Security.Claims; + using LearningHub.Nhs.Models.Enums; - public sealed class ResourceControllerTests : IDisposable + public sealed class ResourceControllerTests { private readonly Mock searchService; private readonly Mock resourceService; @@ -87,6 +91,7 @@ await Assert.ThrowsAsync( public async Task SearchEndpointUsesDefaultLimitGivenInConfig() { // Given + int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth this.GivenSearchServiceSucceedsButFindsNoItems(); this.GivenDefaultLimitForFindwiseSearchIs(12); this.resourceController = new ResourceController( @@ -99,7 +104,7 @@ public async Task SearchEndpointUsesDefaultLimitGivenInConfig() // Then this.searchService.Verify( - service => service.Search(It.Is(request => request.Limit == 12))); + service => service.Search(It.Is(request => request.Limit == 12), currentUserId)); } [Fact] @@ -177,6 +182,41 @@ await Assert.ThrowsAsync( exception.StatusCode.Should().Be(HttpStatusCode.BadRequest); } + [Fact] + public void CurrentUserIdSetByAuth() + { + // Arrange + ResourceController resourceController = new ResourceController( + this.searchService.Object, + this.resourceService.Object, + this.findwiseConfigOptions.Object + ); + + + // This Id is the development accountId + int currentUserId = 57541; + + // Create claims identity with the specified user id + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, currentUserId.ToString()), + }; + var identity = new ClaimsIdentity(claims, "AuthenticationTypes.Federation"); // Set the authentication type to "Federation" + + // Create claims principal with the claims identity + var claimsPrincipal = new ClaimsPrincipal(identity); + + // Create a mock HttpContext and set it to the ControllerContext + var httpContext = new DefaultHttpContext { User = claimsPrincipal }; + var controllerContext = new ControllerContext { HttpContext = httpContext }; + resourceController.ControllerContext = controllerContext; + + // Act + + // Assert that the CurrentUserId property of the resourceController matches the currentUserId + Assert.Equal(currentUserId, resourceController.CurrentUserId); + } + [Theory] [InlineData(1)] [InlineData(20)] @@ -184,6 +224,7 @@ await Assert.ThrowsAsync( public async Task SearchEndpointUsesPassedInLimitIfGiven(int limit) { // Given + int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth this.GivenSearchServiceSucceedsButFindsNoItems(); this.GivenDefaultLimitForFindwiseSearchIs(20); this.resourceController = new ResourceController( @@ -196,12 +237,46 @@ public async Task SearchEndpointUsesPassedInLimitIfGiven(int limit) // Then this.searchService.Verify( - service => service.Search(It.Is(request => request.Limit == limit))); + service => service.Search(It.Is(request => request.Limit == limit), currentUserId)); + } + + [Fact] + public async Task GetResourceReferencesByCompleteThrowsErrorWhenNoUserId() + { + // When + var exception = await Assert.ThrowsAsync(async () => + { + await this.resourceController.GetResourceReferencesByActivityStatus((int)ActivityStatusEnum.Completed); + }); + + // Then + Assert.Equal("User Id required.", exception.Message); + } + + [Fact] + public async Task GetResourceReferencesByInProgressThrowsErrorWhenNoUserId() + { + // When + var exception = await Assert.ThrowsAsync(async () => + { + await this.resourceController.GetResourceReferencesByActivityStatus((int)ActivityStatusEnum.Incomplete);// in complete in db is in progress front endS + }); + + // Then + Assert.Equal("User Id required.", exception.Message); } - public void Dispose() + [Fact] + public async Task GetResourceReferencesBycertificatesThrowsErrorWhenNoUserId() { - this.resourceController?.Dispose(); + // When + var exception = await Assert.ThrowsAsync(async () => + { + await this.resourceController.GetResourceReferencesByCertificates(); + }); + + // Then + Assert.Equal("User Id required.", exception.Message); } private void GivenDefaultLimitForFindwiseSearchIs(int limit) @@ -212,14 +287,17 @@ private void GivenDefaultLimitForFindwiseSearchIs(int limit) private void GivenSearchServiceFailsWithStatus(FindwiseRequestStatus status) { - this.searchService.Setup(ss => ss.Search(It.IsAny())).ReturnsAsync( + int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth + this.searchService.Setup(ss => ss.Search(It.IsAny(), currentUserId)).ReturnsAsync( new ResourceSearchResultModel(new List(), status, 0)); } private void GivenSearchServiceSucceedsButFindsNoItems() { - this.searchService.Setup(ss => ss.Search(It.IsAny())).ReturnsAsync( + int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth + this.searchService.Setup(ss => ss.Search(It.IsAny(), currentUserId)).ReturnsAsync( new ResourceSearchResultModel(new List(), FindwiseRequestStatus.Success, 0)); } + } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs index d6ae8f3db..ddddc6ebc 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs @@ -2,10 +2,12 @@ namespace LearningHub.Nhs.OpenApi.Tests.Services.Services { using System; using System.Collections.Generic; + using System.Linq; using System.Net; using System.Threading.Tasks; using FizzWare.NBuilder; using FluentAssertions; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.OpenApi.Models.Exceptions; @@ -22,14 +24,35 @@ public class ResourceServiceTests private readonly Mock learningHubService; private readonly ResourceService resourceService; private readonly Mock resourceRepository; + private readonly int currentUserId; public ResourceServiceTests() { + // This Id is the development accountId + this.currentUserId = 57541; + this.learningHubService = new Mock(); this.resourceRepository = new Mock(); this.resourceService = new ResourceService(this.learningHubService.Object, this.resourceRepository.Object, new NullLogger()); } + private List ResourceActivityDTOList => new List() + { + new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 5, MajorVersion = 5 }, + new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 7, MajorVersion = 4 }, + new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 3, MajorVersion = 3 }, + new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 7, MajorVersion = 2 }, + new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 3, MajorVersion = 1 }, + + new ResourceActivityDTO{ ResourceId = 2, ActivityStatusId = 5, MajorVersion = 5 }, // Passed + new ResourceActivityDTO{ ResourceId = 2, ActivityStatusId = 4, MajorVersion = 4 }, // Failed + new ResourceActivityDTO{ ResourceId = 2, ActivityStatusId = 3, MajorVersion = 3 }, // complete + + new ResourceActivityDTO{ ResourceId = 3, ActivityStatusId = 4, MajorVersion = 2 }, // Failed + new ResourceActivityDTO{ ResourceId = 3, ActivityStatusId = 4, MajorVersion = 1 }, // Failed + new ResourceActivityDTO{ ResourceId = 3, ActivityStatusId = 7, MajorVersion = 4 }, // In complete + }; + private List ResourceList => new List() { ResourceTestHelper.CreateResourceWithDetails(id: 1, title: "title1", description: "description1", rating: 3m, resourceType: ResourceTypeEnum.Article), @@ -63,7 +86,7 @@ public async Task SingleResourceEndpointReturnsTheCorrectInformationIfThereIsAMa .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(1); + var x = await this.resourceService.GetResourceReferenceByOriginalId(1, null); // Then x.Rating.Should().Be(3); @@ -80,7 +103,7 @@ public async Task SingleResourceReturnsA404IfTheresNoResourceReferenceWithAMatch .ReturnsAsync(new List()); // When / Then - var exception = await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(999)); + var exception = await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(999, null)); exception.StatusCode.Should().Be(HttpStatusCode.NotFound); exception.ResponseBody.Should().Be("No matching resource reference"); } @@ -93,7 +116,7 @@ public async Task SingleResourceEndpointReturnsAResourceMetadataViewModelObjectW .ReturnsAsync(this.ResourceReferenceList.GetRange(1, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(2); + var x = await this.resourceService.GetResourceReferenceByOriginalId(2, null); // Then x.Title.Should().Be("No current resource version"); @@ -108,7 +131,7 @@ public async Task SingleResourceEndpointReturnsAMessageSayingNoCatalogueIfThereI .ReturnsAsync(this.ResourceReferenceList.GetRange(2, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(3); + var x = await this.resourceService.GetResourceReferenceByOriginalId(3, null); // Then x.Catalogue.Name.Should().Be("No catalogue for resource reference"); @@ -122,7 +145,7 @@ public async Task SingleResourceEndpointReturnsAMessageSayingNoCatalogueIfThereI .ReturnsAsync(this.ResourceReferenceList.GetRange(3, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(4); + var x = await this.resourceService.GetResourceReferenceByOriginalId(4, null); // Then x.Catalogue.Name.Should().Be("No catalogue for resource reference"); @@ -136,7 +159,7 @@ public async Task SingleResourceEndpointReturnsAMessageSayingNoCatalogueIfThereI .ReturnsAsync(this.ResourceReferenceList.GetRange(5, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(6); + var x = await this.resourceService.GetResourceReferenceByOriginalId(6, null); // Then x.Catalogue.Name.Should().Be("No catalogue for resource reference"); @@ -150,7 +173,7 @@ public async Task SingleResourceEndpointReturnsAZeroForRatingIfTheresNoRatingSum .ReturnsAsync(this.ResourceReferenceList.GetRange(7, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(8); + var x = await this.resourceService.GetResourceReferenceByOriginalId(8, null); // Then x.Catalogue.Name.Should().Be("catalogue3"); @@ -165,7 +188,7 @@ public async Task SingleResourceEndpointThrowsAnErrorAndReturnsABlankStringIfThe .ReturnsAsync(this.ResourceReferenceList.GetRange(8, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(9); + var x = await this.resourceService.GetResourceReferenceByOriginalId(9, null); // Then x.ResourceType.Should().Be(string.Empty); @@ -179,7 +202,7 @@ public async Task SingleResourceEndpointThrowsAnErrorIfThereIsMoreThanOneResourc .ReturnsAsync(this.ResourceReferenceList.GetRange(9, 2)); // When / Then - await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(10)); + await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(10, null)); } /*[Fact] @@ -198,7 +221,7 @@ public async Task BulkEndpointReturnsAllMatchingResources() .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 2)); // When - var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp); + var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null); // Then x.ResourceReferences.Count.Should().Be(2); @@ -220,7 +243,7 @@ public async Task BulkEndpointReturnsA404IfThereAreNoMatchingResources() .ReturnsAsync(new List()); // When - var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp); + var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null); // Then x.UnmatchedResourceReferenceIds.Count.Should().Be(2); @@ -237,7 +260,7 @@ public async Task BulkEndpointReturnsResourcesWithIncompleteInformation() .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 4)); // When - var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp); + var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null); // Then x.ResourceReferences.Count.Should().Be(4); @@ -257,7 +280,7 @@ public async Task BulkEndpointReturnsUnmatchedResourcesWithMatchedResources() .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1)); // When - var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp); + var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null); // Then x.ResourceReferences.Count.Should().Be(1); @@ -277,7 +300,7 @@ public async Task ResourceServiceReturnsTheOriginalResourceReferenceIdAsTheRefId .ReturnsAsync(this.ResourceReferenceList.GetRange(5, 2)); // When - var x = await this.resourceService.GetResourceReferencesByOriginalIds(list); + var x = await this.resourceService.GetResourceReferencesByOriginalIds(list, null); // Then x.ResourceReferences[0].RefId.Should().Be(6); @@ -292,12 +315,12 @@ public async Task ResourceServiceReturnsTheOriginalResourceReferenceIdAsTheRefId this.resourceRepository.Setup(rr => rr.GetResourceReferencesByOriginalResourceReferenceIds(list)) .ReturnsAsync(this.ResourceReferenceList.GetRange(5, 1)); - // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(6); + // When + var x = await this.resourceService.GetResourceReferenceByOriginalId(6, null); - // Then + // Then x.RefId.Should().Be(6); - } + } [Fact] public async Task ResourceServiceReturnsThatARestrictedCatalogueIsRestricted() @@ -308,7 +331,7 @@ public async Task ResourceServiceReturnsThatARestrictedCatalogueIsRestricted() .ReturnsAsync(this.ResourceReferenceList.GetRange(8, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(9); + var x = await this.resourceService.GetResourceReferenceByOriginalId(9, null); // Then x.Catalogue.IsRestricted.Should().BeTrue(); @@ -323,10 +346,192 @@ public async Task ResourceServiceReturnsThatAnUnrestrictedCatalogueIsUnrestricte .ReturnsAsync(this.ResourceReferenceList.GetRange(7, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(8); + var x = await this.resourceService.GetResourceReferenceByOriginalId(8, null); // Then x.Catalogue.IsRestricted.Should().BeFalse(); } + + [Fact] + public async Task SingleResourceEndpointReturnsActivitySummaryWhenCurrentUserIdProvided() + { + // Given + this.resourceRepository.Setup(rr => rr.GetResourceReferencesByOriginalResourceReferenceIds(new List() { 1 })) + .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1)); + + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(new List() { 1 }, new List() { currentUserId })) + .ReturnsAsync(this.ResourceActivityDTOList.ToList()); + + // When + var x = await this.resourceService.GetResourceReferenceByOriginalId(1, currentUserId); + + // Then + x.UserSummaryActivityStatuses.Should().NotBeNull(); + x.UserSummaryActivityStatuses[0].MajorVersionId.Should().Be(5); + x.UserSummaryActivityStatuses[1].MajorVersionId.Should().Be(4); + x.UserSummaryActivityStatuses[2].MajorVersionId.Should().Be(3); + x.UserSummaryActivityStatuses[3].MajorVersionId.Should().Be(2); + x.UserSummaryActivityStatuses[4].MajorVersionId.Should().Be(1); + + x.UserSummaryActivityStatuses[0].ActivityStatusDescription.Should().Be("Passed"); + x.UserSummaryActivityStatuses[1].ActivityStatusDescription.Should().Be("In progress"); + x.UserSummaryActivityStatuses[2].ActivityStatusDescription.Should().Be("Viewed"); + x.UserSummaryActivityStatuses[3].ActivityStatusDescription.Should().Be("In progress"); + x.UserSummaryActivityStatuses[4].ActivityStatusDescription.Should().Be("Viewed"); + } + + [Fact] + public async Task SingleResourceEndpointReturnsEmptyActivitySummaryWhenNoCurrentUserIdProvided() + { + // Given + this.resourceRepository.Setup(rr => rr.GetResourceReferencesByOriginalResourceReferenceIds(new List() { 1 })) + .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1)); + + // This should not be hit + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(new List() { 1 }, new List() { currentUserId })) + .ReturnsAsync(this.ResourceActivityDTOList.ToList()); + + // When + var x = await this.resourceService.GetResourceReferenceByOriginalId(1, null); + + // Then + x.UserSummaryActivityStatuses.Should().BeEmpty(); + } + + [Fact] + public async Task GetResourceReferencesByCompleteReturnsCorrectInformation() + { + // Given + List resourceIds = new List() { 1, 2 }; + List resources = this.ResourceList.GetRange(0, 2); + resources[0].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 1, title: "title1", description: "description1", rating: 3m, resourceType: ResourceTypeEnum.Article); + resources[1].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 2, hasCurrentResourceVersion: false, hasNodePath: false, resourceType: ResourceTypeEnum.Assessment); + + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(It.IsAny>(), It.IsAny>())) + .ReturnsAsync(this.ResourceActivityDTOList); + + this.resourceRepository.Setup(rr => rr.GetResourcesFromIds(resourceIds)) + .ReturnsAsync(resources); + + // When + var x = await this.resourceService.GetResourceReferenceByActivityStatus(new List() { (int)ActivityStatusEnum.Completed }, currentUserId); + + // Then + + // Two groups resourceId 1 and 2 have completed for a major version. ResourceId 3 had resourceActivity data but not completed + x.Count().Should().Be(2); + + // We are including all the major versions not just the matching ones if there exists one matching one + x[0].ResourceId.Should().Be(1); + x[0].UserSummaryActivityStatuses.Count().Should().Be(5); + + // Return all the activitySummaries if one match + x[1].ResourceId.Should().Be(2); + x[1].UserSummaryActivityStatuses.Count().Should().Be(3); + + // we are not excluding major version that are not completed. We return the resource and all its activitySummaries if one matches + x[0].UserSummaryActivityStatuses[1].ActivityStatusDescription.Should().Be("In progress"); + x[0].UserSummaryActivityStatuses[2].ActivityStatusDescription.Should().Be("Viewed"); // Rename completed and still return it + + } + + [Fact] + public async Task GetResourceReferencesByInProgressReturnsCorrectInformation() + { + // Given + List resourceIds = new List() { 1, 3 }; + List resources = new List() { this.ResourceList[0], this.ResourceList[2] }; + resources[0].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 1, title: "title1", description: "description1", rating: 3m, resourceType: ResourceTypeEnum.Article); + resources[1].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 3, title: "title2", description: "description2"); + + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(It.IsAny>(), It.IsAny>())) + .ReturnsAsync(this.ResourceActivityDTOList); + + this.resourceRepository.Setup(rr => rr.GetResourcesFromIds(resourceIds)) + .ReturnsAsync(resources); + + // When + var x = await this.resourceService.GetResourceReferenceByActivityStatus(new List() { (int)ActivityStatusEnum.Incomplete }, currentUserId); // In complete in the database is in progress im database + + // Then + + // Two groups resourceId 1 and 3 have completed for a major version. ResourceId 2 had resourceActivity data but not "in progress" + x.Count().Should().Be(2); + + // We are including all the major versions not just the matching ones if there exists one matching one + x[0].ResourceId.Should().Be(1); + x[0].UserSummaryActivityStatuses.Count().Should().Be(5); + + // Return all the activitySummaries if one match + x[1].ResourceId.Should().Be(3); + x[1].UserSummaryActivityStatuses.Count().Should().Be(3); + + // we are not excluding major version that are not completed. We return the resource and all its activitySummaries if one matches + x[0].UserSummaryActivityStatuses[1].ActivityStatusDescription.Should().Be("In progress"); + x[0].UserSummaryActivityStatuses[2].ActivityStatusDescription.Should().Be("Viewed"); // Rename completed and still return it + + } + + [Fact] + public async Task GetResourceReferencesByCertificatesReturnsCorrectInformation() + { + + // Given + List resourceIds = new List() { 1, 3 }; // Ids returned from activity + + List resources = new List() { this.ResourceList[0], this.ResourceList[2] }; + resources[0].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 1, title: "title1", description: "description1", rating: 3m, resourceType: ResourceTypeEnum.Article); + resources[1].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 3, title: "title2", description: "description2"); + + + // Will be passed resourceIds and currentUserId + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(It.IsAny>(), It.IsAny>())) + .ReturnsAsync(this.ResourceActivityDTOList); + + this.resourceRepository.Setup(rr => rr.GetAchievedCertificatedResourceIds(currentUserId)) + .ReturnsAsync(resourceIds); + + this.resourceRepository.Setup(rr => rr.GetResourcesFromIds(resourceIds)) + .ReturnsAsync(resources); + + // When + var x = await this.resourceService.GetResourceReferencesForCertificates(currentUserId); + + // Then + + x.Count().Should().Be(2); + + // We are including all the major versions not just the matching ones if there exists one matching one + x[0].ResourceId.Should().Be(1); + x[0].UserSummaryActivityStatuses.Count().Should().Be(5); + + // Return all the activitySummaries if one match + x[1].ResourceId.Should().Be(3); + x[1].UserSummaryActivityStatuses.Count().Should().Be(3); + + // we are not excluding major version that are not completed (assuming here that its completed and has certificated flag). We return the resource and all its activitySummaries if one matches + x[0].UserSummaryActivityStatuses[1].ActivityStatusDescription.Should().Be("In progress"); + x[0].UserSummaryActivityStatuses[2].ActivityStatusDescription.Should().Be("Viewed"); // Rename completed and still return it + } + + [Fact] + public async Task GetResourceReferencesByCompleteNoActivitySummaryFound() + { + // Given + List resourceIds = new List() { }; + List resources = this.ResourceList.GetRange(0, 0); + + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(It.IsAny>(), It.IsAny>())) + .ReturnsAsync(this.ResourceActivityDTOList.GetRange(8, 3)); + + this.resourceRepository.Setup(rr => rr.GetResourcesFromIds(resourceIds)) + .ReturnsAsync(resources); + + // When + var x = await this.resourceService.GetResourceReferenceByActivityStatus(new List() { (int)ActivityStatusEnum.Completed }, currentUserId); + + // Then + x.Count().Should().Be(0); + } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs index d393ce59d..ee84c1c51 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs @@ -6,6 +6,7 @@ namespace LearningHub.Nhs.OpenApi.Tests.Services.Services using FizzWare.NBuilder; using FluentAssertions; using FluentAssertions.Execution; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Search; @@ -73,7 +74,7 @@ public async Task SearchPassesQueryOnToFindwise() .ReturnsAsync(FindwiseResultModel.Failure(FindwiseRequestStatus.Timeout)); // When - await this.searchService.Search(searchRequest); + await this.searchService.Search(searchRequest, null); // Then this.findwiseClient.Verify(fc => fc.Search(searchRequest)); @@ -90,7 +91,7 @@ public async Task SearchReturnsTotalHitsAndSearchResult() this.GivenFindwiseReturnsSuccessfulResponse(74, Enumerable.Range(1, 34)); // When - var searchResult = await this.searchService.Search(searchRequest); + var searchResult = await this.searchService.Search(searchRequest, null); // Then searchResult.Resources.Count.Should().Be(34); @@ -137,7 +138,7 @@ public async Task SearchResultsReturnExpectedValues() this.GivenFindwiseReturnsSuccessfulResponse(2, new[] { 1, 2, 3 }); // When - var searchResult = await this.searchService.Search(searchRequest); + var searchResult = await this.searchService.Search(searchRequest, null); // Then searchResult.Resources.Count.Should().Be(2); @@ -180,7 +181,7 @@ public async Task SearchReturnsResourcesInOrderMatchingFindwise() .ReturnsAsync(resources); // When - var searchResultModel = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10)); + var searchResultModel = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10), null); // Then searchResultModel.Resources.Select(r => r.ResourceId).Should().ContainInOrder(new[] { 1, 3, 2 }); @@ -194,7 +195,6 @@ public async Task SearchReplacesNullPropertiesOfResourceWithDefaultValues() { Builder.CreateNew() .With(r => r.Id = 1) - .With(r => r.CurrentResourceVersion = null) .With( r => r.ResourceReference = new[] { @@ -212,7 +212,7 @@ public async Task SearchReplacesNullPropertiesOfResourceWithDefaultValues() this.GivenFindwiseReturnsSuccessfulResponse(1, new[] { 1 }); // When - var searchResult = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10)); + var searchResult = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10), null); // Then using var scope = new AssertionScope(); @@ -233,7 +233,9 @@ public async Task SearchReplacesNullPropertiesOfResourceWithDefaultValues() string.Empty, expectedResourceReferences, "Article", - 0)); + 0, + 0, + new List(){ })); } private void GivenFindwiseReturnsSuccessfulResponse(int totalHits, IEnumerable resourceIds) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/BookmarkController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/BookmarkController.cs index d5897aa2a..9c7d8f0b9 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/BookmarkController.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/BookmarkController.cs @@ -15,7 +15,7 @@ [Authorize] [Route("Bookmark")] [ApiController] - public class BookmarkController : Controller + public class BookmarkController : OpenApiControllerBase { private readonly IBookmarkService bookmarkService; @@ -28,6 +28,7 @@ public BookmarkController(IBookmarkService bookmarkService) this.bookmarkService = bookmarkService; } + /// /// /// Gets all bookmarks by parent. /// @@ -36,11 +37,7 @@ public BookmarkController(IBookmarkService bookmarkService) [Route("GetAllByParent")] public async Task> GetAllByParent() { - var accessToken = await this.HttpContext - .GetTokenAsync(OpenIdConnectParameterNames.AccessToken); - - return await this.bookmarkService.GetAllByParent( - accessToken); + return await this.bookmarkService.GetAllByParent(this.TokenWithoutBearer); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/OpenApiControllerBase.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/OpenApiControllerBase.cs new file mode 100644 index 000000000..16fd3799c --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/OpenApiControllerBase.cs @@ -0,0 +1,60 @@ +namespace LearningHub.NHS.OpenAPI.Controllers +{ + using System.Net; + using System.Security.Claims; + using LearningHub.Nhs.OpenApi.Models.Exceptions; + using Microsoft.AspNetCore.Mvc; + + /// + /// The base class for API controllers. + /// + public abstract class OpenApiControllerBase : ControllerBase + { + /// + /// Gets the current user's ID. + /// + public int? CurrentUserId + { + get + { + // This check is to determine between the two ways of authorising, OAuth and APIKey.OAuth provides userId and APIKey does not. For OpenApi we provide the data without specific user info. + if ((this.User?.Identity?.AuthenticationType ?? null) == "AuthenticationTypes.Federation") + { + int userId; + if (int.TryParse(User.FindFirst(ClaimTypes.NameIdentifier).Value, out userId)) + { + return userId; + } + else + { + // If parsing fails, return null - for apikey this will be the name + return null; + } + } + else + { + // When authorizing by ApiKey we do not have a user for example + return null; + } + } + } + + /// + /// Gets the bearer token from OAuth and removes "Bearer " prepend. + /// + public string TokenWithoutBearer + { + get + { + string accessToken = this.HttpContext.Request.Headers["Authorization"].ToString(); + + if (string.IsNullOrEmpty(accessToken)) + { + throw new HttpResponseException($"No token provided please use OAuth", HttpStatusCode.Unauthorized); + } + + return accessToken.StartsWith("Bearer ") ? accessToken.Substring("Bearer ".Length) : accessToken; + } + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs index 95591961f..ec8774fea 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs @@ -1,10 +1,12 @@ namespace LearningHub.NHS.OpenAPI.Controllers { using System; + using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.OpenApi.Models.Configuration; using LearningHub.Nhs.OpenApi.Models.Exceptions; using LearningHub.Nhs.OpenApi.Models.ServiceModels.Findwise; @@ -21,7 +23,7 @@ namespace LearningHub.NHS.OpenAPI.Controllers /// [Route("Resource")] [Authorize] - public class ResourceController : Controller + public class ResourceController : OpenApiControllerBase { private const int MaxNumberOfReferenceIds = 1000; private readonly ISearchService searchService; @@ -75,7 +77,7 @@ await this.searchService.Search( offset, limit ?? this.findwiseConfig.DefaultItemLimitForSearch, catalogueId, - resourceTypes)); + resourceTypes), this.CurrentUserId); switch (resourceSearchResult.FindwiseRequestStatus) { @@ -109,7 +111,7 @@ await this.searchService.Search( [HttpGet("{originalResourceReferenceId}")] public async Task GetResourceReferenceByOriginalId(int originalResourceReferenceId) { - return await this.resourceService.GetResourceReferenceByOriginalId(originalResourceReferenceId); + return await this.resourceService.GetResourceReferenceByOriginalId(originalResourceReferenceId, this.CurrentUserId); } /// @@ -118,14 +120,14 @@ public async Task GetResourceRefe /// ids. /// ResourceReferenceViewModels for matching resources. [HttpGet("Bulk")] - public async Task GetResourceReferencesByOriginalIds([FromQuery]List resourceReferenceIds) + public async Task GetResourceReferencesByOriginalIds([FromQuery] List resourceReferenceIds) { if (resourceReferenceIds.Count > MaxNumberOfReferenceIds) { throw new HttpResponseException($"Too many resources requested. The maximum is {MaxNumberOfReferenceIds}", HttpStatusCode.BadRequest); } - return await this.resourceService.GetResourceReferencesByOriginalIds(resourceReferenceIds.ToList()); + return await this.resourceService.GetResourceReferencesByOriginalIds(resourceReferenceIds.ToList(), this.CurrentUserId); } /// @@ -134,7 +136,7 @@ public async Task GetResourceReferencesByOrigina /// ids. /// ResourceReferenceViewModels for matching resources. [HttpGet("BulkJson")] - public async Task GetResourceReferencesByOriginalIdsFromJson([FromQuery]string resourceReferences) + public async Task GetResourceReferencesByOriginalIdsFromJson([FromQuery] string resourceReferences) { var bulkResourceReferences = JsonConvert.DeserializeObject(resourceReferences); @@ -148,7 +150,51 @@ public async Task GetResourceReferencesByOrigina throw new HttpResponseException($"Too many resources requested. The maximum is {MaxNumberOfReferenceIds}", HttpStatusCode.BadRequest); } - return await this.resourceService.GetResourceReferencesByOriginalIds(bulkResourceReferences.ResourceReferenceIds); + return await this.resourceService.GetResourceReferencesByOriginalIds(bulkResourceReferences.ResourceReferenceIds, this.CurrentUserId); + } + + /// + /// Get resourceReferences that have an in progress activity summary + /// + /// activityStatusId. + /// ResourceReferenceViewModels for matching resources. + [HttpGet("User/{activityStatusId}")] + public async Task> GetResourceReferencesByActivityStatus(int activityStatusId) + { + // These activity statuses are set with other activity statuses and resource type within the ActivityStatusHelper.GetActivityStatusDescription + // Note In progress is in complete in the db + List activityStatusIdsNotInUseInDB = new List() { (int)ActivityStatusEnum.Launched, (int)ActivityStatusEnum.InProgress, (int)ActivityStatusEnum.Viewed, (int)ActivityStatusEnum.Downloaded }; + if (this.CurrentUserId == null) + { + throw new UnauthorizedAccessException("User Id required."); + } + + if (!Enum.IsDefined(typeof(ActivityStatusEnum), activityStatusId)) + { + throw new ArgumentOutOfRangeException($"activityStatusId : {activityStatusId} does not exist within ActivityStatusEnum"); + } + + if (activityStatusIdsNotInUseInDB.Contains(activityStatusId)) + { + throw new ArgumentOutOfRangeException($"activityStatusId: {activityStatusId} does not exist within the database definitions"); + } + + return await this.resourceService.GetResourceReferenceByActivityStatus(new List() { activityStatusId }, this.CurrentUserId.Value); + } + + /// + /// Get resourceReferences that have certificates + /// + /// ResourceReferenceViewModels for matching resources. + [HttpGet("User/Certificates")] + public async Task> GetResourceReferencesByCertificates() + { + if (this.CurrentUserId == null) + { + throw new UnauthorizedAccessException("User Id required."); + } + + return await this.resourceService.GetResourceReferencesForCertificates(this.CurrentUserId.Value); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user new file mode 100644 index 000000000..b17387f00 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user @@ -0,0 +1,9 @@ + + + + ProjectDebugger + + + IIS Local + + \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json b/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json index d77f6b823..dcfd80c44 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json @@ -1,9 +1,9 @@ { - "openapi": "3.0.1", + "openapi": "3.0.2", "info": { "title": "LearningHub.NHS.OpenAPI", "version": "1.3.0", - "description": "A set of API endpoints for retrieving learning resource information from the Learning Hub learning platform. The [Learning Hub](https://learninghub.nhs.uk/) is a platform for hosting and sharing learning resources for health and social care provided by Technology Enhanced Learning (TEL) at NHS England. An application API key must be used to authorise calls to the API from external applications. To contact TEL to discuss connecting your external system to the Learning Hub, email [england.tel@nhs.net](england.tel@nhs.net)" + "description": "A set of API endpoints for retrieving learning resource information from the Learning Hub learning platform. The [Learning Hub](https://learninghub.nhs.uk/) is a platform for hosting and sharing learning resources for health and social care provided by Technology Enhanced Learning (TEL) at NHS England. An application API key must be used to authorise calls to the API from external applications. To contact TEL to discuss connecting your external system to the Learning Hub, email england.tel@nhs.net." }, "paths": { "/Bookmark/GetAllByParent": { @@ -295,6 +295,81 @@ } } } + }, + "/Resource/User/{activityStatusId}": { + "get": { + "tags": [ "Resource" ], + "summary": "Get resource references by activity status", + "operationId": "GetResourceReferencesByActivityStatus", + "parameters": [ + { + "name": "activityStatusId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The activity status Id to filter resource references. Valid values are Completed 3 (returned as Completed/Downloaded/Launched/Viewed), Incomplete 7 (returned as In progress), Passed 5, Failed 4." + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceReferenceWithResourceDetailsViewModel" + } + } + } + } + }, + "400": { + "description": "Bad request: The activityStatusId provided is not valid." + }, + "401": { + "description": "Unauthorized: User Id required." + }, + "403": { + "description": "Forbidden: The activityStatusId is not defined within ActivityStatusEnum or is in the list of activityStatusIdsNotInUseInDB." + }, + "500": { + "description": "Internal server error: An unexpected error occurred while processing the request." + } + } + } + }, + "/Resource/User/Certificates": { + "get": { + "tags": [ "Resource" ], + "summary": "Get resource references where a major version has a certificate", + "operationId": "GetResourceReferencesByCertificates", + "parameters": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceReferenceWithResourceDetailsViewModel" + } + } + } + } + }, + "401": { + "description": "Unauthorized: User Id required." + }, + "500": { + "description": "Internal server error: An unexpected error occurred while processing the request." + } + } + } } }, "components": { diff --git a/WebAPI/LearningHub.Nhs.API/Controllers/CatalogueController.cs b/WebAPI/LearningHub.Nhs.API/Controllers/CatalogueController.cs index 4c5d06ea8..b5dbf58a9 100644 --- a/WebAPI/LearningHub.Nhs.API/Controllers/CatalogueController.cs +++ b/WebAPI/LearningHub.Nhs.API/Controllers/CatalogueController.cs @@ -371,5 +371,19 @@ public async Task AccessRequest(int accessRequestId) { return this.Ok(await this.catalogueService.AccessRequestAsync(this.CurrentUserId, accessRequestId)); } + + /// + /// Gets AllCatalogues. + /// + /// The pageSize. + /// The filterChar. + /// IActionResult. + [HttpGet] + [Route("allcatalogues/{pageSize}/{filterChar}")] + public async Task GetAllCataloguesAsync(int pageSize, string filterChar = null) + { + var response = await this.catalogueService.GetAllCataloguesAsync(pageSize, filterChar, this.CurrentUserId); + return this.Ok(response); + } } } diff --git a/WebAPI/LearningHub.Nhs.API/Controllers/SearchController.cs b/WebAPI/LearningHub.Nhs.API/Controllers/SearchController.cs index 424e54b0f..ab4e417d9 100644 --- a/WebAPI/LearningHub.Nhs.API/Controllers/SearchController.cs +++ b/WebAPI/LearningHub.Nhs.API/Controllers/SearchController.cs @@ -211,6 +211,19 @@ public async Task CreateCatalogueSearchTermAction(CatalogueSearch } } + /// + /// Get AllCatalogue search result. + /// + /// The catalogue search request model. + /// The . + [HttpPost] + [Route("GetAllCatalogueSearchResult")] + public async Task GetAllCatalogueSearchResult(AllCatalogueSearchRequestModel catalogueSearchRequestModel) + { + var vm = await this.GetAllCatalogueResults(catalogueSearchRequestModel); + return this.Ok(vm); + } + /// /// Get search result. /// @@ -244,14 +257,13 @@ private async Task GetSearchResults(SearchRequestModel searchRe continue; } - var roleUserGroups = this.catalogueService.GetRoleUserGroupsForCatalogue(catalogue.NodeId, true); - document.CatalogueUrl = catalogue.Url; document.CatalogueBadgeUrl = catalogue.BadgeUrl; document.CatalogueName = catalogue.Name; if (catalogue.RestrictedAccess) { + var roleUserGroups = await this.catalogueService.GetRoleUserGroupsForCatalogueSearch(catalogue.NodeId, this.CurrentUserId); document.CatalogueRestrictedAccess = catalogue.RestrictedAccess; document.CatalogueHasAccess = roleUserGroups.Any(x => x.UserGroup.UserUserGroup.Any(y => y.UserId == this.CurrentUserId) && (x.RoleId == (int)RoleEnum.Editor || x.RoleId == (int)RoleEnum.LocalAdmin || x.RoleId == (int)RoleEnum.Reader)); @@ -334,8 +346,6 @@ private async Task GetCatalogueSearchResults(Catalogue continue; } - var roleUserGroups = this.catalogueService.GetRoleUserGroupsForCatalogue(catalogue.NodeId, true); - // catalogue.No document.Url = catalogue.Url; document.BannerUrl = catalogue.BannerUrl; @@ -344,6 +354,7 @@ private async Task GetCatalogueSearchResults(Catalogue document.NodePathId = catalogue.NodePathId; if (catalogue.RestrictedAccess) { + var roleUserGroups = await this.catalogueService.GetRoleUserGroupsForCatalogueSearch(catalogue.NodeId, this.CurrentUserId); document.RestrictedAccess = catalogue.RestrictedAccess; document.HasAccess = roleUserGroups.Any(x => x.UserGroup.UserUserGroup.Any(y => y.UserId == this.CurrentUserId) && (x.RoleId == (int)RoleEnum.Editor || x.RoleId == (int)RoleEnum.LocalAdmin || x.RoleId == (int)RoleEnum.Reader)); @@ -382,5 +393,76 @@ private async Task GetCatalogueSearchResults(Catalogue return searchViewModel; } + + /// + /// Get All catalogue search results. + /// + /// The catalog search request model. + /// The . + private async Task GetAllCatalogueResults(AllCatalogueSearchRequestModel catalogueSearchRequestModel) + { + var results = await this.searchService.GetAllCatalogueSearchResultsAsync(catalogueSearchRequestModel); + + var documents = results.DocumentList.Documents.ToList(); + var documentIds = documents.Select(x => int.Parse(x.Id)).ToList(); + var catalogues = this.catalogueService.GetCataloguesByNodeId(documentIds); + var bookmarks = this.bookmarkRepository.GetAll().Where(b => documentIds.Contains(b.NodeId ?? -1) && b.UserId == this.CurrentUserId); + var allProviders = await this.providerService.GetAllAsync(); + + foreach (var document in documents) + { + var catalogue = catalogues.SingleOrDefault(x => x.NodeId == int.Parse(document.Id)); + if (catalogue == null) + { + continue; + } + + // catalogue.No + document.Url = catalogue.Url; + document.BannerUrl = catalogue.BannerUrl; + document.BadgeUrl = catalogue.BadgeUrl; + document.CardImageUrl = catalogue.CardImageUrl; + document.NodePathId = catalogue.NodePathId; + + if (catalogue.RestrictedAccess) + { + var roleUserGroups = await this.catalogueService.GetRoleUserGroupsForCatalogueSearch(catalogue.NodeId, this.CurrentUserId); + document.RestrictedAccess = catalogue.RestrictedAccess; + document.HasAccess = roleUserGroups.Any(x => x.UserGroup.UserUserGroup.Any(y => y.UserId == this.CurrentUserId) + && (x.RoleId == (int)RoleEnum.Editor || x.RoleId == (int)RoleEnum.LocalAdmin || x.RoleId == (int)RoleEnum.Reader)); + } + + var bookmark = bookmarks.FirstOrDefault(x => x.NodeId == int.Parse(document.Id)); + if (bookmark != null) + { + document.BookmarkId = bookmark?.Id; + document.IsBookmarked = !bookmark?.Deleted ?? false; + } + + if (document.ProviderIds?.Count > 0) + { + document.Providers = allProviders.Where(n => document.ProviderIds.Contains(n.Id)).ToList(); + } + } + + var searchViewModel = new SearchAllCatalogueViewModel + { + DocumentModel = documents, + SearchString = catalogueSearchRequestModel.SearchText, + Hits = results.DocumentList.Documents.Count(), + DescriptionMaximumLength = this.settings.Findwise.MaximumDescriptionLength, + ErrorOnAPI = results.ErrorsOnAPICall, + Facets = results.Facets, + }; + + if (results.Stats != null) + { + searchViewModel.TotalHits = results.Stats.TotalHits; + } + + searchViewModel.SearchId = catalogueSearchRequestModel.SearchId > 0 ? catalogueSearchRequestModel.SearchId : results.SearchId; + + return searchViewModel; + } } } \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj b/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj index 191c99713..82b88c37a 100644 --- a/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj +++ b/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj @@ -27,7 +27,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj b/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj index fdacce920..f38e0260e 100644 --- a/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj +++ b/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj b/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj index ca2c85a82..f61e84c2f 100644 --- a/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj +++ b/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index f513b6ec7..477a48869 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -200,9 +200,6 @@ - - - @@ -518,10 +515,17 @@ + + + + + + + diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Script.PostDeployment.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Script.PostDeployment.sql index 151c7c2c5..5d99e1884 100644 --- a/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Script.PostDeployment.sql +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Script.PostDeployment.sql @@ -81,4 +81,5 @@ UPDATE [resources].[ResourceVersion] SET CertificateEnabled = 0 WHERE VersionSta :r .\Scripts\InitialiseDataForEmailTemplates.sql :r .\Scripts\TD-2929_ActivityStatusUpdates.sql :r .\Scripts\InitialiseDataForEmailTemplates.sql -:r .\Scripts\AttributeData.sql \ No newline at end of file +:r .\Scripts\AttributeData.sql +:r .\Scripts\PPSXFileType.sql \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Scripts/PPSXFileType.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Scripts/PPSXFileType.sql new file mode 100644 index 000000000..d948664ad --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/Post-Deploy/Scripts/PPSXFileType.sql @@ -0,0 +1,30 @@ +IF NOT EXISTS(SELECT Id FROM [resources].[FileType] where Extension ='ppsx') +BEGIN +INSERT INTO [resources].[FileType] + (Id, + [DefaultResourceTypeId] + ,[Name] + ,[Description] + ,[Extension] + ,[Icon] + ,[NotAllowed] + ,[Deleted] + ,[CreateUserId] + ,[CreateDate] + ,[AmendUserId] + ,[AmendDate]) + VALUES + (70, + 9 + ,'PowerPoint Open XML Slide Show' + ,'PowerPoint Open XML Slide Show' + ,'ppsx' + ,'a-mppoint-icon.svg' + ,0 + ,0 + ,57541 + ,SYSDATETIMEOFFSET() + ,57541 + ,SYSDATETIMEOFFSET()) +END +GO \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetResourceActivityPerResourceMajorVersion.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetResourceActivityPerResourceMajorVersion.sql new file mode 100644 index 000000000..063110343 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetResourceActivityPerResourceMajorVersion.sql @@ -0,0 +1,152 @@ + +------------------------------------------------------------------------------- +-- Author Phil T +-- Created 04-07-24 +-- Purpose Return resource activity for each major version for user + + +-- Description +/* + This procedure returns a single entry per resource Id, selecting the most important one for that major version. + This is so users can still have a resourceActivity history following a majorVersion change + + UserIds is nullable so that general resource activity can be searched for + ResourceIds is nullable so that all a users history can be searched for + + When determining the resourceActivity statusDescription in the front end resourceTypeId is also required for changing completed statuses to resourceType specific ones + + Currently if multiple rows meet the case criteria we retrieve the one with the highest Id which is also expected to be the ActivityEnd part of the activityStatus pair. + +*/ +-- Future Considerations +/* + Because the activityResource should come in pairs one with ActivityStart populated and one with ActivityEnd populated + it could be desireable to join via LaunchResourceActivityId and coalesce the data in future. + Or/And coalesce where the case returns multiple rows. + +*/ +-- Notes + -- resourceId is used not originalResourceId + + +------------------------------------------------------------------------------- + +-- Create the new stored procedure +CREATE PROCEDURE [activity].[GetResourceActivityPerResourceMajorVersion] + @ResourceIds VARCHAR(MAX) = NULL, + @UserIds VARCHAR(MAX) = NULL +AS +BEGIN + + -- Split the comma-separated list into a table of integers + DECLARE @ResourceIdTable TABLE (ResourceId INT); + + IF @ResourceIds IS NOT NULL AND @ResourceIds <> '' + BEGIN + INSERT INTO @ResourceIdTable (ResourceId) + SELECT CAST(value AS INT) + FROM STRING_SPLIT(@ResourceIds, ','); + END; + + -- Split the comma-separated list of UserIds into a table + DECLARE @UserIdTable TABLE (UserId INT); + + IF @UserIds IS NOT NULL AND @UserIds <> '' + BEGIN + INSERT INTO @UserIdTable (UserId) + SELECT CAST(value AS INT) + FROM STRING_SPLIT(@UserIds, ','); + END; + + WITH FilteredResourceActivities AS ( + SELECT + ars.[Id], + ars.[UserId], + ars.[LaunchResourceActivityId], + ars.[ResourceId], + ars.[ResourceVersionId], + ars.[MajorVersion], + ars.[MinorVersion], + ars.[NodePathId], + ars.[ActivityStatusId], + ars.[ActivityStart], + ars.[ActivityEnd], + ars.[DurationSeconds], + ars.[Score], + ars.[Deleted], + ars.[CreateUserID], + ars.[CreateDate], + ars.[AmendUserID], + ars.[AmendDate] + FROM + [activity].[resourceactivity] ars + WHERE + (@UserIds IS NULL OR ars.userId IN (SELECT UserId FROM @UserIdTable) OR NOT EXISTS (SELECT 1 FROM @UserIdTable)) + AND (@ResourceIds IS NULL OR @ResourceIds = '' OR ars.resourceId IN (SELECT ResourceId FROM @ResourceIdTable) OR NOT EXISTS (SELECT 1 FROM @ResourceIdTable)) + AND ars.Deleted = 0 + AND ars.ActivityStatusId NOT IN (1, 6, 2) -- These Ids are not in use - Launched, Downloaded, In Progress (stored as completed and incomplete then renamed in the application) + ), + RankedActivities AS ( + SELECT + ra.[Id], + ra.[UserId], + ra.[LaunchResourceActivityId], + ra.[ResourceId], + ra.[ResourceVersionId], + ra.[MajorVersion], + ra.[MinorVersion], + ra.[NodePathId], + ra.[ActivityStatusId], + ra.[ActivityStart], + ra.[ActivityEnd], + ra.[DurationSeconds], + ra.[Score], + ra.[Deleted], + ra.[CreateUserID], + ra.[CreateDate], + ra.[AmendUserID], + ra.[AmendDate], + ROW_NUMBER() OVER ( + PARTITION BY resourceId, userId, MajorVersion + ORDER BY + CASE + WHEN ActivityStatusId = 5 THEN 1 -- Passed + WHEN ActivityStatusId = 3 THEN 2 -- Completed + WHEN ActivityStatusId = 4 THEN 3 -- Failed + WHEN ActivityStatusId = 7 THEN 4 -- Incomplete + ELSE 5 -- shouldn't be any + END, + Id DESC -- we have two entries per interacting with a resource the start and the end, we are just returning the last entry made + -- there is the option of instead coalescing LaunchResourceActivityId, ActivityStart,ActivityEnd potentially via joining LaunchResourceActivityId and UserId + ) AS RowNum + FROM + FilteredResourceActivities ra + ) + SELECT + ra.[Id], + ra.[UserId], + ra.[LaunchResourceActivityId], + ra.[ResourceId], + ra.[ResourceVersionId], + ra.[MajorVersion], + ra.[MinorVersion], + ra.[NodePathId], + ra.[ActivityStatusId], + ra.[ActivityStart], + ra.[ActivityEnd], + ra.[DurationSeconds], + ra.[Score], + ra.[Deleted], + ra.[CreateUserID], + ra.[CreateDate], + ra.[AmendUserID], + ra.[AmendDate] + FROM + RankedActivities ra + WHERE + RowNum = 1 + order by MajorVersion desc; +END; +GO + + diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserLearningActivities.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserLearningActivities.sql index c96931937..15272dc4e 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserLearningActivities.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserLearningActivities.sql @@ -10,6 +10,7 @@ -- Sarathlal 08-03-2024 -- Sarathlal 23-04-2024 TD-2954: Audio/Video/Assessment issue resolved and duplicate issue also resolved -- Sarathlal 25-04-2024 TD-4067: Resource with muliple version issue resolved +-- Arunima 26-07-2024 TD-4411: "Completed" filter along with "Assessment" doesn't display the correct results ------------------------------------------------------------------------------- CREATE PROCEDURE [activity].[GetUserLearningActivities] ( @userId INT @@ -271,9 +272,35 @@ FROM ( ) ) OR - ([Res].[ResourceTypeId] IN (6,11) AND [ResourceActivity].[ActivityStatusId] = 3) - OR ([Res].[ResourceTypeId] IN (11) AND [ResourceActivity].[ActivityStatusId] = 3 AND [AssessResVer].[AssessmentType]=1) - --OR + ([Res].[ResourceTypeId] IN (6) AND [ResourceActivity].[ActivityStatusId] = 3) + OR ( + EXISTS (SELECT 1 FROM @tmpActivityStatus WHERE ActivityStatusId = 3) + AND + ( + [Res].[ResourceTypeId] = 11 AND [AssessResVer].[AssessmentType]=1 + AND + EXISTS + ( + SELECT 1 + FROM [activity].[AssessmentResourceActivity] AS [AssessmentResourceActivity6] + WHERE + [AssessmentResourceActivity6].[Deleted] = 0 + AND + [ResourceActivity].[Id] = [AssessmentResourceActivity6].[ResourceActivityId] + ) + AND + ( + (SELECT TOP(1) + [AssessmentResourceActivity7].[Score] + FROM [activity].[AssessmentResourceActivity] AS [AssessmentResourceActivity7] + WHERE + [AssessmentResourceActivity7].[Deleted] = 0 + AND [ResourceActivity].[Id] = [AssessmentResourceActivity7].[ResourceActivityId]) >= 0.0 + ) + ) + + ) + --OR --( -- ([Res].[ResourceTypeId] IN (1,5,10,12) AND [ResourceActivity].[ActivityStatusId] = 3) -- AND @@ -507,5 +534,3 @@ LEFT JOIN ( ORDER BY [t2].[ActivityStart] DESC, [t2].[Id], [t2].[Id0], [t2].[Id1], [t2].[Id2], [VideoResourceVersion].[Id], [AudeoResourceVersion].[Id], [t3].[Id], [t4].[Id], [t5].[Id], [t6].[Id], [t7].[Id], [t8].[Id], [t9].[Id], [t10].[Id], [t11].[Id] END - - diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserLearningActivitiesCount.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserLearningActivitiesCount.sql index daa20e0e7..7ce53f78c 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserLearningActivitiesCount.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserLearningActivitiesCount.sql @@ -9,6 +9,7 @@ -- Sarathlal 18-12-2023 -- Sarathlal 08-03-2024 -- Sarathlal 23-04-2024 TD-2954: Audio/Video/Assessment issue resolved and duplicate issue also resolved +-- Arunima 26-07-2024 TD-4411: "Completed" filter along with "Assessment" doesn't display the correct results ------------------------------------------------------------------------------- CREATE PROCEDURE [activity].[GetUserLearningActivitiesCount] ( @userId INT @@ -186,10 +187,35 @@ FROM ( ) ) OR - ([Res].[ResourceTypeId] IN (6,11) AND [ResourceActivity].[ActivityStatusId] = 3) - OR ([Res].[ResourceTypeId] IN (11) AND [ResourceActivity].[ActivityStatusId] = 3 AND [AssessResVer].[AssessmentType]=1) - - --OR + ([Res].[ResourceTypeId] IN (6) AND [ResourceActivity].[ActivityStatusId] = 3) + OR ( + EXISTS (SELECT 1 FROM @tmpActivityStatus WHERE ActivityStatusId = 3) + AND + ( + [Res].[ResourceTypeId] = 11 AND [AssessResVer].[AssessmentType]=1 + AND + EXISTS + ( + SELECT 1 + FROM [activity].[AssessmentResourceActivity] AS [AssessmentResourceActivity6] + WHERE + [AssessmentResourceActivity6].[Deleted] = 0 + AND + [ResourceActivity].[Id] = [AssessmentResourceActivity6].[ResourceActivityId] + ) + AND + ( + (SELECT TOP(1) + [AssessmentResourceActivity7].[Score] + FROM [activity].[AssessmentResourceActivity] AS [AssessmentResourceActivity7] + WHERE + [AssessmentResourceActivity7].[Deleted] = 0 + AND [ResourceActivity].[Id] = [AssessmentResourceActivity7].[ResourceActivityId]) >= 0.0 + ) + ) + + ) + --OR --( -- ([Res].[ResourceTypeId] IN (1,5,10,12) AND [ResourceActivity].[ActivityStatusId] = 3) -- AND diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Hierarchy/GetCatalogues.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Hierarchy/GetCatalogues.sql new file mode 100644 index 000000000..64eb0d38c --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Hierarchy/GetCatalogues.sql @@ -0,0 +1,39 @@ +CREATE PROCEDURE [hierarchy].[GetCatalogues] ( + @userId INT + ,@filterChar nvarchar(10) + ,@OffsetRows int + ,@fetchRows int + ) +AS +BEGIN + + SELECT + nv.NodeId + ,cnv.Id AS NodeVersionId + ,cnv.Name + ,cnv.Description + ,cnv.BannerUrl + ,cnv.BadgeUrl + ,cnv.CardImageUrl + ,cnv.Url + ,cnv.RestrictedAccess + ,CAST(CASE WHEN cnv.RestrictedAccess = 1 AND auth.CatalogueNodeId IS NULL THEN 0 ELSE 1 END AS bit) AS HasAccess + ,ub.Id AS BookMarkId + ,CAST(ISNULL(ub.[Deleted], 1) ^ 1 AS BIT) AS IsBookmarked + FROM [hierarchy].[Node] n + JOIN [hierarchy].[NodeVersion] nv ON nv.NodeId = n.Id + JOIN [hierarchy].[CatalogueNodeVersion] cnv ON cnv.NodeVersionId = nv.Id + LEFT JOIN hub.UserBookmark ub ON ub.UserId = @userId AND ub.NodeId = nv.NodeId + LEFT JOIN ( SELECT DISTINCT CatalogueNodeId + FROM [hub].[RoleUserGroupView] rug JOIN hub.UserUserGroup uug ON rug.UserGroupId = uug.UserGroupId + WHERE rug.ScopeTypeId = 1 and rug.RoleId in (1,2,3) and uug.Deleted = 0 and uug.UserId = @userId) auth ON n.Id = auth.CatalogueNodeId + WHERE n.Id <> 1 AND n.Hidden = 0 AND n.Deleted = 0 AND cnv.Deleted = 0 AND nv.VersionStatusId = 2 + and cnv.Name like @filterChar+'%' + ORDER BY cnv.Name + OFFSET @OffsetRows ROWS + FETCH NEXT @FetchRows ROWS ONLY + + + + +END diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Hierarchy/GetCataloguesCount.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Hierarchy/GetCataloguesCount.sql new file mode 100644 index 000000000..eb22506d0 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Hierarchy/GetCataloguesCount.sql @@ -0,0 +1,61 @@ +CREATE PROCEDURE [hierarchy].[GetCataloguesCount] ( + @userId INT + ) +AS +BEGIN + + WITH Catalogues AS( + SELECT cnv.Name as Name + FROM [hierarchy].[Node] n + JOIN [hierarchy].[NodeVersion] nv ON nv.NodeId = n.Id + JOIN [hierarchy].[CatalogueNodeVersion] cnv ON cnv.NodeVersionId = nv.Id + LEFT JOIN hub.UserBookmark ub ON ub.UserId = @userId AND ub.NodeId = nv.NodeId + LEFT JOIN ( SELECT DISTINCT CatalogueNodeId + FROM [hub].[RoleUserGroupView] rug JOIN hub.UserUserGroup uug ON rug.UserGroupId = uug.UserGroupId + WHERE rug.ScopeTypeId = 1 and rug.RoleId in (1,2,3) and uug.Deleted = 0 and uug.UserId = @userId) auth ON n.Id = auth.CatalogueNodeId + WHERE n.Id <> 1 AND n.Hidden = 0 AND n.Deleted = 0 AND cnv.Deleted = 0 AND nv.VersionStatusId = 2 ), + + +Alphabet AS ( + SELECT 'A' AS Letter UNION ALL + SELECT 'B' UNION ALL + SELECT 'C' UNION ALL + SELECT 'D' UNION ALL + SELECT 'E' UNION ALL + SELECT 'F' UNION ALL + SELECT 'G' UNION ALL + SELECT 'H' UNION ALL + SELECT 'I' UNION ALL + SELECT 'J' UNION ALL + SELECT 'K' UNION ALL + SELECT 'L' UNION ALL + SELECT 'M' UNION ALL + SELECT 'N' UNION ALL + SELECT 'O' UNION ALL + SELECT 'P' UNION ALL + SELECT 'Q' UNION ALL + SELECT 'R' UNION ALL + SELECT 'S' UNION ALL + SELECT 'T' UNION ALL + SELECT 'U' UNION ALL + SELECT 'V' UNION ALL + SELECT 'W' UNION ALL + SELECT 'X' UNION ALL + SELECT 'Y' UNION ALL + SELECT 'Z' UNION ALL + SELECT '0-9' +) +SELECT + Alphabet.Letter AS Alphabet, + COALESCE(COUNT(cnv.Name), 0) AS Count + FROM Alphabet + LEFT JOIN Catalogues cnv + ON + ((LEFT(cnv.Name, 1) = Alphabet.Letter AND LEFT(cnv.Name, 1) BETWEEN 'A' AND 'Z') + OR (Alphabet.Letter = '0-9' AND LEFT(cnv.Name, 1) BETWEEN '0' AND '9') + ) + + GROUP BY Alphabet.Letter + ORDER BY (CASE WHEN Alphabet.Letter like '[a-z]%' THEN 0 ELSE 1 END), Alphabet.Letter; + +END diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetAchievedCertificatedResourcesWithOptionalPagination.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetAchievedCertificatedResourcesWithOptionalPagination.sql new file mode 100644 index 000000000..922480858 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetAchievedCertificatedResourcesWithOptionalPagination.sql @@ -0,0 +1,118 @@ + +------------------------------------------------------------------------------- +-- Author PT +-- Created 11 July 2024 +-- Purpose Get achieved certificated resources with optional pagination +-- Description Extracted from the GetDashboardResources sproc to enable one source of truth for determining achieved certificated resources +-- To support the GetDashboardResources it has pagination and to support other requests the default values disable pagination effects +------------------------------------------------------------------------------- + + + +CREATE PROCEDURE [resources].[GetAchievedcertificatedResourcesWithOptionalPagination] + @UserId INT, + + -- Default values disable pagination + @MaxRows INT = 2147483647, -- Warning! Magic number. To disable pagination by default. + @OffsetRows INT = 0, + @FetchRows INT = 2147483647, -- Warning! Magic number. To disable pagination by default. + + @TotalRecords INT OUTPUT +AS +BEGIN + + -- Step 1: Create a table variable to store intermediate results + DECLARE @MyActivity TABLE ( + ResourceId INT, + ResourceActivityId INT + ); + + INSERT INTO @MyActivity + SELECT TOP (@MaxRows) ra.ResourceId, MAX(ra.Id) ResourceActivityId + FROM + /* resources with resource activity, resource activity determines if certificated*/ + activity.ResourceActivity ra + JOIN [resources].[Resource] r ON ra.ResourceId = r.Id + JOIN [resources].[ResourceVersion] rv ON rv.Id = ra.ResourceVersionId + + /* Determining if certificated scorm, assessment mark, media*/ + LEFT JOIN [resources].[AssessmentResourceVersion] arv ON arv.ResourceVersionId = ra.ResourceVersionId + LEFT JOIN [activity].[AssessmentResourceActivity] ara ON ara.ResourceActivityId = ra.Id + LEFT JOIN [activity].[MediaResourceActivity] mar ON mar.ResourceActivityId = ra.Id + LEFT JOIN [activity].[ScormActivity] sa ON sa.ResourceActivityId = ra.Id + + WHERE ra.UserId = @UserId AND rv.CertificateEnabled = 1 -- detemining if certificated + AND ( + (r.ResourceTypeId IN (2, 7) AND ra.ActivityStatusId IN (3) /* resourceType 2 Audio and 7 is video activityStatusId 3 is completed */ + OR ra.ActivityStart < '2020-09-07 00:00:00 +00:00' /* old activity assumed to be valid*/ + OR mar.Id IS NOT NULL AND mar.PercentComplete = 100 /* media activity 100% complete*/ + ) + /* type 6 scorm elearning,*/ + OR (r.ResourceTypeId = 6 AND (sa.CmiCoreLesson_status IN(3,5) OR (ra.ActivityStatusId IN(3, 5)))) /* activityStatus 3 and 5 are completed and passed */ + /* 11 is assessment */ + OR (r.ResourceTypeId = 11 AND ara.Score >= arv.PassMark OR ra.ActivityStatusId IN(3, 5)) /*assessment mark and activity status passed completed */ + /* 1 Article, 5 Image, 8 Weblink 9 file, 10 case, 12 html */ + OR (r.ResourceTypeId IN (1, 5, 8, 9, 10, 12) AND ra.ActivityStatusId IN (3))) /* Completed */ + GROUP BY ra.ResourceId + ORDER BY ResourceActivityId DESC + + SELECT r.Id AS ResourceId + ,( SELECT TOP 1 rr.OriginalResourceReferenceId + FROM [resources].[ResourceReference] rr + JOIN hierarchy.NodePath np on np.id = rr.NodePathId and np.NodeId = n.Id and np.Deleted = 0 + WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0 + ) AS ResourceReferenceID + ,r.CurrentResourceVersionId AS ResourceVersionId + ,r.ResourceTypeId AS ResourceTypeId + ,rv.Title + ,rv.Description + ,CASE + WHEN r.ResourceTypeId = 7 THEN + (SELECT vrv.DurationInMilliseconds from [resources].[VideoResourceVersion] vrv WHERE vrv.[ResourceVersionId] = r.CurrentResourceVersionId) + WHEN r.ResourceTypeId = 2 THEN + (SELECT vrv.DurationInMilliseconds from [resources].[AudioResourceVersion] vrv WHERE vrv.[ResourceVersionId] = r.CurrentResourceVersionId) + ELSE + NULL + END AS DurationInMilliseconds + ,CASE WHEN n.id = 1 THEN NULL ELSE cnv.Name END AS CatalogueName + ,cnv.Url AS Url + ,CASE WHEN n.id = 1 THEN NULL ELSE cnv.BadgeUrl END AS BadgeUrl + ,cnv.RestrictedAccess + ,CAST(CASE WHEN cnv.RestrictedAccess = 1 AND auth.CatalogueNodeId IS NULL THEN 0 ELSE 1 END AS bit) AS HasAccess + ,ub.Id AS BookMarkId + ,CAST(ISNULL(ub.[Deleted], 1) ^ 1 AS BIT) AS IsBookmarked + ,rs.AverageRating + ,rs.RatingCount + FROM @MyActivity ma + JOIN activity.ResourceActivity ra ON ra.id = ma.ResourceActivityId + JOIN resources.resourceversion rv ON rv.id = ra.ResourceVersionId AND rv.Deleted = 0 + JOIN Resources.Resource r ON r.Id = rv.ResourceId + JOIN hierarchy.Publication p ON rv.PublicationId = p.Id AND p.Deleted = 0 + JOIN resources.ResourceVersionRatingSummary rvrs ON rv.Id = rvrs.ResourceVersionId AND rvrs.Deleted = 0 + + /* Catalogue logic */ + JOIN hierarchy.NodeResource nr ON r.Id = nr.ResourceId AND nr.Deleted = 0 + JOIN hierarchy.Node n ON n.Id = nr.NodeId AND n.Hidden = 0 AND n.Deleted = 0 + JOIN hierarchy.NodePath np ON np.NodeId = n.Id AND np.Deleted = 0 AND np.IsActive = 1 + JOIN hierarchy.NodeVersion nv ON nv.NodeId = np.CatalogueNodeId AND nv.VersionStatusId = 2 AND nv.Deleted = 0 + JOIN hierarchy.CatalogueNodeVersion cnv ON cnv.NodeVersionId = nv.Id AND cnv.Deleted = 0 + + /* Book marks */ + LEFT JOIN hub.UserBookmark ub ON ub.UserId = @UserId AND ub.ResourceReferenceId = (SELECT TOP 1 rr.OriginalResourceReferenceId + FROM [resources].[ResourceReference] rr + JOIN hierarchy.NodePath np on np.id = rr.NodePathId and np.NodeId = n.Id and np.Deleted = 0 + WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0) + LEFT JOIN ( SELECT DISTINCT CatalogueNodeId + FROM [hub].[RoleUserGroupView] rug JOIN hub.UserUserGroup uug ON rug.UserGroupId = uug.UserGroupId + WHERE rug.ScopeTypeId = 1 and rug.RoleId in (1,2,3) and uug.Deleted = 0 and uug.UserId = @userId) auth ON n.Id = auth.CatalogueNodeId + LEFT JOIN resources.ResourceVersionRatingSummary rs ON rs.ResourceVersionId = rv.Id + ORDER BY ma.ResourceActivityId DESC, rv.Title + + /* pagination logic */ + OFFSET @OffsetRows ROWS + FETCH NEXT @FetchRows ROWS ONLY + SELECT @TotalRecords = CASE WHEN COUNT(*) > 12 THEN @MaxRows ELSE COUNT(*) END FROM @MyActivity + END; +GO + + diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetMyCertificatesDashboardResources.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetMyCertificatesDashboardResources.sql index b75e45860..7beec0594 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetMyCertificatesDashboardResources.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetMyCertificatesDashboardResources.sql @@ -6,6 +6,7 @@ -- Modification History -- -- 24 Jun 2024 OA Initial Revision +-- 31 Jun 2024 PT Extracting functionality of certification with optional pagination so can be used on openapi and be single source of truth ------------------------------------------------------------------------------- CREATE PROCEDURE [resources].[GetMyCertificatesDashboardResources] @@ -25,77 +26,11 @@ BEGIN DECLARE @MaxRows INT = @MaxPageNUmber * @FetchRows DECLARE @OffsetRows INT = (@PageNumber - 1) * @FetchRows - DECLARE @MyActivity TABLE (ResourceId [int] NOT NULL PRIMARY KEY, ResourceActivityId [int] NOT NULL); - DECLARE @Resources TABLE (ResourceId [int] NOT NULL PRIMARY KEY, ResourceActivityCount [int] NOT NULL); + EXEC [resources].[GetAchievedcertificatedResourcesWithOptionalPagination] + @UserId = @UserId, + @MaxRows= @MaxRows, + @OffsetRows = @OffsetRows, + @FetchRows = @FetchRows, + @TotalRecords = @TotalRecords; - INSERT INTO @MyActivity - SELECT TOP (@MaxRows) ra.ResourceId, MAX(ra.Id) ResourceActivityId - FROM - activity.ResourceActivity ra - JOIN [resources].[Resource] r ON ra.ResourceId = r.Id - JOIN [resources].[ResourceVersion] rv ON rv.Id = ra.ResourceVersionId - LEFT JOIN [resources].[AssessmentResourceVersion] arv ON arv.ResourceVersionId = ra.ResourceVersionId - LEFT JOIN [activity].[AssessmentResourceActivity] ara ON ara.ResourceActivityId = ra.Id - LEFT JOIN [activity].[MediaResourceActivity] mar ON mar.ResourceActivityId = ra.Id - LEFT JOIN [activity].[ScormActivity] sa ON sa.ResourceActivityId = ra.Id - WHERE ra.UserId = @UserId AND rv.CertificateEnabled = 1 - AND ( - (r.ResourceTypeId IN (2, 7) AND ra.ActivityStatusId = 3 OR ra.ActivityStart < '2020-09-07 00:00:00 +00:00' OR mar.Id IS NOT NULL AND mar.PercentComplete = 100) - OR (r.ResourceTypeId = 6 AND (sa.CmiCoreLesson_status IN(3,5) OR (ra.ActivityStatusId IN(3, 5)))) - OR ((r.ResourceTypeId = 11 AND arv.AssessmentType = 2) AND (ara.Score >= arv.PassMark OR ra.ActivityStatusId IN(3, 5))) - OR ((r.ResourceTypeId = 11 AND arv.AssessmentType =1) AND (ara.Score >= arv.PassMark AND ra.ActivityStatusId IN(3, 5,7))) - OR (r.ResourceTypeId IN (1, 5, 8, 9, 10, 12) AND ra.ActivityStatusId = 3)) - GROUP BY ra.ResourceId - ORDER BY ResourceActivityId DESC - - SELECT r.Id AS ResourceId - ,( SELECT TOP 1 rr.OriginalResourceReferenceId - FROM [resources].[ResourceReference] rr - JOIN hierarchy.NodePath np on np.id = rr.NodePathId and np.NodeId = n.Id and np.Deleted = 0 - WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0 - ) AS ResourceReferenceID - ,r.CurrentResourceVersionId AS ResourceVersionId - ,r.ResourceTypeId AS ResourceTypeId - ,rv.Title - ,rv.Description - ,CASE - WHEN r.ResourceTypeId = 7 THEN - (SELECT vrv.DurationInMilliseconds from [resources].[VideoResourceVersion] vrv WHERE vrv.[ResourceVersionId] = r.CurrentResourceVersionId) - WHEN r.ResourceTypeId = 2 THEN - (SELECT vrv.DurationInMilliseconds from [resources].[AudioResourceVersion] vrv WHERE vrv.[ResourceVersionId] = r.CurrentResourceVersionId) - ELSE - NULL - END AS DurationInMilliseconds - ,CASE WHEN n.id = 1 THEN NULL ELSE cnv.Name END AS CatalogueName - ,cnv.Url AS Url - ,CASE WHEN n.id = 1 THEN NULL ELSE cnv.BadgeUrl END AS BadgeUrl - ,cnv.RestrictedAccess - ,CAST(CASE WHEN cnv.RestrictedAccess = 1 AND auth.CatalogueNodeId IS NULL THEN 0 ELSE 1 END AS bit) AS HasAccess - ,ub.Id AS BookMarkId - ,CAST(ISNULL(ub.[Deleted], 1) ^ 1 AS BIT) AS IsBookmarked - ,rvrs.AverageRating - ,rvrs.RatingCount -FROM @MyActivity ma -JOIN activity.ResourceActivity ra ON ra.id = ma.ResourceActivityId -JOIN resources.resourceversion rv ON rv.id = ra.ResourceVersionId AND rv.Deleted = 0 -JOIN Resources.Resource r ON r.Id = rv.ResourceId -JOIN hierarchy.Publication p ON rv.PublicationId = p.Id AND p.Deleted = 0 -JOIN resources.ResourceVersionRatingSummary rvrs ON rv.Id = rvrs.ResourceVersionId AND rvrs.Deleted = 0 -JOIN hierarchy.NodeResource nr ON r.Id = nr.ResourceId AND nr.Deleted = 0 -JOIN hierarchy.Node n ON n.Id = nr.NodeId AND n.Hidden = 0 AND n.Deleted = 0 -JOIN hierarchy.NodePath np ON np.NodeId = n.Id AND np.Deleted = 0 AND np.IsActive = 1 -JOIN hierarchy.NodeVersion nv ON nv.NodeId = np.CatalogueNodeId AND nv.VersionStatusId = 2 AND nv.Deleted = 0 -JOIN hierarchy.CatalogueNodeVersion cnv ON cnv.NodeVersionId = nv.Id AND cnv.Deleted = 0 -LEFT JOIN hub.UserBookmark ub ON ub.UserId = @UserId AND ub.ResourceReferenceId = (SELECT TOP 1 rr.OriginalResourceReferenceId - FROM [resources].[ResourceReference] rr - JOIN hierarchy.NodePath np on np.id = rr.NodePathId and np.NodeId = n.Id and np.Deleted = 0 - WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0) -LEFT JOIN ( SELECT DISTINCT CatalogueNodeId - FROM [hub].[RoleUserGroupView] rug JOIN hub.UserUserGroup uug ON rug.UserGroupId = uug.UserGroupId - WHERE rug.ScopeTypeId = 1 and rug.RoleId in (1,2,3) and uug.Deleted = 0 and uug.UserId = @userId) auth ON n.Id = auth.CatalogueNodeId -ORDER BY ma.ResourceActivityId DESC, rv.Title -OFFSET @OffsetRows ROWS -FETCH NEXT @FetchRows ROWS ONLY - - SELECT @TotalRecords = CASE WHEN COUNT(*) > 12 THEN @MaxRows ELSE COUNT(*) END FROM @MyActivity END \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetMyLearningCertificatesDashboardResources.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetMyLearningCertificatesDashboardResources.sql new file mode 100644 index 000000000..986e25423 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetMyLearningCertificatesDashboardResources.sql @@ -0,0 +1,101 @@ +------------------------------------------------------------------------------- +-- Author OA +-- Created 24 JUN 2024 Nov 2020 +-- Purpose Break down the GetDashboardResources SP to smaller SP for a specific data type +-- +-- Modification History +-- +-- 24 Jun 2024 OA Initial Revision +------------------------------------------------------------------------------- + +CREATE PROCEDURE [resources].[GetMyLearningCertificatesDashboardResources] + @UserId INT, + @PageNumber INT = 1, + @TotalRecords INT OUTPUT +AS +BEGIN + DECLARE @MaxPageNumber INT = 4 + + IF @PageNumber > 4 + BEGIN + SET @PageNumber = @MaxPageNumber + END + + DECLARE @FetchRows INT = 3 + DECLARE @MaxRows INT = @MaxPageNUmber * @FetchRows + DECLARE @OffsetRows INT = (@PageNumber - 1) * @FetchRows + + DECLARE @MyActivity TABLE (ResourceId [int] NOT NULL PRIMARY KEY, ResourceActivityId [int] NOT NULL); + DECLARE @Resources TABLE (ResourceId [int] NOT NULL PRIMARY KEY, ResourceActivityCount [int] NOT NULL); + + INSERT INTO @MyActivity + SELECT TOP (@MaxRows) ra.ResourceId, MAX(ra.Id) ResourceActivityId + FROM + activity.ResourceActivity ra + JOIN [resources].[Resource] r ON ra.ResourceId = r.Id + JOIN [resources].[ResourceVersion] rv ON rv.Id = ra.ResourceVersionId + LEFT JOIN [resources].[AssessmentResourceVersion] arv ON arv.ResourceVersionId = ra.ResourceVersionId + LEFT JOIN [activity].[AssessmentResourceActivity] ara ON ara.ResourceActivityId = ra.Id + LEFT JOIN [activity].[MediaResourceActivity] mar ON mar.ResourceActivityId = ra.Id + LEFT JOIN [activity].[ScormActivity] sa ON sa.ResourceActivityId = ra.Id + WHERE ra.UserId = @UserId AND rv.CertificateEnabled = 1 + AND ( + (r.ResourceTypeId IN (2, 7) AND ra.ActivityStatusId = 3 OR ra.ActivityStart < '2020-09-07 00:00:00 +00:00' OR mar.Id IS NOT NULL AND mar.PercentComplete = 100) + OR (r.ResourceTypeId = 6 AND (sa.CmiCoreLesson_status IN(3,5) OR (ra.ActivityStatusId IN(3, 5)))) + OR ((r.ResourceTypeId = 11 AND arv.AssessmentType = 2) AND (ara.Score >= arv.PassMark OR ra.ActivityStatusId IN(3, 5))) + OR ((r.ResourceTypeId = 11 AND arv.AssessmentType =1) AND (ara.Score >= arv.PassMark AND ra.ActivityStatusId IN(3, 5,7))) + OR (r.ResourceTypeId IN (1, 5, 8, 9, 10, 12) AND ra.ActivityStatusId = 3)) + GROUP BY ra.ResourceId + ORDER BY ResourceActivityId DESC + + SELECT r.Id AS ResourceId + ,( SELECT TOP 1 rr.OriginalResourceReferenceId + FROM [resources].[ResourceReference] rr + JOIN hierarchy.NodePath np on np.id = rr.NodePathId and np.NodeId = n.Id and np.Deleted = 0 + WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0 + ) AS ResourceReferenceID + ,r.CurrentResourceVersionId AS ResourceVersionId + ,r.ResourceTypeId AS ResourceTypeId + ,rv.Title + ,rv.Description + ,CASE + WHEN r.ResourceTypeId = 7 THEN + (SELECT vrv.DurationInMilliseconds from [resources].[VideoResourceVersion] vrv WHERE vrv.[ResourceVersionId] = r.CurrentResourceVersionId) + WHEN r.ResourceTypeId = 2 THEN + (SELECT vrv.DurationInMilliseconds from [resources].[AudioResourceVersion] vrv WHERE vrv.[ResourceVersionId] = r.CurrentResourceVersionId) + ELSE + NULL + END AS DurationInMilliseconds + ,CASE WHEN n.id = 1 THEN NULL ELSE cnv.Name END AS CatalogueName + ,cnv.Url AS Url + ,CASE WHEN n.id = 1 THEN NULL ELSE cnv.BadgeUrl END AS BadgeUrl + ,cnv.RestrictedAccess + ,CAST(CASE WHEN cnv.RestrictedAccess = 1 AND auth.CatalogueNodeId IS NULL THEN 0 ELSE 1 END AS bit) AS HasAccess + ,ub.Id AS BookMarkId + ,CAST(ISNULL(ub.[Deleted], 1) ^ 1 AS BIT) AS IsBookmarked + ,rvrs.AverageRating + ,rvrs.RatingCount +FROM @MyActivity ma +JOIN activity.ResourceActivity ra ON ra.id = ma.ResourceActivityId +JOIN resources.resourceversion rv ON rv.id = ra.ResourceVersionId AND rv.Deleted = 0 +JOIN Resources.Resource r ON r.Id = rv.ResourceId +JOIN hierarchy.Publication p ON rv.PublicationId = p.Id AND p.Deleted = 0 +JOIN resources.ResourceVersionRatingSummary rvrs ON rv.Id = rvrs.ResourceVersionId AND rvrs.Deleted = 0 +JOIN hierarchy.NodeResource nr ON r.Id = nr.ResourceId AND nr.Deleted = 0 +JOIN hierarchy.Node n ON n.Id = nr.NodeId AND n.Hidden = 0 AND n.Deleted = 0 +JOIN hierarchy.NodePath np ON np.NodeId = n.Id AND np.Deleted = 0 AND np.IsActive = 1 +JOIN hierarchy.NodeVersion nv ON nv.NodeId = np.CatalogueNodeId AND nv.VersionStatusId = 2 AND nv.Deleted = 0 +JOIN hierarchy.CatalogueNodeVersion cnv ON cnv.NodeVersionId = nv.Id AND cnv.Deleted = 0 +LEFT JOIN hub.UserBookmark ub ON ub.UserId = @UserId AND ub.ResourceReferenceId = (SELECT TOP 1 rr.OriginalResourceReferenceId + FROM [resources].[ResourceReference] rr + JOIN hierarchy.NodePath np on np.id = rr.NodePathId and np.NodeId = n.Id and np.Deleted = 0 + WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0) +LEFT JOIN ( SELECT DISTINCT CatalogueNodeId + FROM [hub].[RoleUserGroupView] rug JOIN hub.UserUserGroup uug ON rug.UserGroupId = uug.UserGroupId + WHERE rug.ScopeTypeId = 1 and rug.RoleId in (1,2,3) and uug.Deleted = 0 and uug.UserId = @userId) auth ON n.Id = auth.CatalogueNodeId +ORDER BY ma.ResourceActivityId DESC, rv.Title +OFFSET @OffsetRows ROWS +FETCH NEXT @FetchRows ROWS ONLY + + SELECT @TotalRecords = CASE WHEN COUNT(*) > 12 THEN @MaxRows ELSE COUNT(*) END FROM @MyActivity +END \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Repository.Interface/Hierarchy/ICatalogueNodeVersionRepository.cs b/WebAPI/LearningHub.Nhs.Repository.Interface/Hierarchy/ICatalogueNodeVersionRepository.cs index eb3eed968..1b68a3aef 100644 --- a/WebAPI/LearningHub.Nhs.Repository.Interface/Hierarchy/ICatalogueNodeVersionRepository.cs +++ b/WebAPI/LearningHub.Nhs.Repository.Interface/Hierarchy/ICatalogueNodeVersionRepository.cs @@ -121,5 +121,21 @@ public interface ICatalogueNodeVersionRepository : IGenericRepositoryThe catalogue name. /// The catalogue's node id. Task GetNodeIdByCatalogueName(string catalogueName); + + /// + /// Gets the catalogues count in alphabet list. + /// + /// The userId. + /// The catalogues alphabet count list. + List GetAllCataloguesAlphaCount(int userId); + + /// + /// Gets catalogues based on filter character. + /// + /// The pageSize. + /// The filterChar. + /// The userId. + /// The catalogues. + Task> GetAllCataloguesAsync(int pageSize, string filterChar, int userId); } } diff --git a/WebAPI/LearningHub.Nhs.Repository.Interface/IRoleUserGroupRepository.cs b/WebAPI/LearningHub.Nhs.Repository.Interface/IRoleUserGroupRepository.cs index 0a807c18d..cdffec209 100644 --- a/WebAPI/LearningHub.Nhs.Repository.Interface/IRoleUserGroupRepository.cs +++ b/WebAPI/LearningHub.Nhs.Repository.Interface/IRoleUserGroupRepository.cs @@ -55,5 +55,13 @@ public interface IRoleUserGroupRepository : IGenericRepository /// The userGroupId. /// A list of RoleUserGroupViewModel. Task> GetRoleUserGroupViewModelsByUserId(int userId); + + /// + /// The get all for search. + /// + /// The catalogueNodeId. + /// The userId. + /// The . + Task> GetAllforSearch(int catalogueNodeId, int userId); } } diff --git a/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj b/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj index 2fa729986..a2ac5f308 100644 --- a/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj +++ b/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj @@ -9,7 +9,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Repository/Hierarchy/CatalogueNodeVersionRepository.cs b/WebAPI/LearningHub.Nhs.Repository/Hierarchy/CatalogueNodeVersionRepository.cs index bc8feee2d..e65bd58ef 100644 --- a/WebAPI/LearningHub.Nhs.Repository/Hierarchy/CatalogueNodeVersionRepository.cs +++ b/WebAPI/LearningHub.Nhs.Repository/Hierarchy/CatalogueNodeVersionRepository.cs @@ -355,5 +355,38 @@ join nv in this.DbContext.NodeVersion.AsNoTracking() on cnv.NodeVersionId equals where cnv.Name == catalogueName && cnv.Deleted == false select nv.NodeId).FirstOrDefaultAsync(); } + + /// + /// Gets catalogues count based on alphabets. + /// + /// The userId. + /// resources. + public List GetAllCataloguesAlphaCount(int userId) + { + var param0 = new SqlParameter("@userId", SqlDbType.Int) { Value = userId }; + + var result = this.DbContext.AllCatalogueAlphabetModel.FromSqlRaw("[hierarchy].[GetCataloguesCount] @userid", param0) + .AsNoTracking().ToList(); + return result; + } + + /// + /// Gets catalogues based on filter character. + /// + /// The pageSize. + /// The filterChar. + /// The userId. + /// resources. + public async Task> GetAllCataloguesAsync(int pageSize, string filterChar, int userId) + { + var param0 = new SqlParameter("@userId", SqlDbType.Int) { Value = userId }; + var param1 = new SqlParameter("@filterChar", SqlDbType.NVarChar, 10) { Value = filterChar.Trim() }; + var param2 = new SqlParameter("@OffsetRows", SqlDbType.Int) { Value = 0 }; + var param3 = new SqlParameter("@fetchRows", SqlDbType.Int) { Value = pageSize }; + + var result = await this.DbContext.AllCatalogueViewModel.FromSqlRaw("[hierarchy].[GetCatalogues] @userId, @filterChar, @OffsetRows, @fetchRows", param0, param1, param2, param3) + .AsNoTracking().ToListAsync(); + return result; + } } } diff --git a/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj b/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj index bca29eeb0..0f94d7953 100644 --- a/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj +++ b/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj @@ -9,7 +9,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Repository/LearningHubDbContext.cs b/WebAPI/LearningHub.Nhs.Repository/LearningHubDbContext.cs index ef4775cd8..1d94dc9b6 100644 --- a/WebAPI/LearningHub.Nhs.Repository/LearningHubDbContext.cs +++ b/WebAPI/LearningHub.Nhs.Repository/LearningHubDbContext.cs @@ -770,6 +770,16 @@ public LearningHubDbContextOptions Options /// public virtual DbSet MyLearningActivity { get; set; } + /// + /// Gets or sets the AllCatalogueAlphabet. + /// + public virtual DbSet AllCatalogueAlphabetModel { get; set; } + + /// + /// Gets or sets the AllCatalogueAlphabet. + /// + public virtual DbSet AllCatalogueViewModel { get; set; } + /// /// The on model creating. /// diff --git a/WebAPI/LearningHub.Nhs.Repository/Resources/ResourceVersionRepository.cs b/WebAPI/LearningHub.Nhs.Repository/Resources/ResourceVersionRepository.cs index 584da6344..cee3e6a07 100644 --- a/WebAPI/LearningHub.Nhs.Repository/Resources/ResourceVersionRepository.cs +++ b/WebAPI/LearningHub.Nhs.Repository/Resources/ResourceVersionRepository.cs @@ -690,7 +690,7 @@ public List GetContributions(int userId, ResourceContri switch (dashboardType) { case "my-certificates": - dashboardResources = this.DbContext.DashboardResourceDto.FromSqlRaw("resources.GetMyCertificatesDashboardResources @userId, @pageNumber, @totalRows output", param0, param1, param2).ToList(); + dashboardResources = this.DbContext.DashboardResourceDto.FromSqlRaw("resources.GetMyLearningCertificatesDashboardResources @userId, @pageNumber, @totalRows output", param0, param1, param2).ToList(); break; case "my-recent-completed": dashboardResources = this.DbContext.DashboardResourceDto.FromSqlRaw("resources.GetMyRecentCompletedDashboardResources @userId, @pageNumber, @totalRows output", param0, param1, param2).ToList(); diff --git a/WebAPI/LearningHub.Nhs.Repository/RoleUserGroupRepository.cs b/WebAPI/LearningHub.Nhs.Repository/RoleUserGroupRepository.cs index 906c335cc..74a284696 100644 --- a/WebAPI/LearningHub.Nhs.Repository/RoleUserGroupRepository.cs +++ b/WebAPI/LearningHub.Nhs.Repository/RoleUserGroupRepository.cs @@ -63,6 +63,20 @@ public async Task GetByRoleIdUserGroupIdScopeIdAsync(int roleId, .Where(n => n.Deleted == false); } + /// + /// The get all for Search. + /// + /// The catalogueNodeId. + /// The userId. + /// The . + public async Task> GetAllforSearch(int catalogueNodeId, int userId) + { + return await this.DbContext.RoleUserGroup.Where(rug => rug.Scope.CatalogueNodeId == catalogueNodeId) + .Include(n => n.UserGroup).ThenInclude(u => u.UserUserGroup.Where(p => p.UserId == userId)) + .Include(n => n.Scope).AsNoTracking() + .ToListAsync(); + } + /// /// The get by role id and catalogue id. /// diff --git a/WebAPI/LearningHub.Nhs.Services.Interface/ICatalogueService.cs b/WebAPI/LearningHub.Nhs.Services.Interface/ICatalogueService.cs index 283ea7ae9..69dc89908 100644 --- a/WebAPI/LearningHub.Nhs.Services.Interface/ICatalogueService.cs +++ b/WebAPI/LearningHub.Nhs.Services.Interface/ICatalogueService.cs @@ -192,6 +192,14 @@ public interface ICatalogueService /// The roleUserGroups. List GetRoleUserGroupsForCatalogue(int catalogueNodeId, bool includeUser = false); + /// + /// The GetRolesForCatalogueSearch. + /// + /// The catalogueNodeId. + /// The current user. + /// The roleUserGroups. + Task> GetRoleUserGroupsForCatalogueSearch(int catalogueNodeId, int userId); + /// /// The GetLatestCatalogueAccessRequestAsync. /// @@ -238,5 +246,14 @@ public interface ICatalogueService /// The catalogueAccessRequestId. /// The catalogue access request. Task AccessRequestAsync(int userId, int catalogueAccessRequestId); + + /// + /// GetAllCataloguesAsync. + /// + /// The pageSize. + /// filterChar. + /// userId. + /// The allcatalogue result based on letters. + Task GetAllCataloguesAsync(int pageSize, string filterChar, int userId); } } diff --git a/WebAPI/LearningHub.Nhs.Services.Interface/ISearchService.cs b/WebAPI/LearningHub.Nhs.Services.Interface/ISearchService.cs index ef44e5076..2f1e0a63c 100644 --- a/WebAPI/LearningHub.Nhs.Services.Interface/ISearchService.cs +++ b/WebAPI/LearningHub.Nhs.Services.Interface/ISearchService.cs @@ -168,5 +168,12 @@ public interface ISearchService /// The . /// Task SendCatalogueSearchEventAsync(SearchActionCatalogueModel searchActionCatalogueModel); + + /// + /// Gets AllCatalogue search results async. + /// + /// The allcatalog search request model. + /// The . + Task GetAllCatalogueSearchResultsAsync(AllCatalogueSearchRequestModel catalogSearchRequestModel); } } diff --git a/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj b/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj index 13e00879d..59748eeb8 100644 --- a/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj +++ b/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj @@ -16,7 +16,7 @@ - + all diff --git a/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj b/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj index 87beb0d40..7f312c998 100644 --- a/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj +++ b/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Services/CatalogueService.cs b/WebAPI/LearningHub.Nhs.Services/CatalogueService.cs index a074e2b99..f6ca53582 100644 --- a/WebAPI/LearningHub.Nhs.Services/CatalogueService.cs +++ b/WebAPI/LearningHub.Nhs.Services/CatalogueService.cs @@ -401,8 +401,18 @@ public List GetRoleUserGroupsForCatalogue(int catalogueNodeId, bo query = query.Include(x => x.UserGroup).ThenInclude(x => x.UserUserGroup); } - return query.Where(x => x.Scope.CatalogueNodeId == catalogueNodeId) - .ToList(); + return query.Where(x => x.Scope.CatalogueNodeId == catalogueNodeId).ToList(); + } + + /// + /// The GetRolesForCatalogueSearch. + /// + /// The catalogueNodeId. + /// The current user. + /// The roleUserGroups. + public async Task> GetRoleUserGroupsForCatalogueSearch(int catalogueNodeId, int userId) + { + return await this.roleUserGroupRepository.GetAllforSearch(catalogueNodeId, userId); } /// @@ -954,6 +964,68 @@ public async Task AccessRequestAsync(int userId return vm; } + /// + /// GetAllCataloguesAsync. + /// + /// The pageSize. + /// The filterChar. + /// The userId. + /// A representing the result of the asynchronous operation. + public async Task GetAllCataloguesAsync(int pageSize, string filterChar, int userId) + { + var catalogueAlphaCount = this.catalogueNodeVersionRepository.GetAllCataloguesAlphaCount(userId); + var filterCharMod = filterChar.Trim() == "0-9" ? "[0-9]" : filterChar; + var count = catalogueAlphaCount.FirstOrDefault(ca => ca.Alphabet == filterChar.ToUpper()).Count; + string prevChar = null, nextChar = null, curChar = null; + var filterCharIndex = catalogueAlphaCount.FindIndex(ca => ca.Alphabet == filterChar.ToUpper()); + + // check count and assign prev and next letter + if (count != 0) + { + for (int i = 0; i < catalogueAlphaCount.Count; i++) + { + if (i == filterCharIndex && i == 0) + { + prevChar = null; + } + + if (i == filterCharIndex && i == catalogueAlphaCount.Count - 1) + { + nextChar = null; + } + + if (catalogueAlphaCount[i].Count > 0 && i < filterCharIndex) + { + curChar = catalogueAlphaCount[i].Alphabet; + prevChar = curChar; + } + + if (catalogueAlphaCount[i].Count > 0 && i > filterCharIndex) + { + curChar = catalogueAlphaCount[i].Alphabet; + nextChar = curChar; + break; + } + } + } + + var catalogues = await this.catalogueNodeVersionRepository.GetAllCataloguesAsync(pageSize, filterCharMod, userId); + foreach (var catalogue in catalogues) + { + catalogue.Providers = await this.providerService.GetByCatalogueVersionIdAsync(catalogue.NodeVersionId); + } + + var response = new AllCatalogueResponseViewModel + { + CataloguesCount = catalogueAlphaCount, + Catalogues = catalogues, + FilterChar = filterChar.ToUpper(), + PrevChar = prevChar, + NextChar = nextChar, + }; + return response; + } + /// /// The RecordNodeActivity. /// diff --git a/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj b/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj index b479d88df..e9fa30eef 100644 --- a/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj +++ b/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj @@ -13,7 +13,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Services/ResourceService.cs b/WebAPI/LearningHub.Nhs.Services/ResourceService.cs index 808bd8448..76d8df791 100644 --- a/WebAPI/LearningHub.Nhs.Services/ResourceService.cs +++ b/WebAPI/LearningHub.Nhs.Services/ResourceService.cs @@ -1772,7 +1772,8 @@ public async Task AddResourceVersionKeywordAsync(Re bool doesKeywordAlreadyExist = await this.resourceVersionKeywordRepository.DoesResourceVersionKeywordAlreadyExistAsync(rvk.ResourceVersionId, rvk.Keyword); if (doesKeywordAlreadyExist) { - return new LearningHubValidationResult(false, "This keyword has already been added."); + retVal.CreatedId = 0; + return retVal; } retVal.CreatedId = await this.resourceVersionKeywordRepository.CreateAsync(userId, rvk); diff --git a/WebAPI/LearningHub.Nhs.Services/SearchService.cs b/WebAPI/LearningHub.Nhs.Services/SearchService.cs index 321313bd3..99ea20895 100644 --- a/WebAPI/LearningHub.Nhs.Services/SearchService.cs +++ b/WebAPI/LearningHub.Nhs.Services/SearchService.cs @@ -11,7 +11,7 @@ namespace LearningHub.Nhs.Services using LearningHub.Nhs.Models.Entities.Analytics; using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Search; - using LearningHub.Nhs.Models.Search.SearchFeedback; + using LearningHub.Nhs.Models.Search.SearchClick; using LearningHub.Nhs.Models.Validation; using LearningHub.Nhs.Services.Helpers; using LearningHub.Nhs.Services.Interface; @@ -512,7 +512,7 @@ public async Task CreateCatalogueSearchTermEvent(Ca /// public async Task SendResourceSearchEventClickAsync(SearchActionResourceModel searchActionResourceModel) { - var searchClickPayloadModel = this.mapper.Map(searchActionResourceModel); + var searchClickPayloadModel = this.mapper.Map(searchActionResourceModel); searchClickPayloadModel.TimeOfClick = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); searchClickPayloadModel.SearchSignal.ProfileSignature.ApplicationId = ApplicationId; searchClickPayloadModel.SearchSignal.ProfileSignature.ProfileType = ProfileType; @@ -532,7 +532,7 @@ public async Task SendResourceSearchEventClickAsync(SearchActionResourceMo /// public async Task SendCatalogueSearchEventAsync(SearchActionCatalogueModel searchActionCatalogueModel) { - var searchClickPayloadModel = this.mapper.Map(searchActionCatalogueModel); + var searchClickPayloadModel = this.mapper.Map(searchActionCatalogueModel); searchClickPayloadModel.TimeOfClick = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); searchClickPayloadModel.SearchSignal.ProfileSignature.ApplicationId = ApplicationId; searchClickPayloadModel.SearchSignal.ProfileSignature.ProfileType = ProfileType; @@ -541,6 +541,53 @@ public async Task SendCatalogueSearchEventAsync(SearchActionCatalogueModel return await this.SendSearchEventClickAsync(searchClickPayloadModel, false); } + /// + /// Gets AllCatalogue search results from findwise api call. + /// + /// The allcatalog search request model. + /// The . + public async Task GetAllCatalogueSearchResultsAsync(AllCatalogueSearchRequestModel catalogSearchRequestModel) + { + var viewmodel = new SearchAllCatalogueResultModel(); + try + { + var offset = catalogSearchRequestModel.PageIndex * catalogSearchRequestModel.PageSize; + var client = await this.FindWiseHttpClient.GetClient(this.settings.Findwise.SearchUrl); + var request = string.Format( + this.settings.Findwise.UrlSearchComponent + "?offset={1}&hits={2}&q={3}&token={4}", + this.settings.Findwise.CollectionIds.Catalogue, + offset, + catalogSearchRequestModel.PageSize, + this.EncodeSearchText(catalogSearchRequestModel.SearchText), + this.settings.Findwise.Token); + + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + viewmodel = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + this.Logger.LogError($"Get AllCatalogue Search Result failed in FindWise, HTTP Status Code:{response.StatusCode}"); + throw new Exception("AccessDenied to FindWise Server"); + } + else + { + var error = response.Content.ReadAsStringAsync().Result.ToString(); + this.Logger.LogError($"Get AllCatalogue Search Result failed in FindWise, HTTP Status Code:{response.StatusCode}, Error Message:{error}"); + throw new Exception("Error with FindWise Server"); + } + + return viewmodel; + } + catch (Exception) + { + throw; + } + } + /// /// Send search click payload. /// @@ -549,7 +596,7 @@ public async Task SendCatalogueSearchEventAsync(SearchActionCatalogueModel /// /// The . /// - private async Task SendSearchEventClickAsync(SearchFeedbackPayloadModel searchClickPayloadModel, bool isResource) + private async Task SendSearchEventClickAsync(SearchClickPayloadModel searchClickPayloadModel, bool isResource) { var eventType = isResource ? "resource" : "catalog"; diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj index 5a7f79921..539697d49 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj @@ -24,7 +24,7 @@ - + all diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj index 2306024b3..eb924766b 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj index c45bd555b..6f4c807b7 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj @@ -10,7 +10,7 @@ - + all diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj index 2501535f9..df5c3c6c3 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj @@ -9,7 +9,7 @@ - + diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj index ac9a749f6..16a31717c 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj index b5fc07022..440ec325e 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive