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/LearningHub.Nhs.AdminUI.csproj b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj index 21ec69f5e..798171c31 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/Controllers/CatalogueController.cs b/LearningHub.Nhs.WebUI/Controllers/CatalogueController.cs index efa8cd4f2..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, @@ -124,6 +125,7 @@ public async Task Index(int pageIndex = 1, string term = null) NodeId = int.Parse(t.Id), BadgeUrl = t.BadgeUrl, Providers = t.Providers, + ClickPayload = t.Click.Payload, }).ToList(); } else 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/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index 9e46194b1..3f3f09de7 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/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/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/Views/Catalogue/Catalogues.cshtml b/LearningHub.Nhs.WebUI/Views/Catalogue/Catalogues.cshtml index 024f01b95..b976651b2 100644 --- a/LearningHub.Nhs.WebUI/Views/Catalogue/Catalogues.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Catalogue/Catalogues.cshtml @@ -1,14 +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; - string cardStyle = "card-provider-details--blank"; } @section styles{ @@ -70,7 +86,7 @@

- @item.Name + @item.Name

diff --git a/LearningHub.Nhs.WebUI/Views/Search/_CatalogueSearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_CatalogueSearchResult.cshtml index 56e54eff1..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; } } @@ -71,7 +73,7 @@

- @item.Name + @item.Name

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/_FooterPartial.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/_FooterPartial.cshtml index 29446946a..2c6e87651 100644 --- a/LearningHub.Nhs.WebUI/Views/Shared/_FooterPartial.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Shared/_FooterPartial.cshtml @@ -3,28 +3,27 @@ @using LearningHub.Nhs.WebUI.Configuration; @inject IOptions settings; - +
-@functions{ +@functions { public bool SystemOffline() { return ViewContext.RouteData.Values["controller"].ToString() == "Offline"; 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..6062a7b7a 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..3d46e57aa 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..26457744e 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..2614ed8b7 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,10 @@ public async Task Search(ResourceSearchRequest query) } private async Task> GetResourceMetadataViewModels( - FindwiseResultModel findwiseResultModel) + FindwiseResultModel findwiseResultModel, int? currentUserId) { + List resourceActivities = new List() { }; + List resourceMetadataViewModels = new List() { }; var documentsFound = findwiseResultModel.SearchResults?.DocumentList.Documents?.ToList() ?? new List(); var findwiseResourceIds = documentsFound.Select(d => int.Parse(d.Id)).ToList(); @@ -90,7 +95,15 @@ private async Task> GetResourceMetadataViewModel var resourcesFound = await this.resourceRepository.GetResourcesFromIds(findwiseResourceIds); - var resourceMetadataViewModels = resourcesFound.Select(this.MapToViewModel) + 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() { }; + } + + resourceMetadataViewModels = resourcesFound.Select(resource => this.MapToViewModel(resource, resourceActivities.Where(x => x.ResourceId == resource.Id).ToList())) .OrderBySequence(findwiseResourceIds) .ToList(); @@ -108,11 +121,19 @@ private async Task> GetResourceMetadataViewModel 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 +152,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/LearningHub.Nhs.Api.csproj b/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj index 073baa1ff..b1b3f6e95 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 722cefdee..4a089b9a3 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 d47e3d417..02c58af7b 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 f19a40bbe..8c11263af 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -200,9 +200,6 @@ - - - @@ -511,19 +508,31 @@ - + + + + + + + + + + + + + @@ -594,4 +603,4 @@ - + \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/ELFH Database Scripts/Set up LH Tenant.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/ELFH Database Scripts/Set up LH Tenant.sql index 3fae0f800..36cc3ff9d 100644 --- a/WebAPI/LearningHub.Nhs.Database/Scripts/ELFH Database Scripts/Set up LH Tenant.sql +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/ELFH Database Scripts/Set up LH Tenant.sql @@ -1,4 +1,5 @@ -/* + +/* Script for adding the Learning Hub tenant */ --Required Information - UPDATE BEFORE RUNNING diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/External/AddDigitalLearningSolutionsSso.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/External/AddDigitalLearningSolutionsSso.sql deleted file mode 100644 index 27c0d2056..000000000 --- a/WebAPI/LearningHub.Nhs.Database/Scripts/External/AddDigitalLearningSolutionsSso.sql +++ /dev/null @@ -1,23 +0,0 @@ -/* -Script for adding DigitalLearningSolutionsSso ExternalSystemUser to users which already have DigitalLearningSolutions ExternalSystemUser -*/ -DECLARE @DigitalLearningSolutionsId INT; -DECLARE @DigitalLearningSolutionsSsoId INT; - -SELECT @DigitalLearningSolutionsId = id - FROM [external].[ExternalSystem] - WHERE code = 'DigitalLearningSolutions'; - -SELECT @DigitalLearningSolutionsSsoId = id - FROM [external].[ExternalSystem] - WHERE code = 'DigitalLearningSolutionsSso'; - -INSERT INTO [external].[ExternalSystemUser] (UserId, ExternalSystemId, Deleted, CreateUserId, CreateDate, AmendUserId, AmendDate) -SELECT UserId, @DigitalLearningSolutionsId, Deleted, 4, SYSDATETIMEOFFSET(), 4, SYSDATETIMEOFFSET() -FROM [external].[ExternalSystemUser] -WHERE ExternalSystemId = @DigitalLearningSolutionsSsoId - AND userId NOT IN ( - SELECT userId - FROM [external].[ExternalSystemUser] - WHERE ExternalSystemId = @DigitalLearningSolutionsId - ); \ No newline at end of file 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/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/LearningHub.Nhs.Repository.Interface.csproj b/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj index 35d70c810..a6c146998 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/LearningHub.Nhs.Repository.csproj b/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj index 5e7e65365..3173c4895 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/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.Services.Interface/LearningHub.Nhs.Services.Interface.csproj b/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj index 5d2f278d4..a7a9f0048 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 9869ac4be..d64166759 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/LearningHub.Nhs.Services.csproj b/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj index 44fefe844..c8b4ec333 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 a247d9aa1..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; @@ -596,7 +596,7 @@ public async Task GetAllCatalogueSearchResultsAsy /// /// 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 de8df1a58..f59f836ec 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 52929fbd8..306bfba43 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 a3951c927..fe0eec05d 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 7efd53fab..5a85cff3e 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 ad73a25cc..c167e11e4 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 861270f13..6c31da645 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