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..4ea6a2e6a 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj +++ b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj @@ -89,7 +89,7 @@ - + diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml b/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml index e0e81edcf..19ed8d399 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml +++ b/AdminUI/LearningHub.Nhs.AdminUI/Views/Catalogue/Edit.cshtml @@ -162,6 +162,11 @@ +
+
+ +
+
@@ -341,6 +346,9 @@ } else { $('#add-keyword').removeAttr('disabled'); } + + $('#keyword-error-span').hide(); + $('#keyword-error-span').html(''); }); $('#add-keyword').on('click', function () { @@ -352,9 +360,11 @@ // Split the input value by commas and trim each keyword var values = value.split(',').map(function (item) { - return item.trim(); + return item.trim().toLowerCase(); }); + var duplicateKeywords = []; + $('#keyword-error-span').hide(); values.forEach(function (value) { if (value && keywords.indexOf(value) === -1) { keywords.push(value); @@ -368,9 +378,15 @@ $(x).attr('name', "Keywords[" + i + "]"); }); } + else + { + duplicateKeywords.push(value); + $('#keyword-error-span').show(); + $('#keyword-error-span').html('The keyword(s) have already been added : ' + duplicateKeywords.join(', ')) + } }); - $keywordInput.val(""); + $keywordInput.val(""); if (keywords.length > 4) { $('#add-keyword').attr('disabled', 'disabled'); $('#add-keyword-input').attr('disabled', 'disabled'); @@ -441,4 +457,4 @@ -} \ No newline at end of file +} diff --git a/LearningHub.Nhs.WebUI/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..78ab26256 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -108,7 +108,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/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/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj index 0047f4483..0af6830e4 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj @@ -16,7 +16,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs index 3675959a6..a4c1fbf4c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs @@ -1,7 +1,9 @@ namespace LearningHub.Nhs.OpenApi.Models.ViewModels { + using LearningHub.Nhs.Models.Entities.Activity; using System.Collections.Generic; + /// /// Class. /// @@ -23,20 +25,25 @@ public ResourceMetadataViewModel() /// . /// . /// . + /// . public ResourceMetadataViewModel( int resourceId, string title, string description, List references, string resourceType, - decimal rating) + int? majorVersion, + decimal rating, + List userSummaryActivityStatuses) { this.ResourceId = resourceId; this.Title = title; this.Description = description; this.References = references; this.ResourceType = resourceType; + this.MajorVersion = majorVersion; this.Rating = rating; + this.UserSummaryActivityStatuses = userSummaryActivityStatuses; } /// @@ -64,9 +71,20 @@ public ResourceMetadataViewModel( /// public string ResourceType { get; set; } + /// + /// Gets or sets . + /// + public int? MajorVersion { get; set; } + + /// /// Gets or sets . /// public decimal Rating { get; set; } + + /// + /// Gets or sets . + /// + public List UserSummaryActivityStatuses { get; set; } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs index cf31bdf54..41d1b197b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs @@ -1,3 +1,6 @@ +using LearningHub.Nhs.Models.Entities.Activity; +using System.Collections.Generic; + namespace LearningHub.Nhs.OpenApi.Models.ViewModels { /// @@ -14,8 +17,10 @@ public class ResourceReferenceWithResourceDetailsViewModel /// . /// . /// . + /// /// . /// . + /// public ResourceReferenceWithResourceDetailsViewModel( int resourceId, int refId, @@ -23,17 +28,21 @@ public ResourceReferenceWithResourceDetailsViewModel( string description, CatalogueViewModel catalogueViewModel, string resourceType, + int? majorVersion, decimal rating, - string link) + string link, + List userSummaryActivityStatuses) { this.ResourceId = resourceId; this.RefId = refId; this.Title = title; this.Description = description; this.Catalogue = catalogueViewModel; + this.MajorVersion = majorVersion; this.ResourceType = resourceType; this.Rating = rating; this.Link = link; + this.UserSummaryActivityStatuses = userSummaryActivityStatuses; } /// @@ -66,14 +75,27 @@ public ResourceReferenceWithResourceDetailsViewModel( /// public string ResourceType { get; } + + /// + /// Gets . + /// + public int? MajorVersion { get; } + /// /// Gets . /// + /// + public decimal Rating { get; } /// /// Gets . /// public string Link { get; } + + /// + /// Gets . + /// + public List UserSummaryActivityStatuses { get; } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs index 50eff71f6..52b3dee08 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs @@ -2,6 +2,7 @@ namespace LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories { using System.Collections.Generic; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; /// @@ -23,5 +24,19 @@ public interface IResourceRepository /// Resource references. public Task> GetResourceReferencesByOriginalResourceReferenceIds( IEnumerable originalResourceReferenceIds); + + /// + /// Gets resource activity for resourceReferenceIds and userIds. + /// + /// . + /// + /// ResourceActivityDTO. + Task> GetResourceActivityPerResourceMajorVersion(IEnumerable? resourceReferenceIds, IEnumerable? userIds); + + /// + /// GetAchievedCertificatedResourceIds + /// + /// . + public Task> GetAchievedCertificatedResourceIds(int currentUserId); } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs index 020a4f1f5..c1ebf721b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs @@ -295,6 +295,11 @@ public LearningHubDbContext(LearningHubDbContextOptions options) /// public virtual DbSet FileChunkDetail { get; set; } + /// + /// Gets or sets the ResourceActivityDto. These are not entities. They are returned from the [activity].[GetResourceActivityPerResourceMajorVersion] stored proc.. + /// + public virtual DbSet ResourceActivityDTO { get; set; } + /// /// Gets or sets the RecentlyAddedResources. These are not entities. They are returned from the [resources].[GetRecentlyAddedResources] stored proc.. /// @@ -312,14 +317,14 @@ public LearningHubDbContext(LearningHubDbContextOptions options) public virtual DbSet DashboardResourceDto { get; set; } /// - /// Gets or sets the ScormContentDetailsViewModel. + /// Gets or sets the ExternalContentDetailsViewModel. /// - public virtual DbSet ScormContentDetailsViewModel { get; set; } + public virtual DbSet ExternalContentDetailsViewModel { get; set; } /// - /// Gets or sets the ScormContentServerViewModel. + /// Gets or sets the ContentServerViewModel. /// - public virtual DbSet ScormContentServerViewModel { get; set; } + public virtual DbSet ContentServerViewModel { get; set; } /// /// Gets or sets the DashboardCatalogueDto @@ -520,12 +525,12 @@ public LearningHubDbContext(LearningHubDbContextOptions options) /// /// Gets or sets the whole slide image annotation. /// - public virtual DbSet WholeSlideImageAnnotation { get; set; } + public virtual DbSet ImageAnnotation { get; set; } /// /// Gets or sets the whole slide image annotation mark. /// - public virtual DbSet WholeSlideImageAnnotationMark { get; set; } + public virtual DbSet ImageAnnotationMark { get; set; } /// /// Gets or sets the media block. diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs index 668d318b1..8db1cd876 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs @@ -110,8 +110,8 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs index 1bdbb69c8..42d2e309f 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs @@ -17,8 +17,6 @@ protected override void InternalMap(EntityTypeBuilder entity) { entity.ToTable("PageSectionDetail", "content"); - entity.Property(e => e.AssetPositionId).HasDefaultValueSql("((2))"); - entity.Property(e => e.BackgroundColour).HasMaxLength(20); entity.Property(e => e.Description).HasMaxLength(512); @@ -31,7 +29,7 @@ protected override void InternalMap(EntityTypeBuilder entity) entity.Property(e => e.TextColour).HasMaxLength(20); - entity.Property(e => e.Title).HasMaxLength(128); + entity.Property(e => e.SectionTitle).HasMaxLength(128); entity.Property(e => e.DeletePending).IsRequired(false); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs index 62ebffd0c..0b8bb7428 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs @@ -68,10 +68,6 @@ protected void InternalMap(EntityTypeBuilder modelBuilder) modelBuilder.Property(e => e.UserId) .HasColumnName("UserId"); - modelBuilder.HasOne(d => d.User) - .WithMany(p => p.Logs) - .HasForeignKey(d => d.UserId) - .OnDelete(DeleteBehavior.ClientSetNull); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs index 18a94369a..00f7f46cb 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs @@ -7,7 +7,7 @@ /// /// The whole slide image annotation map. /// - public class WholeSlideImageAnnotationMap : BaseEntityMap + public class ImageAnnotationMap : BaseEntityMap { /// /// The internal map. @@ -15,15 +15,15 @@ public class WholeSlideImageAnnotationMap : BaseEntityMap /// The model builder. /// - protected override void InternalMap(EntityTypeBuilder modelBuilder) + protected override void InternalMap(EntityTypeBuilder modelBuilder) { - modelBuilder.ToTable("WholeSlideImageAnnotation", "resources"); + modelBuilder.ToTable("ImageAnnotation", "resources"); modelBuilder.HasOne(a => a.WholeSlideImage) - .WithMany(i => i.WholeSlideImageAnnotations) + .WithMany(i => i.ImageAnnotations) .HasForeignKey(a => a.WholeSlideImageId) .OnDelete(DeleteBehavior.Cascade) - .HasConstraintName("FK_WholeSlideImageAnnotation_WholeSlideImageId"); + .HasConstraintName("FK_ImageAnnotation_WholeSlideImageId"); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs index 2720b23db..9db0a6db6 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs @@ -7,7 +7,7 @@ /// /// The whole slide image annotation map. /// - public class WholeSlideImageAnnotationMarkMap : BaseEntityMap + public class ImageAnnotationMarkMap : BaseEntityMap { /// /// The internal map. @@ -15,15 +15,15 @@ public class WholeSlideImageAnnotationMarkMap : BaseEntityMap /// The model builder. /// - protected override void InternalMap(EntityTypeBuilder modelBuilder) + protected override void InternalMap(EntityTypeBuilder modelBuilder) { - modelBuilder.ToTable("WholeSlideImageAnnotationMark", "resources"); + modelBuilder.ToTable("ImageAnnotationMark", "resources"); - modelBuilder.HasOne(a => a.WholeSlideImageAnnotation) - .WithMany(i => i.WholeSlideImageAnnotationMarks) - .HasForeignKey(a => a.WholeSlideImageAnnotationId) + modelBuilder.HasOne(a => a.ImageAnnotation) + .WithMany(i => i.ImageAnnotationMarks) + .HasForeignKey(a => a.ImageAnnotationId) .OnDelete(DeleteBehavior.Cascade) - .HasConstraintName("FK_WholeSlideImageAnnotationMark_WholeSlideImageAnnotationId"); + .HasConstraintName("FK_ImageAnnotationMark_ImageAnnotationId"); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs index a87bee808..d96c3b4fe 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs @@ -39,6 +39,9 @@ protected override void InternalMap(EntityTypeBuilder modelBuil .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_ResourceVersion_Resource"); + modelBuilder.Property(e => e.ResourceAccessibilityEnum).HasColumnName("ResourceAccessibilityId") + .HasConversion(); + modelBuilder.Property(e => e.VersionStatusEnum).HasColumnName("VersionStatusId") .HasConversion(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs index a826c627e..79b7a239b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs @@ -1,11 +1,18 @@ namespace LearningHub.Nhs.OpenApi.Repositories.Repositories { + using System; using System.Collections.Generic; + using System.ComponentModel; + using System.Data; using System.Linq; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Dashboard; + using LearningHub.Nhs.Models.Entities; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.OpenApi.Repositories.EntityFramework; using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; + using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; /// @@ -69,5 +76,48 @@ public async Task> GetResourceReferencesByOrigina .ThenInclude(r => r.ResourceVersionRatingSummary) .ToListAsync(); } + + /// + public async Task> GetAchievedCertificatedResourceIds(int currentUserId) + { + // Use dashboard logic to ensure same resources determined has having achieved certificates + var param0 = new SqlParameter("@userId", SqlDbType.Int) { Value = currentUserId }; + var param4 = new SqlParameter("@TotalRecords", SqlDbType.Int) { Direction = ParameterDirection.Output }; + + var result = this.dbContext.DashboardResourceDto.FromSqlRaw("resources.GetAchievedCertificatedResourcesWithOptionalPagination @userId = @userId, @TotalRecords = @TotalRecords output", param0, param4).ToList(); + List achievedCertificatedResourceIds = result.Select(drd => drd.ResourceId).Distinct().ToList(); + + return achievedCertificatedResourceIds; + } + + /// + /// + /// + /// . + /// A representing the result of the asynchronous operation. + public async Task> GetResourceActivityPerResourceMajorVersion( + IEnumerable? resourceIds, IEnumerable? userIds) + { + var resourceIdsParam = resourceIds != null + ? string.Join(",", resourceIds) + : null; + + var userIdsParam = userIds != null + ? string.Join(",", userIds) + : null; + + var resourceIdsParameter = new SqlParameter("@p0", resourceIdsParam ?? (object)DBNull.Value); + var userIdsParameter = new SqlParameter("@p1", userIdsParam ?? (object)DBNull.Value); + + List resourceActivityDTOs = await dbContext.ResourceActivityDTO + .FromSqlRaw( + "[activity].[GetResourceActivityPerResourceMajorVersion] @p0, @p1", + resourceIdsParameter, + userIdsParameter) + .AsNoTracking() + .ToListAsync(); + + return resourceActivityDTOs; + } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj index 6372c89b6..55ce79f06 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj @@ -16,7 +16,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs index 8b2e46dee..f5f59cb1d 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs @@ -10,18 +10,35 @@ namespace LearningHub.Nhs.OpenApi.Services.Interface.Services /// public interface IResourceService { + /// + /// The get resource by activityStatusIds async. + /// + /// activityStatusIds. + /// c. + /// The the resourceMetaDataViewModel corresponding to the resource reference. + Task> GetResourceReferenceByActivityStatus(List activityStatusIds, int currentUserId); + /// /// The get resource by id async. /// /// The original resource reference id. + /// . /// The the resourceMetaDataViewModel corresponding to the resource reference. - Task GetResourceReferenceByOriginalId(int originalResourceReferenceId); + Task GetResourceReferenceByOriginalId(int originalResourceReferenceId, int? currentUserId); + + /// + /// The get resource references for certificates + /// + /// currentUserId. + /// The ResourceReferenceWithResourceDetailsViewModelthe resourceMetaDataViewModel corresponding to the resource reference. + Task> GetResourceReferencesForCertificates(int currentUserId); /// /// The get resources by Ids endpoint. /// /// The original resource reference Ids. + /// . /// The resourceReferenceMetaDataViewModel. - Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds); + Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds, int? currentUserId); } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs index 63155c5eb..c9b6e3031 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs @@ -14,6 +14,6 @@ public interface ISearchService /// /// . /// . - Task Search(ResourceSearchRequest query); + Task Search(ResourceSearchRequest query, int? currentUserId); } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj index 2207dfdd4..dece11f1c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -24,6 +24,7 @@ + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs index 1e3f4e563..4848cc1e6 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs @@ -2,10 +2,14 @@ namespace LearningHub.Nhs.OpenApi.Services.Services { using System; using System.Collections.Generic; + using System.Data; using System.Linq; using System.Net; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; + using LearningHub.Nhs.Models.Enums; + using LearningHub.Nhs.Models.ViewModels.Helpers; using LearningHub.Nhs.OpenApi.Models.Exceptions; using LearningHub.Nhs.OpenApi.Models.ViewModels; using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; @@ -47,9 +51,11 @@ public ResourceService(ILearningHubService learningHubService, IResourceReposito /// the get by id async. /// /// the id. + /// . /// the resource. - public async Task GetResourceReferenceByOriginalId(int originalResourceReferenceId) + public async Task GetResourceReferenceByOriginalId(int originalResourceReferenceId, int? currentUserId) { + List resourceActivities = new List() { }; var list = new List() { originalResourceReferenceId }; var resourceReferences = await this.resourceRepository.GetResourceReferencesByOriginalResourceReferenceIds(list); @@ -64,7 +70,15 @@ public async Task GetResourceRefe throw new HttpResponseException("No matching resource reference", HttpStatusCode.NotFound); } - return this.GetResourceReferenceWithResourceDetailsViewModel(resourceReference); + if (currentUserId.HasValue) + { + List resourceIds = new List() { resourceReference.ResourceId }; + List userIds = new List() { currentUserId.Value }; + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(resourceIds, userIds))?.ToList() ?? new List() { }; + } + + return this.GetResourceReferenceWithResourceDetailsViewModel(resourceReference, resourceActivities); } catch (InvalidOperationException exception) { @@ -78,8 +92,11 @@ public async Task GetResourceRefe /// /// the resource reference ids. /// the resource. - public async Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds) + public async Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds, int? currentUserId) { + List resourceActivities = new List() { }; + List majorVersionIdActivityStatusDescription = new List() { }; + var resourceReferences = await this.resourceRepository.GetResourceReferencesByOriginalResourceReferenceIds(originalResourceReferenceIds); var resourceReferencesList = resourceReferences.ToList(); var matchedIds = resourceReferencesList.Select(r => r.OriginalResourceReferenceId).ToList(); @@ -95,18 +112,85 @@ public async Task GetResourceReferencesByOrigina this.logger.LogWarning($"Multiple resource references found with OriginalResourceReferenceId {duplicateIds.First()}"); } - var matchedResources = resourceReferencesList - .Select(this.GetResourceReferenceWithResourceDetailsViewModel) - .ToList(); + if (currentUserId.HasValue) + { + List resourceIds = resourceReferencesList.Select(rrl => rrl.ResourceId).ToList(); + List userIds = new List() { currentUserId.Value }; + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(resourceIds, userIds))?.ToList() ?? new List() { }; + } + + List matchedResources = resourceReferencesList + .Select(rr => this.GetResourceReferenceWithResourceDetailsViewModel(rr, resourceActivities.Where(ra => ra.ResourceId == rr.ResourceId).ToList())) + .ToList(); return new BulkResourceReferenceViewModel(matchedResources, unmatchedIds); } - private ResourceReferenceWithResourceDetailsViewModel GetResourceReferenceWithResourceDetailsViewModel(ResourceReference resourceReference) + + /// + /// the get by id async. + /// + /// . + /// c. + /// list resource ViewModel. + public async Task> GetResourceReferenceByActivityStatus(List activityStatusIds, int currentUserId) + { + List resourceActivities = new List() { }; + List resourceReferenceWithResourceDetailsViewModelLS = new List() { }; + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(new List(){ }, new List(){ currentUserId }))?.ToList() ?? new List() { }; + + // Removing resources that have no major versions with the required activitystatus + List resourceIds = resourceActivities + .GroupBy(ra => ra.ResourceId) + .Where(group => group.Any(g => activityStatusIds.Contains(g.ActivityStatusId))) + .Select(group => group.Key) + .Distinct() + .ToList(); + + var resourceReferencesList = (await this.resourceRepository.GetResourcesFromIds(resourceIds)).SelectMany(r => r.ResourceReference).ToList(); + + resourceReferenceWithResourceDetailsViewModelLS = resourceReferencesList.Select(rr => this.GetResourceReferenceWithResourceDetailsViewModel(rr, resourceActivities)).ToList(); + + return resourceReferenceWithResourceDetailsViewModelLS; + } + + /// + /// Gets ResourceReferences ForCertificates using the ResourceReferenceWithResourceDetailsViewModel . + /// + /// user Id. + /// list resource reference ViewModel. + public async Task> GetResourceReferencesForCertificates(int currentUserId) + { + + List resourceActivities = new List() { }; + List resourceReferenceWithResourceDetailsViewModelLS = new List() { }; + List achievedCertificatedResourceIds = (await this.resourceRepository.GetAchievedCertificatedResourceIds(currentUserId)).ToList(); + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(achievedCertificatedResourceIds, new List() { currentUserId }))?.ToList() ?? new List() { }; + + var resourceList = (await this.resourceRepository.GetResourcesFromIds(achievedCertificatedResourceIds)).ToList(); + + resourceReferenceWithResourceDetailsViewModelLS = resourceList.SelectMany(r => r.ResourceReference) + .Distinct() + .Select(rr => this.GetResourceReferenceWithResourceDetailsViewModel(rr, resourceActivities)).ToList(); + + return resourceReferenceWithResourceDetailsViewModelLS; + } + + private ResourceReferenceWithResourceDetailsViewModel GetResourceReferenceWithResourceDetailsViewModel(ResourceReference resourceReference, List resourceActivities) { var hasCurrentResourceVersion = resourceReference.Resource.CurrentResourceVersion != null; var hasRating = resourceReference.Resource.CurrentResourceVersion?.ResourceVersionRatingSummary != null; + List majorVersionIdActivityStatusDescription = new List() { }; + + if (resourceActivities != null && resourceActivities.Count != 0) + { + majorVersionIdActivityStatusDescription = ActivityStatusHelper.GetMajorVersionIdActivityStatusDescriptionLSPerResource(resourceReference.Resource, resourceActivities).ToList(); + } + if (resourceReference.Resource == null) { throw new Exception("No matching resource"); @@ -135,8 +219,10 @@ private ResourceReferenceWithResourceDetailsViewModel GetResourceReferenceWithRe resourceReference.Resource.CurrentResourceVersion?.Description ?? string.Empty, resourceReference.GetCatalogue(), resourceTypeNameOrEmpty, + resourceReference.Resource?.CurrentResourceVersion?.MajorVersion ?? 0, resourceReference.Resource?.CurrentResourceVersion?.ResourceVersionRatingSummary?.AverageRating ?? 0, - this.learningHubService.GetResourceLaunchUrl(resourceReference.OriginalResourceReferenceId)); + this.learningHubService.GetResourceLaunchUrl(resourceReference.OriginalResourceReferenceId), + majorVersionIdActivityStatusDescription); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs index 656a8cb14..c5ede76fb 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs @@ -3,8 +3,11 @@ namespace LearningHub.Nhs.OpenApi.Services.Services using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; + using LearningHub.Nhs.Models.Resource; using LearningHub.Nhs.Models.Search; + using LearningHub.Nhs.Models.ViewModels.Helpers; using LearningHub.Nhs.OpenApi.Models.ServiceModels.Findwise; using LearningHub.Nhs.OpenApi.Models.ServiceModels.Resource; using LearningHub.Nhs.OpenApi.Models.ViewModels; @@ -57,7 +60,7 @@ public SearchService( } /// - public async Task Search(ResourceSearchRequest query) + public async Task Search(ResourceSearchRequest query, int? currentUserId) { var findwiseResultModel = await this.findwiseClient.Search(query); @@ -66,7 +69,7 @@ public async Task Search(ResourceSearchRequest query) return ResourceSearchResultModel.FailedWithStatus(findwiseResultModel.FindwiseRequestStatus); } - var resourceMetadataViewModels = await this.GetResourceMetadataViewModels(findwiseResultModel); + var resourceMetadataViewModels = await this.GetResourceMetadataViewModels(findwiseResultModel, currentUserId); var totalHits = findwiseResultModel.SearchResults?.Stats.TotalHits; @@ -77,8 +80,9 @@ public async Task Search(ResourceSearchRequest query) } private async Task> GetResourceMetadataViewModels( - FindwiseResultModel findwiseResultModel) + FindwiseResultModel findwiseResultModel, int? currentUserId) { + List resourceActivities = new List() { }; var documentsFound = findwiseResultModel.SearchResults?.DocumentList.Documents?.ToList() ?? new List(); var findwiseResourceIds = documentsFound.Select(d => int.Parse(d.Id)).ToList(); @@ -90,7 +94,7 @@ private async Task> GetResourceMetadataViewModel var resourcesFound = await this.resourceRepository.GetResourcesFromIds(findwiseResourceIds); - var resourceMetadataViewModels = resourcesFound.Select(this.MapToViewModel) + List resourceMetadataViewModels = resourcesFound.Select(resource => MapToViewModel(resource, resourceActivities.Where(x => x.ResourceId == resource.Id).ToList())) .OrderBySequence(findwiseResourceIds) .ToList(); @@ -105,14 +109,29 @@ private async Task> GetResourceMetadataViewModel unmatchedResourcesIdsString); } + if (currentUserId.HasValue) + { + List resourceIds = resourcesFound.Select(x => x.Id).ToList(); + List userIds = new List() { currentUserId.Value }; + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(resourceIds, userIds))?.ToList() ?? new List() { }; + } return resourceMetadataViewModels; } - private ResourceMetadataViewModel MapToViewModel(Resource resource) + private ResourceMetadataViewModel MapToViewModel(Resource resource, List resourceActivities) { var hasCurrentResourceVersion = resource.CurrentResourceVersion != null; var hasRating = resource.CurrentResourceVersion?.ResourceVersionRatingSummary != null; + List majorVersionIdActivityStatusDescription = new List() { }; + + if (resourceActivities != null && resourceActivities.Count != 0) + { + majorVersionIdActivityStatusDescription = ActivityStatusHelper.GetMajorVersionIdActivityStatusDescriptionLSPerResource(resource, resourceActivities) + .ToList(); + } + if (!hasCurrentResourceVersion) { this.logger.LogInformation( @@ -131,13 +150,17 @@ private ResourceMetadataViewModel MapToViewModel(Resource resource) this.logger.LogError($"Resource has unrecognised type: {resource.ResourceTypeEnum}"); } + return new ResourceMetadataViewModel( resource.Id, resource.CurrentResourceVersion?.Title ?? ResourceHelpers.NoResourceVersionText, resource.CurrentResourceVersion?.Description ?? string.Empty, resource.ResourceReference.Select(this.GetResourceReferenceViewModel).ToList(), resourceTypeNameOrEmpty, - resource.CurrentResourceVersion?.ResourceVersionRatingSummary?.AverageRating ?? 0.0m); + resource.CurrentResourceVersion?.MajorVersion ?? 0, + resource.CurrentResourceVersion?.ResourceVersionRatingSummary?.AverageRating ?? 0.0m, + majorVersionIdActivityStatusDescription + ); } private ResourceReferenceViewModel GetResourceReferenceViewModel( diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs index 49f901c72..d0126a840 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs @@ -18,8 +18,12 @@ namespace LearningHub.Nhs.OpenApi.Tests.Controllers using Moq; using Newtonsoft.Json; using Xunit; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using System.Security.Claims; + using LearningHub.Nhs.Models.Enums; - public sealed class ResourceControllerTests : IDisposable + public sealed class ResourceControllerTests { private readonly Mock searchService; private readonly Mock resourceService; @@ -87,6 +91,7 @@ await Assert.ThrowsAsync( public async Task SearchEndpointUsesDefaultLimitGivenInConfig() { // Given + int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth this.GivenSearchServiceSucceedsButFindsNoItems(); this.GivenDefaultLimitForFindwiseSearchIs(12); this.resourceController = new ResourceController( @@ -99,7 +104,7 @@ public async Task SearchEndpointUsesDefaultLimitGivenInConfig() // Then this.searchService.Verify( - service => service.Search(It.Is(request => request.Limit == 12))); + service => service.Search(It.Is(request => request.Limit == 12), currentUserId)); } [Fact] @@ -177,6 +182,41 @@ await Assert.ThrowsAsync( exception.StatusCode.Should().Be(HttpStatusCode.BadRequest); } + [Fact] + public void CurrentUserIdSetByAuth() + { + // Arrange + ResourceController resourceController = new ResourceController( + this.searchService.Object, + this.resourceService.Object, + this.findwiseConfigOptions.Object + ); + + + // This Id is the development accountId + int currentUserId = 57541; + + // Create claims identity with the specified user id + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, currentUserId.ToString()), + }; + var identity = new ClaimsIdentity(claims, "AuthenticationTypes.Federation"); // Set the authentication type to "Federation" + + // Create claims principal with the claims identity + var claimsPrincipal = new ClaimsPrincipal(identity); + + // Create a mock HttpContext and set it to the ControllerContext + var httpContext = new DefaultHttpContext { User = claimsPrincipal }; + var controllerContext = new ControllerContext { HttpContext = httpContext }; + resourceController.ControllerContext = controllerContext; + + // Act + + // Assert that the CurrentUserId property of the resourceController matches the currentUserId + Assert.Equal(currentUserId, resourceController.CurrentUserId); + } + [Theory] [InlineData(1)] [InlineData(20)] @@ -184,6 +224,7 @@ await Assert.ThrowsAsync( public async Task SearchEndpointUsesPassedInLimitIfGiven(int limit) { // Given + int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth this.GivenSearchServiceSucceedsButFindsNoItems(); this.GivenDefaultLimitForFindwiseSearchIs(20); this.resourceController = new ResourceController( @@ -196,12 +237,46 @@ public async Task SearchEndpointUsesPassedInLimitIfGiven(int limit) // Then this.searchService.Verify( - service => service.Search(It.Is(request => request.Limit == limit))); + service => service.Search(It.Is(request => request.Limit == limit), currentUserId)); + } + + [Fact] + public async Task GetResourceReferencesByCompleteThrowsErrorWhenNoUserId() + { + // When + var exception = await Assert.ThrowsAsync(async () => + { + await this.resourceController.GetResourceReferencesByActivityStatus((int)ActivityStatusEnum.Completed); + }); + + // Then + Assert.Equal("User Id required.", exception.Message); + } + + [Fact] + public async Task GetResourceReferencesByInProgressThrowsErrorWhenNoUserId() + { + // When + var exception = await Assert.ThrowsAsync(async () => + { + await this.resourceController.GetResourceReferencesByActivityStatus((int)ActivityStatusEnum.Incomplete);// in complete in db is in progress front endS + }); + + // Then + Assert.Equal("User Id required.", exception.Message); } - public void Dispose() + [Fact] + public async Task GetResourceReferencesBycertificatesThrowsErrorWhenNoUserId() { - this.resourceController?.Dispose(); + // When + var exception = await Assert.ThrowsAsync(async () => + { + await this.resourceController.GetResourceReferencesByCertificates(); + }); + + // Then + Assert.Equal("User Id required.", exception.Message); } private void GivenDefaultLimitForFindwiseSearchIs(int limit) @@ -212,14 +287,17 @@ private void GivenDefaultLimitForFindwiseSearchIs(int limit) private void GivenSearchServiceFailsWithStatus(FindwiseRequestStatus status) { - this.searchService.Setup(ss => ss.Search(It.IsAny())).ReturnsAsync( + int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth + this.searchService.Setup(ss => ss.Search(It.IsAny(), currentUserId)).ReturnsAsync( new ResourceSearchResultModel(new List(), status, 0)); } private void GivenSearchServiceSucceedsButFindsNoItems() { - this.searchService.Setup(ss => ss.Search(It.IsAny())).ReturnsAsync( + int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth + this.searchService.Setup(ss => ss.Search(It.IsAny(), currentUserId)).ReturnsAsync( new ResourceSearchResultModel(new List(), FindwiseRequestStatus.Success, 0)); } + } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs index d6ae8f3db..ddddc6ebc 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs @@ -2,10 +2,12 @@ namespace LearningHub.Nhs.OpenApi.Tests.Services.Services { using System; using System.Collections.Generic; + using System.Linq; using System.Net; using System.Threading.Tasks; using FizzWare.NBuilder; using FluentAssertions; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.OpenApi.Models.Exceptions; @@ -22,14 +24,35 @@ public class ResourceServiceTests private readonly Mock learningHubService; private readonly ResourceService resourceService; private readonly Mock resourceRepository; + private readonly int currentUserId; public ResourceServiceTests() { + // This Id is the development accountId + this.currentUserId = 57541; + this.learningHubService = new Mock(); this.resourceRepository = new Mock(); this.resourceService = new ResourceService(this.learningHubService.Object, this.resourceRepository.Object, new NullLogger()); } + private List ResourceActivityDTOList => new List() + { + new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 5, MajorVersion = 5 }, + new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 7, MajorVersion = 4 }, + new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 3, MajorVersion = 3 }, + new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 7, MajorVersion = 2 }, + new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 3, MajorVersion = 1 }, + + new ResourceActivityDTO{ ResourceId = 2, ActivityStatusId = 5, MajorVersion = 5 }, // Passed + new ResourceActivityDTO{ ResourceId = 2, ActivityStatusId = 4, MajorVersion = 4 }, // Failed + new ResourceActivityDTO{ ResourceId = 2, ActivityStatusId = 3, MajorVersion = 3 }, // complete + + new ResourceActivityDTO{ ResourceId = 3, ActivityStatusId = 4, MajorVersion = 2 }, // Failed + new ResourceActivityDTO{ ResourceId = 3, ActivityStatusId = 4, MajorVersion = 1 }, // Failed + new ResourceActivityDTO{ ResourceId = 3, ActivityStatusId = 7, MajorVersion = 4 }, // In complete + }; + private List ResourceList => new List() { ResourceTestHelper.CreateResourceWithDetails(id: 1, title: "title1", description: "description1", rating: 3m, resourceType: ResourceTypeEnum.Article), @@ -63,7 +86,7 @@ public async Task SingleResourceEndpointReturnsTheCorrectInformationIfThereIsAMa .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(1); + var x = await this.resourceService.GetResourceReferenceByOriginalId(1, null); // Then x.Rating.Should().Be(3); @@ -80,7 +103,7 @@ public async Task SingleResourceReturnsA404IfTheresNoResourceReferenceWithAMatch .ReturnsAsync(new List()); // When / Then - var exception = await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(999)); + var exception = await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(999, null)); exception.StatusCode.Should().Be(HttpStatusCode.NotFound); exception.ResponseBody.Should().Be("No matching resource reference"); } @@ -93,7 +116,7 @@ public async Task SingleResourceEndpointReturnsAResourceMetadataViewModelObjectW .ReturnsAsync(this.ResourceReferenceList.GetRange(1, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(2); + var x = await this.resourceService.GetResourceReferenceByOriginalId(2, null); // Then x.Title.Should().Be("No current resource version"); @@ -108,7 +131,7 @@ public async Task SingleResourceEndpointReturnsAMessageSayingNoCatalogueIfThereI .ReturnsAsync(this.ResourceReferenceList.GetRange(2, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(3); + var x = await this.resourceService.GetResourceReferenceByOriginalId(3, null); // Then x.Catalogue.Name.Should().Be("No catalogue for resource reference"); @@ -122,7 +145,7 @@ public async Task SingleResourceEndpointReturnsAMessageSayingNoCatalogueIfThereI .ReturnsAsync(this.ResourceReferenceList.GetRange(3, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(4); + var x = await this.resourceService.GetResourceReferenceByOriginalId(4, null); // Then x.Catalogue.Name.Should().Be("No catalogue for resource reference"); @@ -136,7 +159,7 @@ public async Task SingleResourceEndpointReturnsAMessageSayingNoCatalogueIfThereI .ReturnsAsync(this.ResourceReferenceList.GetRange(5, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(6); + var x = await this.resourceService.GetResourceReferenceByOriginalId(6, null); // Then x.Catalogue.Name.Should().Be("No catalogue for resource reference"); @@ -150,7 +173,7 @@ public async Task SingleResourceEndpointReturnsAZeroForRatingIfTheresNoRatingSum .ReturnsAsync(this.ResourceReferenceList.GetRange(7, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(8); + var x = await this.resourceService.GetResourceReferenceByOriginalId(8, null); // Then x.Catalogue.Name.Should().Be("catalogue3"); @@ -165,7 +188,7 @@ public async Task SingleResourceEndpointThrowsAnErrorAndReturnsABlankStringIfThe .ReturnsAsync(this.ResourceReferenceList.GetRange(8, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(9); + var x = await this.resourceService.GetResourceReferenceByOriginalId(9, null); // Then x.ResourceType.Should().Be(string.Empty); @@ -179,7 +202,7 @@ public async Task SingleResourceEndpointThrowsAnErrorIfThereIsMoreThanOneResourc .ReturnsAsync(this.ResourceReferenceList.GetRange(9, 2)); // When / Then - await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(10)); + await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(10, null)); } /*[Fact] @@ -198,7 +221,7 @@ public async Task BulkEndpointReturnsAllMatchingResources() .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 2)); // When - var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp); + var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null); // Then x.ResourceReferences.Count.Should().Be(2); @@ -220,7 +243,7 @@ public async Task BulkEndpointReturnsA404IfThereAreNoMatchingResources() .ReturnsAsync(new List()); // When - var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp); + var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null); // Then x.UnmatchedResourceReferenceIds.Count.Should().Be(2); @@ -237,7 +260,7 @@ public async Task BulkEndpointReturnsResourcesWithIncompleteInformation() .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 4)); // When - var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp); + var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null); // Then x.ResourceReferences.Count.Should().Be(4); @@ -257,7 +280,7 @@ public async Task BulkEndpointReturnsUnmatchedResourcesWithMatchedResources() .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1)); // When - var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp); + var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null); // Then x.ResourceReferences.Count.Should().Be(1); @@ -277,7 +300,7 @@ public async Task ResourceServiceReturnsTheOriginalResourceReferenceIdAsTheRefId .ReturnsAsync(this.ResourceReferenceList.GetRange(5, 2)); // When - var x = await this.resourceService.GetResourceReferencesByOriginalIds(list); + var x = await this.resourceService.GetResourceReferencesByOriginalIds(list, null); // Then x.ResourceReferences[0].RefId.Should().Be(6); @@ -292,12 +315,12 @@ public async Task ResourceServiceReturnsTheOriginalResourceReferenceIdAsTheRefId this.resourceRepository.Setup(rr => rr.GetResourceReferencesByOriginalResourceReferenceIds(list)) .ReturnsAsync(this.ResourceReferenceList.GetRange(5, 1)); - // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(6); + // When + var x = await this.resourceService.GetResourceReferenceByOriginalId(6, null); - // Then + // Then x.RefId.Should().Be(6); - } + } [Fact] public async Task ResourceServiceReturnsThatARestrictedCatalogueIsRestricted() @@ -308,7 +331,7 @@ public async Task ResourceServiceReturnsThatARestrictedCatalogueIsRestricted() .ReturnsAsync(this.ResourceReferenceList.GetRange(8, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(9); + var x = await this.resourceService.GetResourceReferenceByOriginalId(9, null); // Then x.Catalogue.IsRestricted.Should().BeTrue(); @@ -323,10 +346,192 @@ public async Task ResourceServiceReturnsThatAnUnrestrictedCatalogueIsUnrestricte .ReturnsAsync(this.ResourceReferenceList.GetRange(7, 1)); // When - var x = await this.resourceService.GetResourceReferenceByOriginalId(8); + var x = await this.resourceService.GetResourceReferenceByOriginalId(8, null); // Then x.Catalogue.IsRestricted.Should().BeFalse(); } + + [Fact] + public async Task SingleResourceEndpointReturnsActivitySummaryWhenCurrentUserIdProvided() + { + // Given + this.resourceRepository.Setup(rr => rr.GetResourceReferencesByOriginalResourceReferenceIds(new List() { 1 })) + .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1)); + + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(new List() { 1 }, new List() { currentUserId })) + .ReturnsAsync(this.ResourceActivityDTOList.ToList()); + + // When + var x = await this.resourceService.GetResourceReferenceByOriginalId(1, currentUserId); + + // Then + x.UserSummaryActivityStatuses.Should().NotBeNull(); + x.UserSummaryActivityStatuses[0].MajorVersionId.Should().Be(5); + x.UserSummaryActivityStatuses[1].MajorVersionId.Should().Be(4); + x.UserSummaryActivityStatuses[2].MajorVersionId.Should().Be(3); + x.UserSummaryActivityStatuses[3].MajorVersionId.Should().Be(2); + x.UserSummaryActivityStatuses[4].MajorVersionId.Should().Be(1); + + x.UserSummaryActivityStatuses[0].ActivityStatusDescription.Should().Be("Passed"); + x.UserSummaryActivityStatuses[1].ActivityStatusDescription.Should().Be("In progress"); + x.UserSummaryActivityStatuses[2].ActivityStatusDescription.Should().Be("Viewed"); + x.UserSummaryActivityStatuses[3].ActivityStatusDescription.Should().Be("In progress"); + x.UserSummaryActivityStatuses[4].ActivityStatusDescription.Should().Be("Viewed"); + } + + [Fact] + public async Task SingleResourceEndpointReturnsEmptyActivitySummaryWhenNoCurrentUserIdProvided() + { + // Given + this.resourceRepository.Setup(rr => rr.GetResourceReferencesByOriginalResourceReferenceIds(new List() { 1 })) + .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1)); + + // This should not be hit + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(new List() { 1 }, new List() { currentUserId })) + .ReturnsAsync(this.ResourceActivityDTOList.ToList()); + + // When + var x = await this.resourceService.GetResourceReferenceByOriginalId(1, null); + + // Then + x.UserSummaryActivityStatuses.Should().BeEmpty(); + } + + [Fact] + public async Task GetResourceReferencesByCompleteReturnsCorrectInformation() + { + // Given + List resourceIds = new List() { 1, 2 }; + List resources = this.ResourceList.GetRange(0, 2); + resources[0].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 1, title: "title1", description: "description1", rating: 3m, resourceType: ResourceTypeEnum.Article); + resources[1].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 2, hasCurrentResourceVersion: false, hasNodePath: false, resourceType: ResourceTypeEnum.Assessment); + + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(It.IsAny>(), It.IsAny>())) + .ReturnsAsync(this.ResourceActivityDTOList); + + this.resourceRepository.Setup(rr => rr.GetResourcesFromIds(resourceIds)) + .ReturnsAsync(resources); + + // When + var x = await this.resourceService.GetResourceReferenceByActivityStatus(new List() { (int)ActivityStatusEnum.Completed }, currentUserId); + + // Then + + // Two groups resourceId 1 and 2 have completed for a major version. ResourceId 3 had resourceActivity data but not completed + x.Count().Should().Be(2); + + // We are including all the major versions not just the matching ones if there exists one matching one + x[0].ResourceId.Should().Be(1); + x[0].UserSummaryActivityStatuses.Count().Should().Be(5); + + // Return all the activitySummaries if one match + x[1].ResourceId.Should().Be(2); + x[1].UserSummaryActivityStatuses.Count().Should().Be(3); + + // we are not excluding major version that are not completed. We return the resource and all its activitySummaries if one matches + x[0].UserSummaryActivityStatuses[1].ActivityStatusDescription.Should().Be("In progress"); + x[0].UserSummaryActivityStatuses[2].ActivityStatusDescription.Should().Be("Viewed"); // Rename completed and still return it + + } + + [Fact] + public async Task GetResourceReferencesByInProgressReturnsCorrectInformation() + { + // Given + List resourceIds = new List() { 1, 3 }; + List resources = new List() { this.ResourceList[0], this.ResourceList[2] }; + resources[0].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 1, title: "title1", description: "description1", rating: 3m, resourceType: ResourceTypeEnum.Article); + resources[1].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 3, title: "title2", description: "description2"); + + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(It.IsAny>(), It.IsAny>())) + .ReturnsAsync(this.ResourceActivityDTOList); + + this.resourceRepository.Setup(rr => rr.GetResourcesFromIds(resourceIds)) + .ReturnsAsync(resources); + + // When + var x = await this.resourceService.GetResourceReferenceByActivityStatus(new List() { (int)ActivityStatusEnum.Incomplete }, currentUserId); // In complete in the database is in progress im database + + // Then + + // Two groups resourceId 1 and 3 have completed for a major version. ResourceId 2 had resourceActivity data but not "in progress" + x.Count().Should().Be(2); + + // We are including all the major versions not just the matching ones if there exists one matching one + x[0].ResourceId.Should().Be(1); + x[0].UserSummaryActivityStatuses.Count().Should().Be(5); + + // Return all the activitySummaries if one match + x[1].ResourceId.Should().Be(3); + x[1].UserSummaryActivityStatuses.Count().Should().Be(3); + + // we are not excluding major version that are not completed. We return the resource and all its activitySummaries if one matches + x[0].UserSummaryActivityStatuses[1].ActivityStatusDescription.Should().Be("In progress"); + x[0].UserSummaryActivityStatuses[2].ActivityStatusDescription.Should().Be("Viewed"); // Rename completed and still return it + + } + + [Fact] + public async Task GetResourceReferencesByCertificatesReturnsCorrectInformation() + { + + // Given + List resourceIds = new List() { 1, 3 }; // Ids returned from activity + + List resources = new List() { this.ResourceList[0], this.ResourceList[2] }; + resources[0].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 1, title: "title1", description: "description1", rating: 3m, resourceType: ResourceTypeEnum.Article); + resources[1].ResourceReference.ToList()[0].Resource = ResourceTestHelper.CreateResourceWithDetails(id: 3, title: "title2", description: "description2"); + + + // Will be passed resourceIds and currentUserId + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(It.IsAny>(), It.IsAny>())) + .ReturnsAsync(this.ResourceActivityDTOList); + + this.resourceRepository.Setup(rr => rr.GetAchievedCertificatedResourceIds(currentUserId)) + .ReturnsAsync(resourceIds); + + this.resourceRepository.Setup(rr => rr.GetResourcesFromIds(resourceIds)) + .ReturnsAsync(resources); + + // When + var x = await this.resourceService.GetResourceReferencesForCertificates(currentUserId); + + // Then + + x.Count().Should().Be(2); + + // We are including all the major versions not just the matching ones if there exists one matching one + x[0].ResourceId.Should().Be(1); + x[0].UserSummaryActivityStatuses.Count().Should().Be(5); + + // Return all the activitySummaries if one match + x[1].ResourceId.Should().Be(3); + x[1].UserSummaryActivityStatuses.Count().Should().Be(3); + + // we are not excluding major version that are not completed (assuming here that its completed and has certificated flag). We return the resource and all its activitySummaries if one matches + x[0].UserSummaryActivityStatuses[1].ActivityStatusDescription.Should().Be("In progress"); + x[0].UserSummaryActivityStatuses[2].ActivityStatusDescription.Should().Be("Viewed"); // Rename completed and still return it + } + + [Fact] + public async Task GetResourceReferencesByCompleteNoActivitySummaryFound() + { + // Given + List resourceIds = new List() { }; + List resources = this.ResourceList.GetRange(0, 0); + + this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(It.IsAny>(), It.IsAny>())) + .ReturnsAsync(this.ResourceActivityDTOList.GetRange(8, 3)); + + this.resourceRepository.Setup(rr => rr.GetResourcesFromIds(resourceIds)) + .ReturnsAsync(resources); + + // When + var x = await this.resourceService.GetResourceReferenceByActivityStatus(new List() { (int)ActivityStatusEnum.Completed }, currentUserId); + + // Then + x.Count().Should().Be(0); + } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs index d393ce59d..ee84c1c51 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs @@ -6,6 +6,7 @@ namespace LearningHub.Nhs.OpenApi.Tests.Services.Services using FizzWare.NBuilder; using FluentAssertions; using FluentAssertions.Execution; + using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Search; @@ -73,7 +74,7 @@ public async Task SearchPassesQueryOnToFindwise() .ReturnsAsync(FindwiseResultModel.Failure(FindwiseRequestStatus.Timeout)); // When - await this.searchService.Search(searchRequest); + await this.searchService.Search(searchRequest, null); // Then this.findwiseClient.Verify(fc => fc.Search(searchRequest)); @@ -90,7 +91,7 @@ public async Task SearchReturnsTotalHitsAndSearchResult() this.GivenFindwiseReturnsSuccessfulResponse(74, Enumerable.Range(1, 34)); // When - var searchResult = await this.searchService.Search(searchRequest); + var searchResult = await this.searchService.Search(searchRequest, null); // Then searchResult.Resources.Count.Should().Be(34); @@ -137,7 +138,7 @@ public async Task SearchResultsReturnExpectedValues() this.GivenFindwiseReturnsSuccessfulResponse(2, new[] { 1, 2, 3 }); // When - var searchResult = await this.searchService.Search(searchRequest); + var searchResult = await this.searchService.Search(searchRequest, null); // Then searchResult.Resources.Count.Should().Be(2); @@ -180,7 +181,7 @@ public async Task SearchReturnsResourcesInOrderMatchingFindwise() .ReturnsAsync(resources); // When - var searchResultModel = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10)); + var searchResultModel = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10), null); // Then searchResultModel.Resources.Select(r => r.ResourceId).Should().ContainInOrder(new[] { 1, 3, 2 }); @@ -194,7 +195,6 @@ public async Task SearchReplacesNullPropertiesOfResourceWithDefaultValues() { Builder.CreateNew() .With(r => r.Id = 1) - .With(r => r.CurrentResourceVersion = null) .With( r => r.ResourceReference = new[] { @@ -212,7 +212,7 @@ public async Task SearchReplacesNullPropertiesOfResourceWithDefaultValues() this.GivenFindwiseReturnsSuccessfulResponse(1, new[] { 1 }); // When - var searchResult = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10)); + var searchResult = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10), null); // Then using var scope = new AssertionScope(); @@ -233,7 +233,9 @@ public async Task SearchReplacesNullPropertiesOfResourceWithDefaultValues() string.Empty, expectedResourceReferences, "Article", - 0)); + 0, + 0, + new List(){ })); } private void GivenFindwiseReturnsSuccessfulResponse(int totalHits, IEnumerable resourceIds) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/BookmarkController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/BookmarkController.cs index d5897aa2a..9c7d8f0b9 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/BookmarkController.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/BookmarkController.cs @@ -15,7 +15,7 @@ [Authorize] [Route("Bookmark")] [ApiController] - public class BookmarkController : Controller + public class BookmarkController : OpenApiControllerBase { private readonly IBookmarkService bookmarkService; @@ -28,6 +28,7 @@ public BookmarkController(IBookmarkService bookmarkService) this.bookmarkService = bookmarkService; } + /// /// /// Gets all bookmarks by parent. /// @@ -36,11 +37,7 @@ public BookmarkController(IBookmarkService bookmarkService) [Route("GetAllByParent")] public async Task> GetAllByParent() { - var accessToken = await this.HttpContext - .GetTokenAsync(OpenIdConnectParameterNames.AccessToken); - - return await this.bookmarkService.GetAllByParent( - accessToken); + return await this.bookmarkService.GetAllByParent(this.TokenWithoutBearer); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/OpenApiControllerBase.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/OpenApiControllerBase.cs new file mode 100644 index 000000000..16fd3799c --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/OpenApiControllerBase.cs @@ -0,0 +1,60 @@ +namespace LearningHub.NHS.OpenAPI.Controllers +{ + using System.Net; + using System.Security.Claims; + using LearningHub.Nhs.OpenApi.Models.Exceptions; + using Microsoft.AspNetCore.Mvc; + + /// + /// The base class for API controllers. + /// + public abstract class OpenApiControllerBase : ControllerBase + { + /// + /// Gets the current user's ID. + /// + public int? CurrentUserId + { + get + { + // This check is to determine between the two ways of authorising, OAuth and APIKey.OAuth provides userId and APIKey does not. For OpenApi we provide the data without specific user info. + if ((this.User?.Identity?.AuthenticationType ?? null) == "AuthenticationTypes.Federation") + { + int userId; + if (int.TryParse(User.FindFirst(ClaimTypes.NameIdentifier).Value, out userId)) + { + return userId; + } + else + { + // If parsing fails, return null - for apikey this will be the name + return null; + } + } + else + { + // When authorizing by ApiKey we do not have a user for example + return null; + } + } + } + + /// + /// Gets the bearer token from OAuth and removes "Bearer " prepend. + /// + public string TokenWithoutBearer + { + get + { + string accessToken = this.HttpContext.Request.Headers["Authorization"].ToString(); + + if (string.IsNullOrEmpty(accessToken)) + { + throw new HttpResponseException($"No token provided please use OAuth", HttpStatusCode.Unauthorized); + } + + return accessToken.StartsWith("Bearer ") ? accessToken.Substring("Bearer ".Length) : accessToken; + } + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs index 95591961f..ec8774fea 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs @@ -1,10 +1,12 @@ namespace LearningHub.NHS.OpenAPI.Controllers { using System; + using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; + using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.OpenApi.Models.Configuration; using LearningHub.Nhs.OpenApi.Models.Exceptions; using LearningHub.Nhs.OpenApi.Models.ServiceModels.Findwise; @@ -21,7 +23,7 @@ namespace LearningHub.NHS.OpenAPI.Controllers /// [Route("Resource")] [Authorize] - public class ResourceController : Controller + public class ResourceController : OpenApiControllerBase { private const int MaxNumberOfReferenceIds = 1000; private readonly ISearchService searchService; @@ -75,7 +77,7 @@ await this.searchService.Search( offset, limit ?? this.findwiseConfig.DefaultItemLimitForSearch, catalogueId, - resourceTypes)); + resourceTypes), this.CurrentUserId); switch (resourceSearchResult.FindwiseRequestStatus) { @@ -109,7 +111,7 @@ await this.searchService.Search( [HttpGet("{originalResourceReferenceId}")] public async Task GetResourceReferenceByOriginalId(int originalResourceReferenceId) { - return await this.resourceService.GetResourceReferenceByOriginalId(originalResourceReferenceId); + return await this.resourceService.GetResourceReferenceByOriginalId(originalResourceReferenceId, this.CurrentUserId); } /// @@ -118,14 +120,14 @@ public async Task GetResourceRefe /// ids. /// ResourceReferenceViewModels for matching resources. [HttpGet("Bulk")] - public async Task GetResourceReferencesByOriginalIds([FromQuery]List resourceReferenceIds) + public async Task GetResourceReferencesByOriginalIds([FromQuery] List resourceReferenceIds) { if (resourceReferenceIds.Count > MaxNumberOfReferenceIds) { throw new HttpResponseException($"Too many resources requested. The maximum is {MaxNumberOfReferenceIds}", HttpStatusCode.BadRequest); } - return await this.resourceService.GetResourceReferencesByOriginalIds(resourceReferenceIds.ToList()); + return await this.resourceService.GetResourceReferencesByOriginalIds(resourceReferenceIds.ToList(), this.CurrentUserId); } /// @@ -134,7 +136,7 @@ public async Task GetResourceReferencesByOrigina /// ids. /// ResourceReferenceViewModels for matching resources. [HttpGet("BulkJson")] - public async Task GetResourceReferencesByOriginalIdsFromJson([FromQuery]string resourceReferences) + public async Task GetResourceReferencesByOriginalIdsFromJson([FromQuery] string resourceReferences) { var bulkResourceReferences = JsonConvert.DeserializeObject(resourceReferences); @@ -148,7 +150,51 @@ public async Task GetResourceReferencesByOrigina throw new HttpResponseException($"Too many resources requested. The maximum is {MaxNumberOfReferenceIds}", HttpStatusCode.BadRequest); } - return await this.resourceService.GetResourceReferencesByOriginalIds(bulkResourceReferences.ResourceReferenceIds); + return await this.resourceService.GetResourceReferencesByOriginalIds(bulkResourceReferences.ResourceReferenceIds, this.CurrentUserId); + } + + /// + /// Get resourceReferences that have an in progress activity summary + /// + /// activityStatusId. + /// ResourceReferenceViewModels for matching resources. + [HttpGet("User/{activityStatusId}")] + public async Task> GetResourceReferencesByActivityStatus(int activityStatusId) + { + // These activity statuses are set with other activity statuses and resource type within the ActivityStatusHelper.GetActivityStatusDescription + // Note In progress is in complete in the db + List activityStatusIdsNotInUseInDB = new List() { (int)ActivityStatusEnum.Launched, (int)ActivityStatusEnum.InProgress, (int)ActivityStatusEnum.Viewed, (int)ActivityStatusEnum.Downloaded }; + if (this.CurrentUserId == null) + { + throw new UnauthorizedAccessException("User Id required."); + } + + if (!Enum.IsDefined(typeof(ActivityStatusEnum), activityStatusId)) + { + throw new ArgumentOutOfRangeException($"activityStatusId : {activityStatusId} does not exist within ActivityStatusEnum"); + } + + if (activityStatusIdsNotInUseInDB.Contains(activityStatusId)) + { + throw new ArgumentOutOfRangeException($"activityStatusId: {activityStatusId} does not exist within the database definitions"); + } + + return await this.resourceService.GetResourceReferenceByActivityStatus(new List() { activityStatusId }, this.CurrentUserId.Value); + } + + /// + /// Get resourceReferences that have certificates + /// + /// ResourceReferenceViewModels for matching resources. + [HttpGet("User/Certificates")] + public async Task> GetResourceReferencesByCertificates() + { + if (this.CurrentUserId == null) + { + throw new UnauthorizedAccessException("User Id required."); + } + + return await this.resourceService.GetResourceReferencesForCertificates(this.CurrentUserId.Value); } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user new file mode 100644 index 000000000..b17387f00 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user @@ -0,0 +1,9 @@ + + + + ProjectDebugger + + + IIS Local + + \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json b/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json index d77f6b823..4fdbbba8b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json @@ -1,5 +1,5 @@ { - "openapi": "3.0.1", + "openapi": "3.0.2", "info": { "title": "LearningHub.NHS.OpenAPI", "version": "1.3.0", @@ -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..82b88c37a 100644 --- a/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj +++ b/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj @@ -27,7 +27,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj b/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj index 722cefdee..f38e0260e 100644 --- a/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj +++ b/WebAPI/LearningHub.Nhs.Api.Shared/LearningHub.Nhs.Api.Shared.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj b/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj index d47e3d417..f61e84c2f 100644 --- a/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj +++ b/WebAPI/LearningHub.Nhs.Api.UnitTests/LearningHub.Nhs.Api.UnitTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index f19a40bbe..477a48869 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -200,9 +200,6 @@ - - - @@ -518,12 +515,17 @@ + + + + + @@ -594,4 +596,4 @@ - + \ 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..a2ac5f308 100644 --- a/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj +++ b/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj @@ -9,7 +9,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj b/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj index 5e7e65365..0f94d7953 100644 --- a/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj +++ b/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj @@ -9,7 +9,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Repository/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..59748eeb8 100644 --- a/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj +++ b/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj @@ -16,7 +16,7 @@ - + all diff --git a/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj b/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj index 9869ac4be..7f312c998 100644 --- a/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj +++ b/WebAPI/LearningHub.Nhs.Services.UnitTests/LearningHub.Nhs.Services.UnitTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj b/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj index 44fefe844..e9fa30eef 100644 --- a/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj +++ b/WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj @@ -13,7 +13,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Services/ResourceService.cs b/WebAPI/LearningHub.Nhs.Services/ResourceService.cs index 808bd8448..76d8df791 100644 --- a/WebAPI/LearningHub.Nhs.Services/ResourceService.cs +++ b/WebAPI/LearningHub.Nhs.Services/ResourceService.cs @@ -1772,7 +1772,8 @@ public async Task AddResourceVersionKeywordAsync(Re bool doesKeywordAlreadyExist = await this.resourceVersionKeywordRepository.DoesResourceVersionKeywordAlreadyExistAsync(rvk.ResourceVersionId, rvk.Keyword); if (doesKeywordAlreadyExist) { - return new LearningHubValidationResult(false, "This keyword has already been added."); + retVal.CreatedId = 0; + return retVal; } retVal.CreatedId = await this.resourceVersionKeywordRepository.CreateAsync(userId, rvk); diff --git a/WebAPI/LearningHub.Nhs.Services/SearchService.cs b/WebAPI/LearningHub.Nhs.Services/SearchService.cs index 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..539697d49 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj @@ -24,7 +24,7 @@ - + all diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj index 52929fbd8..eb924766b 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Interface/LearningHub.Nhs.Migration.Interface.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj index a3951c927..6f4c807b7 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Models/LearningHub.Nhs.Migration.Models.csproj @@ -10,7 +10,7 @@ - + all diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj index 7efd53fab..df5c3c6c3 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Repository/LearningHub.Nhs.Migration.Staging.Repository.csproj @@ -9,7 +9,7 @@ - + diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj index ad73a25cc..16a31717c 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.UnitTests/LearningHub.Nhs.Migration.UnitTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj index 861270f13..440ec325e 100644 --- a/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj +++ b/WebAPI/MigrationTool/LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive