From 8d63ee1d8d9f6db4d897861e82b08316c745edda Mon Sep 17 00:00:00 2001 From: OluwatobiAwe Date: Wed, 9 Apr 2025 07:07:01 +0100 Subject: [PATCH 001/106] updated documentation based on the imported API --- .../SwaggerDefinitions/v1.3.0.json | 14231 +++++++++++++++- 1 file changed, 13877 insertions(+), 354 deletions(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json b/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json index dcfd80c44..e8d830480 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json @@ -1,10 +1,10 @@ { "openapi": "3.0.2", - "info": { - "title": "LearningHub.NHS.OpenAPI", - "version": "1.3.0", - "description": "A set of API endpoints for retrieving learning resource information from the Learning Hub learning platform. The [Learning Hub](https://learninghub.nhs.uk/) is a platform for hosting and sharing learning resources for health and social care provided by Technology Enhanced Learning (TEL) at NHS England. An application API key must be used to authorise calls to the API from external applications. To contact TEL to discuss connecting your external system to the Learning Hub, email england.tel@nhs.net." - }, + "info": { + "title": "LearningHub.NHS.OpenAPI", + "version": "1.3.0", + "description": "A set of API endpoints for retrieving learning resource information from the Learning Hub learning platform. The [Learning Hub](https://learninghub.nhs.uk/) is a platform for hosting and sharing learning resources for health and social care provided by Technology Enhanced Learning (TEL) at NHS England. An application API key must be used to authorise calls to the API from external applications. To contact TEL to discuss connecting your external system to the Learning Hub, email england.tel@nhs.net." + }, "paths": { "/Bookmark/GetAllByParent": { "get": { @@ -44,6 +44,130 @@ } } }, + "/Bookmark/Create": { + "post": { + "tags": [ + "Bookmark" + ], + "summary": "The Create.", + "requestBody": { + "description": "The bookmarkViewModelLearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Bookmark/toggle": { + "put": { + "tags": [ + "Bookmark" + ], + "summary": "The Toggle.", + "requestBody": { + "description": "The bookmarkViewModelLearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Bookmark/Edit": { + "put": { + "tags": [ + "Bookmark" + ], + "summary": "The Edit.", + "requestBody": { + "description": "The bookmarkViewModelLearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Bookmark/deletefolder/{bookmarkId}": { + "delete": { + "tags": [ + "Bookmark" + ], + "summary": "The DeleteFolder.", + "parameters": [ + { + "name": "bookmarkId", + "in": "path", + "description": "The bookmarkIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/Catalogues": { "get": { "tags": [ @@ -74,316 +198,13774 @@ } } }, - "/Resource/Search": { + "/Catalogues/Catalogues/{id}": { "get": { "tags": [ - "Resource" + "Catalogue" ], - "summary": "Gets a set of Learning Hub resources that match the search string provided. Includes paging, catalogue filtering and resource type filtering options.", + "summary": "The GetCatalogue.", "parameters": [ { - "name": "text", - "in": "query", - "schema": { - "type": "string" - }, + "name": "id", + "in": "path", + "description": "The catalogue node version id.", "required": true, - "description": "Search string." - }, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/Resources/{id}/{page}/{pageSize}/{sortColumn}/{sortDirection}/{filter}": { + "get": { + "tags": [ + "Catalogue" + ], + "summary": "The GetCatalogueResources.", + "parameters": [ { - "name": "offset", - "in": "query", + "name": "id", + "in": "path", + "description": "The id.", + "required": true, "schema": { "type": "integer", - "format": "int32", - "default": 0, - "maximum": 9999 - }, - "description": "The number of items to skip before starting to collect the result set. Use in combination with \"limit\" to implement pagination. The limit plus offset must not exceed 10,000." + "format": "int32" + } }, { - "name": "limit", - "in": "query", + "name": "page", + "in": "path", + "description": "The page.", + "required": true, "schema": { "type": "integer", - "format": "int32", - "default": 10, - "maximum": 10000 - }, - "description": "Maximum number of matching resources to include in the result set. Use in combination with \"offset\" to implement pagination. The limit plus offset must not exceed 10,000." + "format": "int32" + } }, { - "name": "catalogueId", - "in": "query", + "name": "pageSize", + "in": "path", + "description": "The pageSize.", + "required": true, "schema": { "type": "integer", "format": "int32" - }, - "description": "Filters the result set to only include resources that have a reference in the matching catalogue. Resources may contain other references not in the matching catalogue. This should match catalogue IDs returned by the Learning Hub Open API." + } }, { - "name": "resourceTypes", - "in": "query", + "name": "sortColumn", + "in": "path", + "description": "The sortColumn.", + "required": true, "schema": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": "A list of resource types to include in the results set. Must be a valid JSON array of strings. Filters the result set to only include resources matching the array of specified types. These should match resource types returned by the Learning Hub Open API. Any invalid resource types included in the array will be ignored." + "type": "string" + } + }, + { + "name": "sortDirection", + "in": "path", + "description": "The sortDirection.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "in": "path", + "description": "The filter.", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { "200": { - "description": "Success. If no matching results are found, an empty result set will be returned.", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ResourceSearchResultViewModel" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResourceSearchResultViewModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ResourceSearchResultViewModel" - } + "description": "OK" + } + } + } + }, + "/Catalogues/resources": { + "post": { + "tags": [ + "Catalogue" + ], + "summary": "GetResources.", + "requestBody": { + "description": "requestViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueResourceRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueResourceRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueResourceRequestViewModel" } } } + }, + "responses": { + "200": { + "description": "OK" + } } } }, - "/Resource/{resourceReferenceId}": { + "/Catalogues/catalogue/{reference}": { "get": { "tags": [ - "Resource" + "Catalogue" ], - "description": "Gets the Learning Hub resource that matches the Resource Reference ID supplied.", + "summary": "Get Catalogue by reference.", "parameters": [ { - "name": "resourceReferenceId", + "name": "reference", "in": "path", + "description": "The reference.", "required": true, "schema": { - "type": "integer", - "format": "int32" - }, - "description": "The resource reference id of the Learning Hub resource (in catalogue) to return. 404 will be returned if no matching resource reference is found." + "type": "string" + } } ], "responses": { "200": { - "description": "Success", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/ResourceReferenceWithResourceDetailsViewModel" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResourceReferenceWithResourceDetailsViewModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/ResourceReferenceWithResourceDetailsViewModel" - } - } - } + "description": "OK" } } } }, - "/Resource/Bulk": { + "/Catalogues/catalogue-recorded/{reference}": { "get": { "tags": [ - "Resource" + "Catalogue" ], - "description": "Gets a set of Learning Hub resources that match the list of Resource Reference IDs supplied. This endpoint is deprecated and exists only for backward compatibility: \"/Resource/BulkJson\" should be used instead.", - "deprecated": true, + "summary": "Get Catalogue by reference.", "parameters": [ { - "name": "resourceReferenceIds", - "in": "query", + "name": "reference", + "in": "path", + "description": "The reference.", + "required": true, "schema": { - "type": "array", - "items": { - "type": "integer", - "format": "int32" - } - }, - "description": "Resource references should be passed in the form \"resourceReferenceIds=123&resourceReferenceIds=234&…\". Each integer should be the Resource Reference ID of a Learning Hub resource to be included in the result set. Any unmatched results will be listed in the unmatchedResourceReferenceIds return property." + "type": "string" + } } ], "responses": { "200": { - "description": "Success", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/BulkResourceReferenceViewModel" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkResourceReferenceViewModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BulkResourceReferenceViewModel" - } - } - } + "description": "OK" } } } }, - "/Resource/BulkJson": { + "/Catalogues/GetForCurrentUser": { "get": { "tags": [ - "Resource" + "Catalogue" ], - "description": "Gets a set of Learning Hub resources that match the list of Resource Reference IDs supplied.", - "parameters": [ + "summary": "The published catalogues for current user.", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/Catalogues": { + "post": { + "tags": [ + "Catalogue" + ], + "summary": "The CreateCatalogue.", + "parameters": [ { - "name": "resourceReferences", + "name": "CatalogueNodeVersionId", "in": "query", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "referenceIds": { - "type": "array", - "items": { - "type": "integer" - } - } - } - } + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "NodeVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "Url", + "in": "query", + "required": true, + "schema": { + "maxLength": 1000, + "type": "string" + } + }, + { + "name": "CardImageUrl", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "BannerUrl", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CertificateUrl", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Description", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Keywords", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" } - }, - "description": "Resource references should be passed in the form \"{referenceIds:[123,234,…]}\". Each integer should be the Resource Reference ID of a Learning Hub resource to be included in the result set. Any unmatched results will be listed in the unmatchedResourceReferenceIds return property." - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/BulkResourceReferenceViewModel" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkResourceReferenceViewModel" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/BulkResourceReferenceViewModel" - } + } + }, + { + "name": "OwnerName", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "OwnerEmailAddress", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Notes", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "LastModifiedDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "Status", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.VersionStatusEnum" + } + }, + { + "name": "ResourceOrder", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.CatalogueOrder" + } + }, + { + "name": "NodePathId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "HasUserGroup", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "BookmarkId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "IsBookmarked", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "Providers", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Provider.ProviderViewModel" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.CatalogueNodeVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.ProviderId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.RemovalDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.Name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.Description", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.Logo", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserName", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.VersionStartTime", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.VersionEndTime", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserUserGroup", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserUserGroup" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.ResourceVersion", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.HierarchyEdit", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.HierarchyEdit" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.ResourceVersionEvent", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionEvent" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.AssignedRoles", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Role" } } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.Token", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.CreatedNotifications", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.AmendedNotifications", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserNotificationAmendUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserNotificationCreateUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserNotificationUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserProvider", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProvider" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.Deleted", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.CreateUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.CreateDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.AmendUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.AmendDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserName", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.VersionStartTime", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.VersionEndTime", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserUserGroup", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserUserGroup" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.ResourceVersion", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.HierarchyEdit", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.HierarchyEdit" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.ResourceVersionEvent", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionEvent" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.AssignedRoles", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Role" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.Token", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.CreatedNotifications", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.AmendedNotifications", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserNotificationAmendUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserNotificationCreateUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserNotificationUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserProvider", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProvider" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.Deleted", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.CreateUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.CreateDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.AmendUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.AmendDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.UserProvider", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProvider" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.ResourceVersionProvider", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionProvider" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.Deleted", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Deleted", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CatalogueNodeVersionProvider.CreateUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.CreateDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.AmendUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.AmendDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "NodeId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "Name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Hidden", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "BadgeUrl", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "RestrictedAccess", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "put": { + "tags": [ + "Catalogue" + ], + "summary": "The UpdateCatalogue.", + "parameters": [ + { + "name": "CatalogueNodeVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "NodeVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "Url", + "in": "query", + "required": true, + "schema": { + "maxLength": 1000, + "type": "string" + } + }, + { + "name": "CardImageUrl", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "BannerUrl", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CertificateUrl", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Description", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Keywords", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "OwnerName", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "OwnerEmailAddress", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Notes", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "LastModifiedDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "Status", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.VersionStatusEnum" + } + }, + { + "name": "ResourceOrder", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.CatalogueOrder" + } + }, + { + "name": "NodePathId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "HasUserGroup", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "BookmarkId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "IsBookmarked", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "Providers", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Provider.ProviderViewModel" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.CatalogueNodeVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.ProviderId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.RemovalDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.Name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.Description", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.Logo", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserName", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.VersionStartTime", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.VersionEndTime", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserUserGroup", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserUserGroup" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.ResourceVersion", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.HierarchyEdit", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.HierarchyEdit" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.ResourceVersionEvent", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionEvent" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.AssignedRoles", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Role" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.Token", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.CreatedNotifications", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.AmendedNotifications", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserNotificationAmendUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserNotificationCreateUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserNotificationUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.UserProvider", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProvider" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.Deleted", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.CreateUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.CreateDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.AmendUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUser.AmendDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserName", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.VersionStartTime", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.VersionEndTime", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserUserGroup", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserUserGroup" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.ResourceVersion", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.HierarchyEdit", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.HierarchyEdit" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.ResourceVersionEvent", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionEvent" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.AssignedRoles", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Role" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.Token", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.CreatedNotifications", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.AmendedNotifications", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserNotificationAmendUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserNotificationCreateUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserNotificationUser", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.UserProvider", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProvider" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.Deleted", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.CreateUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.CreateDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.AmendUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUser.AmendDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.UserProvider", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProvider" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.ResourceVersionProvider", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionProvider" + } + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.Deleted", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.CreateDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Provider.AmendDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.Deleted", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CatalogueNodeVersionProvider.CreateUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.CreateDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "CatalogueNodeVersionProvider.AmendUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "CatalogueNodeVersionProvider.AmendDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "NodeId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "Name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Hidden", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "BadgeUrl", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "RestrictedAccess", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/CanCurrentUserEditCatalogue/{catalogueId}": { + "get": { + "tags": [ + "Catalogue" + ], + "summary": "Returns true if the catalogue is editable by the current user.", + "parameters": [ + { + "name": "catalogueId", + "in": "path", + "description": "The catalogue id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/InviteUser": { + "post": { + "tags": [ + "Catalogue" + ], + "summary": "The InviteUser.", + "requestBody": { + "description": "The view model.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueInviteUserViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueInviteUserViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueInviteUserViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/RequestAccess/{reference}/{accessType}": { + "post": { + "tags": [ + "Catalogue" + ], + "summary": "The RequestAccess.", + "parameters": [ + { + "name": "reference", + "in": "path", + "description": "The reference.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "accessType", + "in": "path", + "description": "The accessType.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The view model.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueAccessRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueAccessRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueAccessRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/GetRestrictedCatalogueSummary/{catalogueNodeId}": { + "get": { + "tags": [ + "Catalogue" + ], + "summary": "Gets the restricted catalogues summary for the supplied catalogue node id.", + "parameters": [ + { + "name": "catalogueNodeId", + "in": "path", + "description": "The catalogueNodeId.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/AccessRequest/{accessRequestId}": { + "get": { + "tags": [ + "Catalogue" + ], + "summary": "The AccessRequest.", + "parameters": [ + { + "name": "accessRequestId", + "in": "path", + "description": "The access request id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/ShowCatalogue": { + "post": { + "tags": [ + "Catalogue" + ], + "summary": "The ShowCatalogue.", + "requestBody": { + "description": "The vm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueBasicViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueBasicViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueBasicViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/HideCatalogue": { + "post": { + "tags": [ + "Catalogue" + ], + "summary": "The HideCatalogue.", + "requestBody": { + "description": "The vm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueBasicViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueBasicViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueBasicViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/AccessDetails/{reference}": { + "get": { + "tags": [ + "Catalogue" + ], + "summary": "The AccessDetails.", + "parameters": [ + { + "name": "reference", + "in": "path", + "description": "The catalogue reference.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/GetCatalogueAccessRequests/{catalogueNodeId}": { + "get": { + "tags": [ + "Catalogue" + ], + "summary": "The GetCatalogueAccessRequests.", + "parameters": [ + { + "name": "catalogueNodeId", + "in": "path", + "description": "The request model.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/GetLatestCatalogueAccessRequest/{catalogueNodeId}": { + "get": { + "tags": [ + "Catalogue" + ], + "summary": "The GetLatestCatalogueAccessRequest.", + "parameters": [ + { + "name": "catalogueNodeId", + "in": "path", + "description": "The request model.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/GetRestrictedCatalogueAccessRequests": { + "post": { + "tags": [ + "Catalogue" + ], + "summary": "Gets the restricted catalogues access requests for the supplied request view model.", + "requestBody": { + "description": "The request model.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueAccessRequestsRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueAccessRequestsRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueAccessRequestsRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/GetRestrictedCatalogueUsers": { + "post": { + "tags": [ + "Catalogue" + ], + "summary": "Gets the restricted catalogues users for the supplied request view model.", + "requestBody": { + "description": "The request model.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueUsersRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueUsersRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueUsersRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/RejectAccessRequest/{accessRequestId}": { + "post": { + "tags": [ + "Catalogue" + ], + "summary": "The RejectAccessRequest.", + "parameters": [ + { + "name": "accessRequestId", + "in": "path", + "description": "The accessRequestId.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "description": "The vm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueAccessRejectionViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueAccessRejectionViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Catalogue.CatalogueAccessRejectionViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Catalogues/AcceptAccessRequest/{accessRequestId}": { + "post": { + "tags": [ + "Catalogue" + ], + "summary": "The AcceptAccessRequest.", + "parameters": [ + { + "name": "accessRequestId", + "in": "path", + "description": "The accessRequestId.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/GetNodeDetails/{nodeId}": { + "get": { + "tags": [ + "Hierarchy" + ], + "summary": "Gets the basic details of a node. Currently catalogues or folders.", + "parameters": [ + { + "name": "nodeId", + "in": "path", + "description": "The node id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.NodeViewModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.NodeViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.NodeViewModel" + } + } + } + } + } + } + }, + "/Hierarchy/GetNodePathNodes/{nodePathId}": { + "get": { + "tags": [ + "Hierarchy" + ], + "summary": "Gets the basic details of all Nodes in a particular NodePath.", + "parameters": [ + { + "name": "nodePathId", + "in": "path", + "description": "The NodePath id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.NodePathNodeViewModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.NodePathNodeViewModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.NodePathNodeViewModel" + } + } + } + } + } + } + } + }, + "/Hierarchy/GetNodeContentsAdmin/{nodeId}/{readOnly}": { + "get": { + "tags": [ + "Hierarchy" + ], + "summary": "Gets the contents of a node (catalogue/folder/course) - i.e. returns a list of subfolders and resources. Only returns the\r\nitems from the first level down. Doesn't recurse through subfolders.", + "parameters": [ + { + "name": "nodeId", + "in": "path", + "description": "The node id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "readOnly", + "in": "path", + "description": "Set to true if read only data set is required.", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/GetNodePathsForNode/{nodeId}": { + "get": { + "tags": [ + "Hierarchy" + ], + "summary": "The get node paths for node.", + "parameters": [ + { + "name": "nodeId", + "in": "path", + "description": "The nodeIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/GetFolder/{nodeVersionId}": { + "get": { + "tags": [ + "Hierarchy" + ], + "summary": "The GetFolder.", + "parameters": [ + { + "name": "nodeVersionId", + "in": "path", + "description": "The id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/GetNodeContentsForCatalogueEditor/{nodeId}": { + "get": { + "tags": [ + "Hierarchy" + ], + "summary": "Gets the contents of a node for the My Contributions page - i.e. published folders only, and all resources (i.e. all statuses).\r\nOnly returns the items found directly in the specified node, does not recurse down through subfolders.", + "parameters": [ + { + "name": "nodeId", + "in": "path", + "description": "The node id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/GetHierarchyEdits/{rootNodeId}": { + "get": { + "tags": [ + "Hierarchy" + ], + "summary": "Gets hierarchy edit detail for the supplied root node id.", + "parameters": [ + { + "name": "rootNodeId", + "in": "path", + "description": "The root node id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/GetNodeResourceLookup/{nodeId}": { + "get": { + "tags": [ + "Hierarchy" + ], + "summary": "The get node resource lookup.\r\nIT1 - quick lookup for whether a node has published resources.\r\nServices Content Structure Admin.", + "parameters": [ + { + "name": "nodeId", + "in": "path", + "description": "The nodeIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/CreateHierarchyEdit/{rootNodeId}": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "Creates hierarchy edit detail for the supplied root node id.", + "parameters": [ + { + "name": "rootNodeId", + "in": "path", + "description": "The root node id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/DiscardHierarchyEdit/{hierarchyEditId}": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "Discards hierarchy edit detail for the supplied root node id.", + "parameters": [ + { + "name": "hierarchyEditId", + "in": "path", + "description": "The hierarchy edit view model.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/CreateFolder": { + "post": { + "tags": [ + "Hierarchy" + ], + "summary": "Creates a new folder.", + "requestBody": { + "description": "The folderEditViewModelLearningHub.Nhs.Models.Hierarchy.FolderEditViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.FolderEditViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.FolderEditViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.FolderEditViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/UpdateFolder": { + "post": { + "tags": [ + "Hierarchy" + ], + "summary": "Updates a folder.", + "requestBody": { + "description": "The folderEditViewModelLearningHub.Nhs.Models.Hierarchy.FolderEditViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.FolderEditViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.FolderEditViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.FolderEditViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/DeleteFolder/{hierarchyEditDetailId}": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "The DeleteFolder.", + "parameters": [ + { + "name": "hierarchyEditDetailId", + "in": "path", + "description": "The id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/MoveNodeUp/{hierarchyEditDetailId}": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "The MoveNodeUp.", + "parameters": [ + { + "name": "hierarchyEditDetailId", + "in": "path", + "description": "The id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/MoveNodeDown/{hierarchyEditDetailId}": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "The MoveNodeDown.", + "parameters": [ + { + "name": "hierarchyEditDetailId", + "in": "path", + "description": "The id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/MoveNode": { + "post": { + "tags": [ + "Hierarchy" + ], + "summary": "Moves a node.", + "requestBody": { + "description": "The moveNodeViewModelLearningHub.Nhs.Models.Hierarchy.MoveNodeViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.MoveNodeViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.MoveNodeViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.MoveNodeViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/HierarchyEditMoveResourceUp/{hierarchyEditDetailId}": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "Moves a resource up in a hierarchy edit.", + "parameters": [ + { + "name": "hierarchyEditDetailId", + "in": "path", + "description": "The id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/HierarchyEditMoveResourceDown/{hierarchyEditDetailId}": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "Moves a resource down in a hierarchy edit.", + "parameters": [ + { + "name": "hierarchyEditDetailId", + "in": "path", + "description": "The id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/HierarchyEditMoveResource": { + "post": { + "tags": [ + "Hierarchy" + ], + "summary": "Moves a resource in a hierarchy edit.", + "requestBody": { + "description": "The moveResourceViewModelLearningHub.Nhs.Models.Hierarchy.HierarchyEditMoveResourceViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.HierarchyEditMoveResourceViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.HierarchyEditMoveResourceViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.HierarchyEditMoveResourceViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/MoveResourceUp/{nodeId}/{resourceId}": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "The MoveResourceUp.", + "parameters": [ + { + "name": "nodeId", + "in": "path", + "description": "The id of the node containing the resource.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "resourceId", + "in": "path", + "description": "The resource id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/MoveResourceDown/{nodeId}/{resourceId}": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "The MoveResourceDown.", + "parameters": [ + { + "name": "nodeId", + "in": "path", + "description": "The id of the node containing the resource.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "resourceId", + "in": "path", + "description": "The resource id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/MoveResource/{sourceNodeId}/{destinationNodeId}/{resourceId}": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "The MoveResource.", + "parameters": [ + { + "name": "sourceNodeId", + "in": "path", + "description": "The id of the node to move the resource from.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "destinationNodeId", + "in": "path", + "description": "The id of the node to move the resource to.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "resourceId", + "in": "path", + "description": "The resource id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/SubmitHierarchyEditForPublish": { + "put": { + "tags": [ + "Hierarchy" + ], + "summary": "Submit HierarchyEdit For Publish.", + "requestBody": { + "description": "The publishViewModelLearningHub.Nhs.Models.Hierarchy.PublishHierarchyEditViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.PublishHierarchyEditViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.PublishHierarchyEditViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.PublishHierarchyEditViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Hierarchy/PublishHierarchyEdit": { + "post": { + "tags": [ + "Hierarchy" + ], + "summary": "Publish HierarchyEdit\r\n TODO - requires validation.", + "requestBody": { + "description": "The publishViewModelLearningHub.Nhs.Models.Hierarchy.PublishHierarchyEditViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.PublishHierarchyEditViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.PublishHierarchyEditViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Hierarchy.PublishHierarchyEditViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Provider/all": { + "get": { + "tags": [ + "Provider" + ], + "summary": "Get Providers.", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Provider/{id}": { + "get": { + "tags": [ + "Provider" + ], + "summary": "Get specific Provider by Id.", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Provider/GetProvidersByUserId/{userId}": { + "get": { + "tags": [ + "Provider" + ], + "summary": "Get providers details by user Id.", + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "The user id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Provider/GetProvidersByResource/{resourceVersionId}": { + "get": { + "tags": [ + "Provider" + ], + "summary": "Get providers by resource version Id.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersion id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/Search": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Gets a set of Learning Hub resources that match the search string provided. Includes paging, catalogue filtering and resource type filtering options.", + "parameters": [ + { + "name": "text", + "in": "query", + "schema": { + "type": "string" + }, + "required": true, + "description": "Search string." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "maximum": 9999 + }, + "description": "The number of items to skip before starting to collect the result set. Use in combination with \"limit\" to implement pagination. The limit plus offset must not exceed 10,000." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 10, + "maximum": 10000 + }, + "description": "Maximum number of matching resources to include in the result set. Use in combination with \"offset\" to implement pagination. The limit plus offset must not exceed 10,000." + }, + { + "name": "catalogueId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "Filters the result set to only include resources that have a reference in the matching catalogue. Resources may contain other references not in the matching catalogue. This should match catalogue IDs returned by the Learning Hub Open API." + }, + { + "name": "resourceTypes", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "A list of resource types to include in the results set. Must be a valid JSON array of strings. Filters the result set to only include resources matching the array of specified types. These should match resource types returned by the Learning Hub Open API. Any invalid resource types included in the array will be ignored." + } + ], + "responses": { + "200": { + "description": "Success. If no matching results are found, an empty result set will be returned.", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ResourceSearchResultViewModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSearchResultViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSearchResultViewModel" + } + } + } + } + } + } + }, + "/Resource/GetResourceReferenceByOriginalId/{originalResourceReferenceId}": { + "get": { + "tags": [ + "Resource" + ], + "description": "Gets the Learning Hub resource that matches the Resource Reference ID supplied.", + "parameters": [ + { + "name": "resourceReferenceId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The resource reference id of the Learning Hub resource (in catalogue) to return. 404 will be returned if no matching resource reference is found." + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ResourceReferenceWithResourceDetailsViewModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceReferenceWithResourceDetailsViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ResourceReferenceWithResourceDetailsViewModel" + } + } + } + } + } + } + }, + "/Resource/Bulk": { + "get": { + "tags": [ + "Resource" + ], + "description": "Gets a set of Learning Hub resources that match the list of Resource Reference IDs supplied. This endpoint is deprecated and exists only for backward compatibility: \"/Resource/BulkJson\" should be used instead.", + "deprecated": true, + "parameters": [ + { + "name": "resourceReferenceIds", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "description": "Resource references should be passed in the form \"resourceReferenceIds=123&resourceReferenceIds=234&…\". Each integer should be the Resource Reference ID of a Learning Hub resource to be included in the result set. Any unmatched results will be listed in the unmatchedResourceReferenceIds return property." + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/BulkResourceReferenceViewModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkResourceReferenceViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/BulkResourceReferenceViewModel" + } + } + } + } + } + } + }, + "/Resource/BulkJson": { + "get": { + "tags": [ + "Resource" + ], + "description": "Gets a set of Learning Hub resources that match the list of Resource Reference IDs supplied.", + "parameters": [ + { + "name": "resourceReferences", + "in": "query", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "referenceIds": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + }, + "description": "Resource references should be passed in the form \"{referenceIds:[123,234,…]}\". Each integer should be the Resource Reference ID of a Learning Hub resource to be included in the result set. Any unmatched results will be listed in the unmatchedResourceReferenceIds return property." + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/BulkResourceReferenceViewModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkResourceReferenceViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/BulkResourceReferenceViewModel" + } + } + } + } + } + } + }, + "/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." + } + } + } + }, + "/Resource/{id}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific Resource by Id.", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetGenericFileDetails/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific GenericFileDetails by ResourceVersionId.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetHtmlDetails/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific Html resource details by ResourceVersionId.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetScormDetails/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific GetScormFileDetails by ResourceVersionId.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetWeblinkDetails/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The get web link resource version async.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetCaseDetails/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The get case resource version async.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resource version id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetFileStatusDetails": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The GetFileStatusDetailsAsync.", + "parameters": [ + { + "name": "fileIds", + "in": "query", + "description": "The File Ids.", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetFileTypes": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get all file types.", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetFile/{fileId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The get file async.", + "parameters": [ + { + "name": "fileId", + "in": "path", + "description": "The fileIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetResourceLicences": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get all resource licences.", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetResourceVersionViewModel/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific ResourceVersionViewModel by Id.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetResourceVersionForVideo/{fileId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific resource version view model that is linked to a Video file from the file Id.", + "parameters": [ + { + "name": "fileId", + "in": "path", + "description": "The fileIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetResourceVersionForWholeSlideImage/{fileId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific resource version view model that is linked to a whole slide image file from the file Id.", + "parameters": [ + { + "name": "fileId", + "in": "path", + "description": "The fileIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetResourceVersionByResourceReference/{resourceReferenceId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific ResourceVersionViewModel by Resource Reference Id.", + "parameters": [ + { + "name": "resourceReferenceId", + "in": "path", + "description": "The resource reference id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UnpublishResourceVersion": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Unpublish resource version.", + "requestBody": { + "description": "The unpublishViewModelLearningHub.Nhs.Models.Resource.UnpublishViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.UnpublishViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.UnpublishViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.UnpublishViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/RevertToDraft": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Unpublish resource version.", + "requestBody": { + "description": "The resourceVersionIdSystem.Int32.", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/*+json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/AcceptSensitiveContent": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The mark sensitive conatet async.", + "requestBody": { + "description": "The resourceVersionIdSystem.Int32.", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "text/json": { + "schema": { + "type": "integer", + "format": "int32" + } + }, + "application/*+json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetResourceVersions/{resourceId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The get resource versions.", + "parameters": [ + { + "name": "resourceId", + "in": "path", + "description": "The resourceIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetVideoDetails/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific VideoDetails by ResourceVersionId.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetAudioDetails/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific AudioDetails by ResourceVersionId.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetArticleDetails/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get specific Article Details by ResourceVersionId.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/AddResourceVersionKeyword": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Add a new Resource Version Keyword\r\n TODO - requires complete validation - same Keyword added > once.", + "requestBody": { + "description": "The resourceKeywordViewModelLearningHub.Nhs.Models.Resource.ResourceKeywordViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceKeywordViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceKeywordViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceKeywordViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetResourceVersionValidationResult/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The get resource version validation result.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetResourceVersionFlags/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The get resource version flags.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetAssessmentDetails/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Retrieves the entire assessment details given a resource version ID.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resource version id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetAssessmentContent/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Retrieves the assessment details up to the first question, leaving out the feedback and answer types nullified.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resource version id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetAssessmentProgress/activity/{assessmentResourceActivityId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Retrieves the assessment progress.", + "parameters": [ + { + "name": "assessmentResourceActivityId", + "in": "path", + "description": "The assessment resource activity id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetAssessmentProgress/resource/{resourceVersionId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Retrieves the latest assessment progress of a user for the given resource version id, or an empty response if no such attempt exists.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resource version id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetResourceInformationViewModelAsync/{resourceReferenceId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The get resource information view model async.", + "parameters": [ + { + "name": "resourceReferenceId", + "in": "path", + "description": "The resourceReferenceIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetResourceItemViewModelAsync/{resourceReferenceId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The get resource item view model async.", + "parameters": [ + { + "name": "resourceReferenceId", + "in": "path", + "description": "The resourceReferenceIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetCatalogueLocations/{resourceReferenceId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The get catalogue locations.", + "parameters": [ + { + "name": "resourceReferenceId", + "in": "path", + "description": "The resourceReferenceIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetMyContributionsTotals/{catalogueId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Returns totals for \"My Contributions\".", + "parameters": [ + { + "name": "catalogueId", + "in": "path", + "description": "The catalogueIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/HasPublishedResources": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Returns if the user has published resources.", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/SaveArticleAttachedFileDetails": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The save file details for an article async.", + "requestBody": { + "description": "The fileCreateRequestViewModelLearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/SaveResourceAttributeFileDetails": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The save file details for a resource attribute async.", + "requestBody": { + "description": "The fileCreateRequestViewModelLearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/DuplicateBlocks": { + "post": { + "tags": [ + "Resource" + ], + "summary": "DuplicateResourceAsync.", + "requestBody": { + "description": "The duplicateResourceRequestModelLearningHub.Nhs.Models.Resource.Contribute.DuplicateResourceRequestModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.DuplicateBlocksRequestModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.DuplicateBlocksRequestModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.DuplicateBlocksRequestModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/CreateResource": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Create a new Resource and an initial ResourceVersion with a status of \"Draft\".", + "parameters": [ + { + "name": "Title", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Description", + "in": "query", + "schema": { + "maxLength": 4000, + "type": "string" + } + }, + { + "name": "AdditionalInformation", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "ResourceLicenceId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceCatalogueId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "NodeId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "PublishedResourceCatalogueId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "SensitiveContent", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CertificateEnabled", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CreateUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceProviderId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceLicence.Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceLicence.Title", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "ResourceLicence.DisplayOrder", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "VersionStatusEnum", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.VersionStatusEnum" + } + }, + { + "name": "ResourceAccessibilityEnum", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceAccessibilityEnum" + } + }, + { + "name": "CurrentResourceVersionStatusEnum", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.VersionStatusEnum" + } + }, + { + "name": "ResourceAuthors", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceAuthorViewModel" + } + } + }, + { + "name": "ResourceKeywords", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceKeywordViewModel" + } + } + }, + { + "name": "Flags", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceVersionFlagViewModel" + } + } + }, + { + "name": "ResourceId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceType", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceTypeEnum" + } + }, + { + "name": "CurrentResourceVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "DuplicatedFromResourceVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/DuplicateResource": { + "post": { + "tags": [ + "Resource" + ], + "summary": "DuplicateResourceAsync.", + "requestBody": { + "description": "The duplicateResourceRequestModelLearningHub.Nhs.Models.Resource.Contribute.DuplicateResourceRequestModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.DuplicateResourceRequestModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.DuplicateResourceRequestModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.DuplicateResourceRequestModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/CreateNewResourceVersion": { + "post": { + "tags": [ + "Resource" + ], + "summary": "CreateNewResourceVersion.", + "requestBody": { + "description": "The model.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/System.Collections.Generic.KeyValuePair`2[System.String,System.Int32]" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/System.Collections.Generic.KeyValuePair`2[System.String,System.Int32]" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/System.Collections.Generic.KeyValuePair`2[System.String,System.Int32]" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/SetResourceType": { + "post": { + "tags": [ + "Resource" + ], + "summary": "SetResourceType\r\nApplies a ResourceType to a ResourceVersion \"Draft\"\r\nCannot change the ResourceType of a Published ResourceVersion(?)\r\nTODO - requires validation.", + "requestBody": { + "description": "The resourceViewModelLearningHub.Nhs.Models.Resource.Contribute.ResourceViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.ResourceViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.ResourceViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.ResourceViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/SubmitResourceVersionForPublish": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Submit ResourceVersion For Publish.", + "requestBody": { + "description": "The publishViewModelLearningHub.Nhs.Models.Resource.PublishViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.PublishViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.PublishViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.PublishViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateResourceVersion": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The update resource version async.", + "parameters": [ + { + "name": "Title", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Description", + "in": "query", + "schema": { + "maxLength": 4000, + "type": "string" + } + }, + { + "name": "AdditionalInformation", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "ResourceLicenceId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceCatalogueId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "NodeId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "PublishedResourceCatalogueId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "SensitiveContent", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CertificateEnabled", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "CreateUserId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceProviderId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceLicence.Id", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceLicence.Title", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "ResourceLicence.DisplayOrder", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "VersionStatusEnum", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.VersionStatusEnum" + } + }, + { + "name": "ResourceAccessibilityEnum", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceAccessibilityEnum" + } + }, + { + "name": "CurrentResourceVersionStatusEnum", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.VersionStatusEnum" + } + }, + { + "name": "ResourceAuthors", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceAuthorViewModel" + } + } + }, + { + "name": "ResourceKeywords", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceKeywordViewModel" + } + } + }, + { + "name": "Flags", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceVersionFlagViewModel" + } + } + }, + { + "name": "ResourceId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "ResourceType", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceTypeEnum" + } + }, + { + "name": "CurrentResourceVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "DuplicatedFromResourceVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/DeleteResourceVersion/{resourceVersionId}": { + "delete": { + "tags": [ + "Resource" + ], + "summary": "Delete a resource version async.", + "parameters": [ + { + "name": "resourceVersionId", + "in": "path", + "description": "The resourceVersionIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateGenericFileDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Update Generic File Details.", + "requestBody": { + "description": "The genericFileViewModelLearningHub.Nhs.Models.Resource.Contribute.GenericFileUpdateRequestViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.GenericFileUpdateRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.GenericFileUpdateRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.GenericFileUpdateRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateScormDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Update Scorm Detail.", + "requestBody": { + "description": "The scormUpdateRequestViewModelLearningHub.Nhs.Models.Resource.Contribute.ScormUpdateRequestViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.ScormUpdateRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.ScormUpdateRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.ScormUpdateRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateHtmlDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Update HTML Detail.", + "requestBody": { + "description": "Html resource update view model.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.HtmlResourceUpdateRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.HtmlResourceUpdateRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.HtmlResourceUpdateRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateImageDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The update image detail async.", + "requestBody": { + "description": "The imageViewModelLearningHub.Nhs.Models.Resource.Contribute.ImageUpdateRequestViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.ImageUpdateRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.ImageUpdateRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.ImageUpdateRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateVideoDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The update video detail async.", + "requestBody": { + "description": "The videoViewModelLearningHub.Nhs.Models.Resource.Contribute.VideoUpdateRequestViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.VideoUpdateRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.VideoUpdateRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.VideoUpdateRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/DeleteResourceAttributeFile": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Delete resource attribute File async.", + "requestBody": { + "description": "The fileDeleteRequestModelLearningHub.Nhs.Models.Resource.Contribute.FileDeleteRequestModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileDeleteRequestModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileDeleteRequestModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileDeleteRequestModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateAudioDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The update audio detail async.", + "requestBody": { + "description": "The audioViewModelLearningHub.Nhs.Models.Resource.Contribute.AudioUpdateRequestViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.AudioUpdateRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.AudioUpdateRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.AudioUpdateRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateArticleDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Update article detail async.", + "parameters": [ + { + "name": "ResourceVersionId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "Description", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/DeleteArticleFile": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Delete article detail async.", + "requestBody": { + "description": "The fileDeleteRequestModelLearningHub.Nhs.Models.Resource.Contribute.FileDeleteRequestModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileDeleteRequestModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileDeleteRequestModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileDeleteRequestModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/AddResourceVersionAuthor": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Add a new Resource Version Author.", + "requestBody": { + "description": "The resourceAuthorViewModelLearningHub.Nhs.Models.Resource.ResourceAuthorViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceAuthorViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceAuthorViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceAuthorViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/DeleteResourceVersionAuthor": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The delete resource version author async.", + "requestBody": { + "description": "The resourceAuthorViewModelLearningHub.Nhs.Models.Resource.Contribute.AuthorDeleteRequestModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.AuthorDeleteRequestModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.AuthorDeleteRequestModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.AuthorDeleteRequestModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/DeleteResourceVersionKeyword": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The delete resource version keyword async.", + "requestBody": { + "description": "The resourceKeywordViewModelLearningHub.Nhs.Models.Resource.Contribute.KeywordDeleteRequestModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.KeywordDeleteRequestModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.KeywordDeleteRequestModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.KeywordDeleteRequestModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/SaveResourceVersionFlag": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Save the Resource Version Flag.", + "requestBody": { + "description": "The resourceVersionFlagViewModelLearningHub.Nhs.Models.Resource.ResourceVersionFlagViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceVersionFlagViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceVersionFlagViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.ResourceVersionFlagViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/DeleteResourceVersionFlag": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The delete resource version flag async.", + "parameters": [ + { + "name": "resourceVersionFlagId", + "in": "query", + "description": "The resourceVersionFlagIdSystem.Int32.", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateWeblinkDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The update web link resource version async.", + "requestBody": { + "description": "The webLinkViewModelLearningHub.Nhs.Models.Resource.WebLinkViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.WebLinkViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.WebLinkViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.WebLinkViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateCaseDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The update case resource version async.", + "requestBody": { + "description": "The web link view model.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.CaseViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.CaseViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.CaseViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/UpdateAssessmentDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "This method updates the database entry with the assessment details, passed down as a parameter.", + "requestBody": { + "description": "The web link view model.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.AssessmentViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.AssessmentViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.AssessmentViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/SaveFileChunkDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The save file chunk detail async.", + "requestBody": { + "description": "The fileChunkDetailCreateRequestViewModelLearningHub.Nhs.Models.Resource.Contribute.FileChunkDetailViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileChunkDetailViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileChunkDetailViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileChunkDetailViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/GetFileChunkDetail/{fileChunkDetailId}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "The get file chunk detail async.", + "parameters": [ + { + "name": "fileChunkDetailId", + "in": "path", + "description": "The fileChunkDetailIdSystem.Int32.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/DeleteFileChunkDetail": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Delete a file chunk detail async.", + "requestBody": { + "description": "The fileChunkDetailDeleteRequestModelLearningHub.Nhs.Models.Resource.Contribute.FileChunkDetailDeleteRequestModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileChunkDetailDeleteRequestModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileChunkDetailDeleteRequestModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileChunkDetailDeleteRequestModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Resource/SaveFileDetails": { + "post": { + "tags": [ + "Resource" + ], + "summary": "The save file details async.", + "requestBody": { + "description": "The fileCreateRequestViewModelLearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Search/GetAllCatalogueSearchResult": { + "post": { + "tags": [ + "Search" + ], + "summary": "Get AllCatalogue search result.", + "requestBody": { + "description": "The catalogue search request model.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Search.AllCatalogueSearchRequestModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Search.AllCatalogueSearchRequestModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Search.AllCatalogueSearchRequestModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Search/GetAutoSuggestionResult/{term}": { + "get": { + "tags": [ + "Search" + ], + "summary": "Get AutoSuggestionResults.", + "parameters": [ + { + "name": "term", + "in": "path", + "description": "The term.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/User/CreateUserProfile": { + "post": { + "tags": [ + "User" + ], + "summary": "Create specific User Profile.", + "requestBody": { + "description": "The userProfile.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + } + } + } + } + } + }, + "/User/UpdateUserProfile": { + "put": { + "tags": [ + "User" + ], + "summary": "Update specific User Profile.", + "requestBody": { + "description": "The userProfile.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + } + } + } + } + } + }, + "/User/GetByUserId/{id}": { + "get": { + "tags": [ + "User" + ], + "summary": "Get specific User Profile by Id.", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + } + } + } + } + } + } + }, + "/User/GetCurrentUserProfile": { + "get": { + "tags": [ + "User" + ], + "summary": "Get current User Profile.", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProfile" + } + } + } + } + } + } + }, + "/User/CreateUser": { + "post": { + "tags": [ + "User" + ], + "summary": "Create a new user.", + "requestBody": { + "description": "The userCreateViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.User.UserCreateViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.User.UserCreateViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.User.UserCreateViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/User/UpdateUser": { + "post": { + "tags": [ + "User" + ], + "summary": "Create a new user.", + "requestBody": { + "description": "The userCreateViewModel.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.User.UserUpdateViewModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.User.UserUpdateViewModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.User.UserUpdateViewModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/User/GetLastIssuedEmailChangeValidationToken": { + "get": { + "tags": [ + "User" + ], + "summary": "GetLastIssuedEmailChangeValidationToken.", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/User/GenerateEmailChangeValidationTokenAndSendEmail/{emailAddress}/{isUserRoleUpgrade}": { + "get": { + "tags": [ + "User" + ], + "summary": "Generate email change token.", + "parameters": [ + { + "name": "emailAddress", + "in": "path", + "description": "emailAddress.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "isUserRoleUpgrade", + "in": "path", + "description": "isUserRoleUpgrade.", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/User/ReGenerateEmailChangeValidationToken/{newPrimaryEmail}/{isUserRoleUpgrade}": { + "get": { + "tags": [ + "User" + ], + "summary": "Regenerate email change token.", + "parameters": [ + { + "name": "newPrimaryEmail", + "in": "path", + "description": "emailAddress.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "isUserRoleUpgrade", + "in": "path", + "description": "isUserRoleUpgrade.", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/User/CancelEmailChangeValidationToken": { + "get": { + "tags": [ + "User" + ], + "summary": "Regenerate email change token.", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/User/ValidateEmailChangeToken/{token}/{loctoken}/{isUserRoleUpgrade}": { + "get": { + "tags": [ + "User" + ], + "summary": "Validate email change token.", + "parameters": [ + { + "name": "token", + "in": "path", + "description": "The token.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "loctoken", + "in": "path", + "description": "The loc Token.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "isUserRoleUpgrade", + "in": "path", + "description": "isUserRoleUpgrade.", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "LearningHub.Nhs.Models.Bookmark.UserBookmarkViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "bookmarkTypeId": { + "type": "integer", + "format": "int32" + }, + "parentId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "userId": { + "type": "integer", + "format": "int32" + }, + "resourceReferenceId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "resourceTypeId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "nodeId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "link": { + "type": "string", + "nullable": true + }, + "position": { + "type": "integer", + "format": "int32" + }, + "childrenCount": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Catalogue.CatalogueAccessRejectionViewModel": { + "type": "object", + "properties": { + "rejectionReason": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Catalogue.CatalogueAccessRequestViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "username": { + "type": "string", + "nullable": true + }, + "userId": { + "type": "integer", + "format": "int32" + }, + "userFullName": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.CatalogueAccessRequestStatus" + }, + "dateRequested": { + "type": "string", + "format": "date-time" + }, + "dateApproved": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "dateRejected": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "emailAddress": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + }, + "responseMessage": { + "type": "string", + "nullable": true + }, + "lastResponseMessage": { + "type": "string", + "nullable": true + }, + "roleId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Catalogue.CatalogueBasicViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "nodeId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "hidden": { + "type": "boolean" + }, + "url": { + "type": "string", + "nullable": true + }, + "badgeUrl": { + "type": "string", + "nullable": true + }, + "restrictedAccess": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Catalogue.CatalogueResourceRequestViewModel": { + "type": "object", + "properties": { + "nodeId": { + "type": "integer", + "format": "int32" + }, + "offset": { + "type": "integer", + "format": "int32" + }, + "catalogueOrder": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.CatalogueOrder" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueAccessRequestsRequestViewModel": { + "type": "object", + "properties": { + "catalogueNodeId": { + "type": "integer", + "format": "int32" + }, + "includeNew": { + "type": "boolean" + }, + "includeApproved": { + "type": "boolean" + }, + "includeDenied": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueInviteUserViewModel": { + "type": "object", + "properties": { + "catalogueNodeId": { + "type": "integer", + "format": "int32" + }, + "emailAddress": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Catalogue.RestrictedCatalogueUsersRequestViewModel": { + "type": "object", + "properties": { + "catalogueNodeId": { + "type": "integer", + "format": "int32" + }, + "emailAddressFilter": { + "type": "string", + "nullable": true + }, + "includeCatalogueAdmins": { + "type": "boolean" + }, + "includePlatformAdmins": { + "type": "boolean" + }, + "skip": { + "type": "integer", + "format": "int32" + }, + "take": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.ActivityStatus": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "nullable": true + }, + "resourceActivity": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ResourceActivity" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivity": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceActivityId": { + "type": "integer", + "format": "int32" + }, + "score": { + "type": "number", + "format": "double", + "nullable": true + }, + "reason": { + "type": "string", + "nullable": true + }, + "resourceActivity": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ResourceActivity" + }, + "assessmentResourceActivityInteractions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivityInteraction" + }, + "nullable": true + }, + "matchQuestions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivityMatchQuestion" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivityInteraction": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "assessmentResourceActivityId": { + "type": "integer", + "format": "int32" + }, + "assessmentResourceActivity": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivity" + }, + "questionBlockId": { + "type": "integer", + "format": "int32" + }, + "questionBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.QuestionBlock" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivityInteractionAnswer" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivityInteractionAnswer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "assessmentResourceActivityInteractionId": { + "type": "integer", + "format": "int32" + }, + "assessmentResourceActivityInteraction": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivityInteraction" + }, + "questionAnswerId": { + "type": "integer", + "format": "int32" + }, + "questionAnswer": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.QuestionAnswer" + }, + "matchedQuestionAnswerId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "matchedQuestionAnswer": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.QuestionAnswer" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivityMatchQuestion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "assessmentResourceActivityId": { + "type": "integer", + "format": "int32" + }, + "assessmentResourceActivity": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivity" + }, + "questionNumber": { + "type": "integer", + "format": "int32" + }, + "firstMatchAnswerId": { + "type": "integer", + "format": "int32" + }, + "firstMatchAnswer": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.QuestionAnswer" + }, + "secondMatchAnswerId": { + "type": "integer", + "format": "int32" + }, + "secondMatchAnswer": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.QuestionAnswer" + }, + "order": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.MajorVersionIdActivityStatusDescription": { + "type": "object", + "properties": { + "majorVersionId": { + "type": "integer", + "format": "int32" + }, + "activityStatusDescription": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.MediaResourceActivity": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceActivityId": { + "type": "integer", + "format": "int32" + }, + "activityStart": { + "type": "string", + "format": "date-time" + }, + "secondsPlayed": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "percentComplete": { + "type": "number", + "format": "double", + "nullable": true + }, + "resourceActivity": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ResourceActivity" + }, + "mediaResourceActivityInteraction": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.MediaResourceActivityInteraction" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.MediaResourceActivityInteraction": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "mediaResourceActivityId": { + "type": "integer", + "format": "int32" + }, + "displayTime": { + "type": "string", + "format": "date-span" + }, + "mediaResourceActivityTypeId": { + "type": "integer", + "format": "int32" + }, + "clientDateTime": { + "type": "string", + "format": "date-time" + }, + "mediaResourceActivityType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.MediaResourceActivityType" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.MediaResourceActivityType": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "nullable": true + }, + "mediaResourceActivityInteraction": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.MediaResourceActivityInteraction" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.ResourceActivity": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "userId": { + "type": "integer", + "format": "int32" + }, + "resourceId": { + "type": "integer", + "format": "int32" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "launchResourceActivityId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "majorVersion": { + "type": "integer", + "format": "int32" + }, + "minorVersion": { + "type": "integer", + "format": "int32" + }, + "nodePathId": { + "type": "integer", + "format": "int32" + }, + "activityStatusId": { + "type": "integer", + "format": "int32" + }, + "activityStart": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "activityEnd": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "durationSeconds": { + "type": "integer", + "format": "int32" + }, + "score": { + "type": "number", + "format": "double", + "nullable": true + }, + "resource": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Resource" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "activityStatus": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ActivityStatus" + }, + "launchResourceActivity": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ResourceActivity" + }, + "nodePath": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodePath" + }, + "inverseLaunchResourceActivity": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ResourceActivity" + }, + "nullable": true + }, + "mediaResourceActivity": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.MediaResourceActivity" + }, + "nullable": true + }, + "assessmentResourceActivity": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.AssessmentResourceActivity" + }, + "nullable": true + }, + "scormActivity": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ScormActivity" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.ScormActivity": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceActivityId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "cmiCoreLessonLocation": { + "type": "string", + "nullable": true + }, + "cmiCoreLessonStatus": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "cmiCoreScoreRaw": { + "type": "number", + "format": "double", + "nullable": true + }, + "cmiCoreScoreMin": { + "type": "number", + "format": "double", + "nullable": true + }, + "cmiCoreScoreMax": { + "type": "number", + "format": "double", + "nullable": true + }, + "cmiCoreExit": { + "type": "string", + "nullable": true + }, + "cmiCoreSessionTime": { + "type": "string", + "nullable": true + }, + "cmiSuspendData": { + "type": "string", + "nullable": true + }, + "durationSeconds": { + "type": "integer", + "format": "int32" + }, + "resourceActivity": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ResourceActivity" + }, + "scormActivityInteraction": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ScormActivityInteraction" + }, + "nullable": true + }, + "scormActivityObjective": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ScormActivityObjective" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.ScormActivityInteraction": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "scormActivityId": { + "type": "integer", + "format": "int32" + }, + "interactionId": { + "type": "string", + "nullable": true + }, + "sequenceNumber": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string", + "nullable": true + }, + "weighting": { + "type": "number", + "format": "double", + "nullable": true + }, + "studentResponse": { + "type": "string", + "nullable": true + }, + "result": { + "type": "string", + "nullable": true + }, + "latency": { + "type": "string", + "nullable": true + }, + "scormActivity": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ScormActivity" + }, + "scormActivityInteractionCorrectResponse": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ScormActivityInteractionCorrectResponse" + }, + "nullable": true + }, + "scormActivityInteractionObjective": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ScormActivityInteractionObjective" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.ScormActivityInteractionCorrectResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "scormActivityInteractionId": { + "type": "integer", + "format": "int32" + }, + "index": { + "type": "integer", + "format": "int32" + }, + "pattern": { + "type": "string", + "nullable": true + }, + "scormActivityInteraction": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ScormActivityInteraction" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.ScormActivityInteractionObjective": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "scormActivityInteractionId": { + "type": "integer", + "format": "int32" + }, + "index": { + "type": "integer", + "format": "int32" + }, + "objectiveId": { + "type": "string", + "nullable": true + }, + "scormActivityInteraction": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ScormActivityInteraction" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Activity.ScormActivityObjective": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "scormActivityId": { + "type": "integer", + "format": "int32" + }, + "objectiveId": { + "type": "string", + "nullable": true + }, + "sequenceNumber": { + "type": "integer", + "format": "int32" + }, + "scoreRaw": { + "type": "string", + "nullable": true + }, + "scoreMax": { + "type": "string", + "nullable": true + }, + "scoreMin": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string", + "nullable": true + }, + "scormActivity": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ScormActivity" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Address": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "address1": { + "type": "string", + "nullable": true + }, + "address2": { + "type": "string", + "nullable": true + }, + "address3": { + "type": "string", + "nullable": true + }, + "address4": { + "type": "string", + "nullable": true + }, + "town": { + "type": "string", + "nullable": true + }, + "county": { + "type": "string", + "nullable": true + }, + "postCode": { + "type": "string", + "nullable": true + }, + "equipmentResourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.EquipmentResourceVersion" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Attribute": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "attributeTypeEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.AttributeTypeEnum" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Content.ImageAsset": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "pageSectionDetailId": { + "type": "integer", + "format": "int32" + }, + "imageFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "altTag": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "imageFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "pageSectionDetail": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.PageSectionDetail" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Content.Page": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string", + "nullable": true + }, + "pageSections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.PageSection" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Content.PageSection": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "pageId": { + "type": "integer", + "format": "int32" + }, + "position": { + "type": "integer", + "format": "int32" + }, + "isHidden": { + "type": "boolean" + }, + "sectionTemplateTypeId": { + "type": "integer", + "format": "int32" + }, + "page": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.Page" + }, + "pageSectionDetails": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.PageSectionDetail" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Content.PageSectionDetail": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "pageSectionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "sectionTitle": { + "type": "string", + "nullable": true + }, + "sectionTitleElement": { + "type": "string", + "nullable": true + }, + "topMargin": { + "type": "boolean" + }, + "bottomMargin": { + "type": "boolean" + }, + "hasBorder": { + "type": "boolean" + }, + "backgroundColour": { + "type": "string", + "nullable": true + }, + "textColour": { + "type": "string", + "nullable": true + }, + "hyperLinkColour": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "textBackgroundColour": { + "type": "string", + "nullable": true + }, + "sectionLayoutTypeId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "pageSectionStatusId": { + "type": "integer", + "format": "int32" + }, + "deletePending": { + "type": "boolean", + "nullable": true + }, + "draftHidden": { + "type": "boolean", + "nullable": true + }, + "draftPosition": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "pageSection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.PageSection" + }, + "imageAsset": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.ImageAsset" + }, + "videoAsset": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.VideoAsset" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Content.VideoAsset": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "pageSectionDetailId": { + "type": "integer", + "format": "int32" + }, + "videoFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "transcriptFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "closedCaptionsFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "thumbnailImageFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "durationInMilliseconds": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "azureMediaAssetId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "azureMediaAsset": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceAzureMediaAsset" + }, + "closedCaptionsFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "pageSectionDetail": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.PageSectionDetail" + }, + "thumbnailImageFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "transcriptFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "videoFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.CatalogueNodeVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "nodeVersionId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string", + "nullable": true + }, + "badgeUrl": { + "type": "string", + "nullable": true + }, + "cardImageUrl": { + "type": "string", + "nullable": true + }, + "bannerUrl": { + "type": "string", + "nullable": true + }, + "certificateUrl": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "ownerName": { + "type": "string", + "nullable": true + }, + "ownerEmailAddress": { + "type": "string", + "nullable": true + }, + "notes": { + "type": "string", + "nullable": true + }, + "order": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.CatalogueOrder" + }, + "keywords": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.CatalogueNodeVersionKeyword" + }, + "nullable": true + }, + "catalogueNodeVersionProvider": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.CatalogueNodeVersionProvider" + }, + "nodeVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodeVersion" + }, + "restrictedAccess": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.CatalogueNodeVersionKeyword": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "catalogueNodeVersionId": { + "type": "integer", + "format": "int32" + }, + "keyword": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.CatalogueNodeVersionProvider": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "catalogueNodeVersionId": { + "type": "integer", + "format": "int32" + }, + "providerId": { + "type": "integer", + "format": "int32" + }, + "removalDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "provider": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Provider" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.FolderNodeVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "nodeVersionId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "nodeVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodeVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.HierarchyEdit": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "rootNodeId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "publicationId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "rootNode": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + }, + "publication": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Publication" + }, + "createUser": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + }, + "hierarchyEditStatus": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.HierarchyEditStatusEnum" + }, + "hierarchyEditDetail": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.HierarchyEditDetail" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.HierarchyEditDetail": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "hierarchyEditId": { + "type": "integer", + "format": "int32" + }, + "hierarchyEditDetailType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.HierarchyEditDetailTypeEnum" + }, + "hierarchyEditDetailOperation": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.HierarchyEditDetailOperationEnum" + }, + "nodeId": { + "type": "integer", + "format": "int32" + }, + "nodeVersionId": { + "type": "integer", + "format": "int32" + }, + "parentNodeId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "nodeLinkId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "displayOrder": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "hierarchyEdit": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.HierarchyEdit" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.Node": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "currentNodeVersionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "hidden": { + "type": "boolean" + }, + "currentNodeVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodeVersion" + }, + "nodeTypeEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.NodeTypeEnum" + }, + "nodePaths": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodePath" + }, + "nullable": true + }, + "nodePathNodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodePathNode" + }, + "nullable": true + }, + "nodeLinkChildNode": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodeLink" + }, + "nullable": true + }, + "nodeLinkParentNode": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodeLink" + }, + "nullable": true + }, + "nodeVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodeVersion" + }, + "nullable": true + }, + "nodeResource": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodeResource" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.NodeLink": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "parentNodeId": { + "type": "integer", + "format": "int32" + }, + "childNodeId": { + "type": "integer", + "format": "int32" + }, + "displayOrder": { + "type": "integer", + "format": "int32" + }, + "childNode": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + }, + "parentNode": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.NodePath": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "nodeId": { + "type": "integer", + "format": "int32" + }, + "nodePathString": { + "type": "string", + "nullable": true + }, + "catalogueNodeId": { + "type": "integer", + "format": "int32" + }, + "isActive": { + "type": "boolean" + }, + "nodePathNode": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodePathNode" + }, + "nullable": true + }, + "node": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + }, + "catalogueNode": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + }, + "nodePathDisplay": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodePathDisplay" + }, + "resourceReference": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceReference" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.NodePathDisplay": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "nodePathId": { + "type": "integer", + "format": "int32" + }, + "displayName": { + "type": "string", + "nullable": true + }, + "icon": { + "type": "string", + "nullable": true + }, + "nodePath": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodePath" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.NodePathNode": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "nodePathId": { + "type": "integer", + "format": "int32" + }, + "nodeId": { + "type": "integer", + "format": "int32" + }, + "depth": { + "type": "integer", + "format": "int32" + }, + "node": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + }, + "nodePath": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodePath" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.NodeResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "node": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + }, + "nodeId": { + "type": "integer", + "format": "int32" + }, + "resource": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Resource" + }, + "resourceId": { + "type": "integer", + "format": "int32" + }, + "displayOrder": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "versionStatusEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.VersionStatusEnum" + }, + "publicationId": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.NodeVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "nodeId": { + "type": "integer", + "format": "int32" + }, + "versionStatusEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.VersionStatusEnum" + }, + "majorVersion": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "minorVersion": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "publication": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Publication" + }, + "node": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + }, + "nodeWhereCurrent": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + }, + "catalogueNodeVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.CatalogueNodeVersion" + }, + "folderNodeVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.FolderNodeVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.Publication": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "nodeVersionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "notes": { + "type": "string", + "nullable": true + }, + "submittedToSearch": { + "type": "boolean" + }, + "nodeVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodeVersion" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "publicationLog": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.PublicationLog" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Hierarchy.PublicationLog": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "publicationId": { + "type": "integer", + "format": "int32" + }, + "nodeId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "publication": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Publication" + }, + "node": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Notification": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "title": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "endDate": { + "type": "string", + "format": "date-time" + }, + "userDismissable": { + "type": "boolean" + }, + "notificationTypeEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.NotificationTypeEnum" + }, + "notificationPriorityEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.NotificationPriorityEnum" + }, + "isUserSpecific": { + "type": "boolean" + }, + "createUser": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + }, + "amendUser": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + }, + "userNotification": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Permission": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "code": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "permissionRole": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.PermissionRole" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.PermissionRole": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "permissionId": { + "type": "integer", + "format": "int32" + }, + "roleId": { + "type": "integer", + "format": "int32" + }, + "permission": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Permission" + }, + "role": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Role" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Provider": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "logo": { + "type": "string", + "nullable": true + }, + "createUser": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + }, + "amendUser": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + }, + "userProvider": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProvider" + }, + "nullable": true + }, + "resourceVersionProvider": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionProvider" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ArticleResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "content": { + "type": "string", + "nullable": true + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "articleResourceVersionFile": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ArticleResourceVersionFile" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ArticleResourceVersionFile": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "articleResourceVersionId": { + "type": "integer", + "format": "int32" + }, + "fileId": { + "type": "integer", + "format": "int32" + }, + "articleResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ArticleResourceVersion" + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.AssessmentResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "assessmentType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.AssessmentTypeEnum" + }, + "assessmentContentId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "assessmentContent": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.BlockCollection" + }, + "maximumAttempts": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "passMark": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "answerInOrder": { + "type": "boolean" + }, + "endGuidanceId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "endGuidance": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.BlockCollection" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.AudioResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "audioFileId": { + "type": "integer", + "format": "int32" + }, + "transcriptFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "transcriptFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "resourceAzureMediaAssetId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "durationInMilliseconds": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "resourceAzureMediaAsset": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceAzureMediaAsset" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.Attachment": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "fileId": { + "type": "integer", + "format": "int32" + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "mediaBlocks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.MediaBlock" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.Block": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "blockCollectionId": { + "type": "integer", + "format": "int32" + }, + "blockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.BlockCollection" + }, + "order": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "blockType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockType" + }, + "textBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.TextBlock" + }, + "wholeSlideImageBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.WholeSlideImageBlock" + }, + "mediaBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.MediaBlock" + }, + "questionBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.QuestionBlock" + }, + "imageCarouselBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.ImageCarouselBlock" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.BlockCollection": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "blocks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Block" + }, + "nullable": true + }, + "caseResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.CaseResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.Image": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "fileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "altText": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "mediaBlocks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.MediaBlock" + }, + "nullable": true + }, + "imageAnnotations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ImageAnnotation" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.ImageCarouselBlock": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "blockId": { + "type": "integer", + "format": "int32" + }, + "block": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Block" + }, + "imageBlockCollectionId": { + "type": "integer", + "format": "int32" + }, + "imageBlockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.BlockCollection" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.MediaBlock": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "blockId": { + "type": "integer", + "format": "int32" + }, + "block": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Block" + }, + "mediaType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.MediaType" + }, + "attachmentId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "attachment": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Attachment" + }, + "imageId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "image": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Image" + }, + "videoId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "video": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Video" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.QuestionBlock": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "blockId": { + "type": "integer", + "format": "int32" + }, + "block": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Block" + }, + "questionBlockCollectionId": { + "type": "integer", + "format": "int32" + }, + "questionBlockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.BlockCollection" + }, + "feedbackBlockCollectionId": { + "type": "integer", + "format": "int32" + }, + "feedbackBlockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.BlockCollection" + }, + "questionType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.QuestionTypeEnum" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.QuestionAnswer" + }, + "nullable": true + }, + "allowReveal": { + "type": "boolean", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.TextBlock": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "blockId": { + "type": "integer", + "format": "int32" + }, + "block": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Block" + }, + "content": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.Video": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "fileId": { + "type": "integer", + "format": "int32" + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "mediaBlocks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.MediaBlock" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.WholeSlideImage": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "title": { + "type": "string", + "nullable": true + }, + "fileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "wholeSlideImageBlockItems": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.WholeSlideImageBlockItem" + }, + "nullable": true + }, + "imageAnnotations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ImageAnnotation" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.WholeSlideImageBlock": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "blockId": { + "type": "integer", + "format": "int32" + }, + "block": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Block" + }, + "wholeSlideImageBlockItems": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.WholeSlideImageBlockItem" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Blocks.WholeSlideImageBlockItem": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "wholeSlideImageBlockId": { + "type": "integer", + "format": "int32" + }, + "wholeSlideImageBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.WholeSlideImageBlock" + }, + "wholeSlideImageId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "wholeSlideImage": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.WholeSlideImage" + }, + "placeholderText": { + "type": "string", + "nullable": true + }, + "order": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.CaptionsFile": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "fileId": { + "type": "integer", + "format": "int32" + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "videoFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.VideoFile" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.CaseResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "blockCollectionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "blockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.BlockCollection" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.EmbeddedResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "embedCode": { + "type": "string", + "nullable": true + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.EquipmentResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "contactName": { + "type": "string", + "nullable": true + }, + "contactTelephone": { + "type": "string", + "nullable": true + }, + "contactEmail": { + "type": "string", + "nullable": true + }, + "addressId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "address": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Address" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.File": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "fileTypeId": { + "type": "integer", + "format": "int32" + }, + "fileChunkDetailId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "fileName": { + "type": "string", + "nullable": true + }, + "filePath": { + "type": "string", + "nullable": true + }, + "fileSizeKb": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "fileType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.FileType" + }, + "articleResourceVersionFile": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ArticleResourceVersionFile" + }, + "nullable": true + }, + "genericFileResourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.GenericFileResourceVersion" + }, + "nullable": true + }, + "htmlResourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.HtmlResourceVersion" + }, + "nullable": true + }, + "imageResourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ImageResourceVersion" + }, + "nullable": true + }, + "videoResourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.VideoResourceVersion" + }, + "nullable": true + }, + "audioResourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.AudioResourceVersion" + }, + "nullable": true + }, + "partialFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.PartialFile" + }, + "wholeSlideImageFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.WholeSlideImageFile" + }, + "videoFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.VideoFile" + }, + "captionsFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.CaptionsFile" + }, + "transcriptFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.TranscriptFile" + }, + "wholeSlideImages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.WholeSlideImage" + }, + "nullable": true + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Attachment" + }, + "nullable": true + }, + "images": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Image" + }, + "nullable": true + }, + "videos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Video" + }, + "nullable": true + }, + "imageAssets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.ImageAsset" + }, + "nullable": true + }, + "videoAssettClosedCaptionsFiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.VideoAsset" + }, + "nullable": true + }, + "videoAssettThumbnailImageFiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.VideoAsset" + }, + "nullable": true + }, + "videoAssettTranscriptFiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.VideoAsset" + }, + "nullable": true + }, + "videoAssettVideoFiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Content.VideoAsset" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.FileChunkDetail": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "fileName": { + "type": "string", + "nullable": true + }, + "chunkCount": { + "type": "integer", + "format": "int32" + }, + "filePath": { + "type": "string", + "nullable": true + }, + "fileSizeKb": { + "type": "integer", + "format": "int32" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.FileType": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "defaultResourceTypeId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "extension": { + "type": "string", + "nullable": true + }, + "icon": { + "type": "string", + "nullable": true + }, + "notAllowed": { + "type": "boolean" + }, + "defaultResourceType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceType" + }, + "file": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.GenericFileResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "fileId": { + "type": "integer", + "format": "int32" + }, + "scormAiccContent": { + "type": "boolean" + }, + "authoredYear": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "authoredMonth": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "authoredDayOfMonth": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "esrLinkTypeId": { + "type": "integer", + "format": "int32" + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.HtmlResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "fileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "contentFilePath": { + "type": "string", + "nullable": true + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "popupWidth": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "popupHeight": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "esrLinkTypeId": { + "type": "integer", + "format": "int32" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ImageAnnotation": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "wholeSlideImageId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "wholeSlideImage": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.WholeSlideImage" + }, + "imageId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "image": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.Image" + }, + "order": { + "type": "integer", + "format": "int32" + }, + "label": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "pinXCoordinate": { + "type": "number", + "format": "double" + }, + "pinYCoordinate": { + "type": "number", + "format": "double" + }, + "colour": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ImageAnnotationColourEnum" + }, + "imageAnnotationMarks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ImageAnnotationMark" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ImageAnnotationMark": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "imageAnnotationId": { + "type": "integer", + "format": "int32" + }, + "imageAnnotation": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ImageAnnotation" + }, + "type": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ImageAnnotationMarkTypeEnum" + }, + "markShapeData": { + "type": "string", + "nullable": true + }, + "markLabel": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ImageResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "imageFileId": { + "type": "integer", + "format": "int32" + }, + "altTag": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.PartialFile": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "fileId": { + "type": "integer", + "format": "int32" + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "totalSize": { + "type": "integer", + "format": "int64" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.QuestionAnswer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "questionBlockId": { + "type": "integer", + "format": "int32" + }, + "questionBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.QuestionBlock" + }, + "order": { + "type": "integer", + "format": "int32" + }, + "status": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.QuestionAnswerStatus" + }, + "blockCollectionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "blockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Blocks.BlockCollection" + }, + "imageAnnotationId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "imageAnnotation": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ImageAnnotation" + }, + "imageAnnotationOrder": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.Resource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "currentResourceVersionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "duplicatedFromResourceVersionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "duplicatedFromResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "resourceTypeEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceTypeEnum" + }, + "currentResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "resourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "nullable": true + }, + "nodeResource": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodeResource" + }, + "nullable": true + }, + "resourceReference": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceReference" + }, + "nullable": true + }, + "resourceActivity": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.ResourceActivity" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceAzureMediaAsset": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "filePath": { + "type": "string", + "nullable": true + }, + "locatorUri": { + "type": "string", + "nullable": true + }, + "encodeJobName": { + "type": "string", + "nullable": true + }, + "audioResourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.AudioResourceVersion" + }, + "nullable": true + }, + "videoResourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.VideoResourceVersion" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceLicence": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "title": { + "type": "string", + "nullable": true + }, + "displayOrder": { + "type": "integer", + "format": "int32" + }, + "resourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceReference": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "originalResourceReferenceId": { + "type": "integer", + "format": "int32" + }, + "resourceId": { + "type": "integer", + "format": "int32" + }, + "nodePathId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "resource": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Resource" + }, + "nodePath": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.NodePath" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceType": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "fileType": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.FileType" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceId": { + "type": "integer", + "format": "int32" + }, + "publicationId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "majorVersion": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "minorVersion": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "additionalInformation": { + "type": "string", + "nullable": true + }, + "providerId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "devId": { + "type": "string", + "nullable": true + }, + "reviewDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "resource": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Resource" + }, + "resourceAccessibilityEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceAccessibilityEnum" + }, + "resourceWhereCurrent": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.Resource" + }, + "versionStatusEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.VersionStatusEnum" + }, + "articleResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ArticleResourceVersion" + }, + "embeddedResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.EmbeddedResourceVersion" + }, + "equipmentResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.EquipmentResourceVersion" + }, + "webLinkResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.WebLinkResourceVersion" + }, + "genericFileResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.GenericFileResourceVersion" + }, + "htmlResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.HtmlResourceVersion" + }, + "imageResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ImageResourceVersion" + }, + "videoResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.VideoResourceVersion" + }, + "audioResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.AudioResourceVersion" + }, + "scormResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ScormResourceVersion" + }, + "resourceVersionValidationResult": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionValidationResult" + }, + "caseResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.CaseResourceVersion" + }, + "assessmentResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.AssessmentResourceVersion" + }, + "resourceVersionAuthor": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionAuthor" + }, + "nullable": true + }, + "resourceVersionKeyword": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionKeyword" + }, + "nullable": true + }, + "resourceVersionEvent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionEvent" + }, + "nullable": true + }, + "resourceVersionFlag": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionFlag" + }, + "nullable": true + }, + "publication": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Publication" + }, + "hasCost": { + "type": "boolean" + }, + "cost": { + "type": "number", + "format": "double", + "nullable": true + }, + "resourceLicenceId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "resourceLicence": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceLicence" + }, + "sensitiveContent": { + "type": "boolean" + }, + "certificateEnabled": { + "type": "boolean", + "nullable": true + }, + "createUser": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + }, + "resourceVersionRatings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionRating" + }, + "nullable": true + }, + "resourceVersionRatingSummary": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionRatingSummary" + }, + "fileChunkDetail": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.FileChunkDetail" + }, + "nullable": true + }, + "resourceVersionUserAcceptance": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionUserAcceptance" + }, + "nullable": true + }, + "resourceVersionProvider": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionProvider" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersionAuthor": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "authorUserId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "authorName": { + "type": "string", + "nullable": true + }, + "organisation": { + "type": "string", + "nullable": true + }, + "role": { + "type": "string", + "nullable": true + }, + "isContributor": { + "type": "boolean" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersionEvent": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "resourceVersionEventType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceVersionEventTypeEnum" + }, + "details": { + "type": "string", + "nullable": true + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "createUser": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersionFlag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "details": { + "type": "string", + "nullable": true + }, + "isActive": { + "type": "boolean" + }, + "resolution": { + "type": "string", + "nullable": true + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersionKeyword": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "keyword": { + "type": "string", + "nullable": true + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersionProvider": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "providerId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "provider": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Provider" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersionRating": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "userId": { + "type": "integer", + "format": "int32" + }, + "rating": { + "type": "integer", + "format": "int32" + }, + "jobRoleId": { + "type": "integer", + "format": "int32" + }, + "locationId": { + "type": "integer", + "format": "int32" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersionRatingSummary": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "averageRating": { + "type": "number", + "format": "double" + }, + "ratingCount": { + "type": "integer", + "format": "int32" + }, + "rating1StarCount": { + "type": "integer", + "format": "int32" + }, + "rating2StarCount": { + "type": "integer", + "format": "int32" + }, + "rating3StarCount": { + "type": "integer", + "format": "int32" + }, + "rating4StarCount": { + "type": "integer", + "format": "int32" + }, + "rating5StarCount": { + "type": "integer", + "format": "int32" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersionUserAcceptance": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "userId": { + "type": "integer", + "format": "int32" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersionValidationResult": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "resourceVersionValidationRuleResults": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionValidationRuleResult" + }, + "nullable": true + }, + "success": { + "type": "boolean" + }, + "details": { + "type": "string", + "nullable": true + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "createUser": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ResourceVersionValidationRuleResult": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionValidationResultId": { + "type": "integer", + "format": "int32" + }, + "resourceTypeValidationRuleEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceTypeValidationRuleEnum" + }, + "success": { + "type": "boolean" + }, + "details": { + "type": "string", + "nullable": true + }, + "resourceVersionValidationResult": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionValidationResult" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ScormResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "fileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "contentFilePath": { + "type": "string", + "nullable": true + }, + "developmentId": { + "type": "string", + "nullable": true + }, + "esrLinkTypeId": { + "type": "integer", + "format": "int32" + }, + "canDownload": { + "type": "boolean" + }, + "popupWidth": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "popupHeight": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "clearSuspendData": { + "type": "boolean" + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "scormResourceVersionManifest": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ScormResourceVersionManifest" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.ScormResourceVersionManifest": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "scormResourceVersionId": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "author": { + "type": "string", + "nullable": true + }, + "keywords": { + "type": "string", + "nullable": true + }, + "duration": { + "type": "string", + "nullable": true + }, + "manifestUrl": { + "type": "string", + "nullable": true + }, + "quicklinkId": { + "type": "string", + "nullable": true + }, + "catalogEntry": { + "type": "string", + "nullable": true + }, + "masteryScore": { + "type": "number", + "format": "double", + "nullable": true + }, + "copyright": { + "type": "string", + "nullable": true + }, + "resourceIdentifier": { + "type": "string", + "nullable": true + }, + "itemIdentifier": { + "type": "string", + "nullable": true + }, + "templateVersion": { + "type": "string", + "nullable": true + }, + "launchData": { + "type": "string", + "nullable": true + }, + "maxTimeAllowed": { + "type": "string", + "nullable": true + }, + "timeLimitAction": { + "type": "string", + "nullable": true + }, + "scormResourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ScormResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.TranscriptFile": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "fileId": { + "type": "integer", + "format": "int32" + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "videoFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.VideoFile" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.VideoFile": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "fileId": { + "type": "integer", + "format": "int32" + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "captionsFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "captionsFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.CaptionsFile" + }, + "transcriptFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "transcriptFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.TranscriptFile" + }, + "status": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.VideoFileStatus" + }, + "processingErrorMessage": { + "type": "string", + "nullable": true + }, + "azureAssetOutputFilePath": { + "type": "string", + "nullable": true + }, + "locatorUri": { + "type": "string", + "nullable": true + }, + "encodeJobName": { + "type": "string", + "nullable": true + }, + "durationInMilliseconds": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.VideoResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "videoFileId": { + "type": "integer", + "format": "int32" + }, + "transcriptFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "closedCaptionsFileId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "resourceAzureMediaAssetId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "durationInMilliseconds": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "transcriptFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "closedCaptionsFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "resourceAzureMediaAsset": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceAzureMediaAsset" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.WebLinkResourceVersion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "webLinkUrl": { + "type": "string", + "nullable": true + }, + "displayText": { + "type": "string", + "nullable": true + }, + "resourceVersion": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Resource.WholeSlideImageFile": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "fileId": { + "type": "integer", + "format": "int32" + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.File" + }, + "status": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.WholeSlideImageFileStatus" + }, + "processingErrorMessage": { + "type": "string", + "nullable": true + }, + "width": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "height": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "deepZoomTileSize": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "deepZoomOverlap": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "layers": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Role": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "permissionRole": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.PermissionRole" + }, + "nullable": true + }, + "roleUserGroup": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.RoleUserGroup" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.RoleUserGroup": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "roleId": { + "type": "integer", + "format": "int32" + }, + "userGroupId": { + "type": "integer", + "format": "int32" + }, + "scopeId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "role": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Role" + }, + "userGroup": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserGroup" + }, + "scope": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Scope" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.Scope": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "catalogueNodeId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "scopeType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ScopeTypeEnum" + }, + "catalogueNode": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.Node" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "userName": { + "type": "string", + "nullable": true + }, + "versionStartTime": { + "type": "string", + "format": "date-time" + }, + "versionEndTime": { + "type": "string", + "format": "date-time" + }, + "userUserGroup": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserUserGroup" + }, + "nullable": true + }, + "resourceVersion": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersion" + }, + "nullable": true + }, + "hierarchyEdit": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Hierarchy.HierarchyEdit" + }, + "nullable": true + }, + "resourceVersionEvent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Resource.ResourceVersionEvent" + }, + "nullable": true + }, + "assignedRoles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Role" + }, + "nullable": true, + "readOnly": true + }, + "token": { + "type": "string", + "nullable": true + }, + "createdNotifications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + }, + "nullable": true + }, + "amendedNotifications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + }, + "nullable": true + }, + "userNotificationAmendUser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + }, + "nullable": true + }, + "userNotificationCreateUser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + }, + "nullable": true + }, + "userNotificationUser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserNotification" + }, + "nullable": true + }, + "userProvider": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserProvider" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.UserGroup": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "roleUserGroup": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.RoleUserGroup" + }, + "nullable": true + }, + "userGroupAttribute": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserGroupAttribute" + }, + "nullable": true + }, + "userUserGroup": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserUserGroup" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.UserGroupAttribute": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "userGroupId": { + "type": "integer", + "format": "int32" + }, + "attributeId": { + "type": "integer", + "format": "int32" + }, + "scopeId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "userGroup": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserGroup" + }, + "attribute": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Attribute" + }, + "scope": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Scope" + }, + "intValue": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "textValue": { + "type": "string", + "nullable": true + }, + "booleanValue": { + "type": "boolean", + "nullable": true + }, + "dateValue": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.UserNotification": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "notificationId": { + "type": "integer", + "format": "int32" + }, + "dismissed": { + "type": "boolean" + }, + "userId": { + "type": "integer", + "format": "int32" + }, + "readOnDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "user": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + }, + "notification": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Notification" + }, + "createUser": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + }, + "amendUser": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "userName": { + "type": "string", + "nullable": true + }, + "emailAddress": { + "type": "string", + "nullable": true + }, + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + }, + "active": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.UserProvider": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "userId": { + "type": "integer", + "format": "int32" + }, + "providerId": { + "type": "integer", + "format": "int32" + }, + "user": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + }, + "provider": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Provider" + }, + "removalDate": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Entities.UserUserGroup": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "deleted": { + "type": "boolean" + }, + "createUserId": { + "type": "integer", + "format": "int32" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "amendUserId": { + "type": "integer", + "format": "int32" + }, + "amendDate": { + "type": "string", + "format": "date-time" + }, + "userId": { + "type": "integer", + "format": "int32" + }, + "userGroupId": { + "type": "integer", + "format": "int32" + }, + "user": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.User" + }, + "userGroup": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.UserGroup" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Enums.AssessmentTypeEnum": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.AttachedFileTypeEnum": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.AttributeTypeEnum": { + "enum": [ + 1, + 2, + 3, + 4 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.CatalogueAccessRequestStatus": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.CatalogueOrder": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.EsrLinkType": { + "enum": [ + 1, + 2, + 3, + 4 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.EventTypeEnum": { + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.HierarchyEditDetailOperationEnum": { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.HierarchyEditDetailTypeEnum": { + "enum": [ + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.HierarchyEditStatusEnum": { + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.ImageAnnotationColourEnum": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.ImageAnnotationMarkTypeEnum": { + "enum": [ + 0 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.MediaType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.NodeTypeEnum": { + "enum": [ + 1, + 2, + 3, + 4 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.NotificationPriorityEnum": { + "enum": [ + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.NotificationTypeEnum": { + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.PublisherActionEnum": { + "enum": [ + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.QuestionAnswerStatus": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.QuestionTypeEnum": { + "enum": [ + 1, + 2, + 3, + 4 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.ResourceAccessibilityEnum": { + "enum": [ + 1, + 2, + 3, + 4 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.ResourceTypeEnum": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.ResourceTypeValidationRuleEnum": { + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.ResourceVersionEventTypeEnum": { + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.ScopeTypeEnum": { + "enum": [ + 1 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Enums.VersionStatusEnum": { + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Hierarchy.FolderEditViewModel": { + "type": "object", + "properties": { + "hierarchyEditId": { + "type": "integer", + "format": "int32" + }, + "hierarchyEditDetailId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "nodeVersionId": { + "type": "integer", + "format": "int32" + }, + "parentNodeId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Hierarchy.HierarchyEditMoveResourceViewModel": { + "type": "object", + "properties": { + "hierarchyEditDetailId": { + "type": "integer", + "format": "int32" + }, + "moveToHierarchyEditDetailId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Hierarchy.MoveNodeViewModel": { + "type": "object", + "properties": { + "hierarchyEditDetailId": { + "type": "integer", + "format": "int32" + }, + "moveToHierarchyEditDetailId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Hierarchy.NodePathNodeViewModel": { + "type": "object", + "properties": { + "nodeId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Hierarchy.NodeViewModel": { + "type": "object", + "properties": { + "nodeId": { + "type": "integer", + "format": "int32" + }, + "nodePathId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Hierarchy.PublishHierarchyEditViewModel": { + "type": "object", + "properties": { + "hierarchyEditId": { + "type": "integer", + "format": "int32" + }, + "isMajorRevision": { + "type": "boolean" + }, + "notes": { + "type": "string", + "nullable": true + }, + "userId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Provider.ProviderViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "logo": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.AssessmentViewModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "assessmentType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.AssessmentTypeEnum" + }, + "assessmentContent": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockCollectionViewModel" + }, + "maximumAttempts": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "passMark": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "answerInOrder": { + "type": "boolean" + }, + "endGuidance": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockCollectionViewModel" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.Annotations.AnnotationMarkFreehandViewModel": { + "type": "object", + "properties": { + "pathCoordinates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.Annotations.PathCoordinatesViewModel" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.Annotations.ImageAnnotationMarkViewModel": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ImageAnnotationMarkTypeEnum" + }, + "freehandMarkShapeData": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.Annotations.AnnotationMarkFreehandViewModel" + }, + "markLabel": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.Annotations.ImageAnnotationViewModel": { + "type": "object", + "properties": { + "order": { + "type": "integer", + "format": "int32" + }, + "label": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "pinXCoordinate": { + "type": "number", + "format": "double" + }, + "pinYCoordinate": { + "type": "number", + "format": "double" + }, + "colour": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ImageAnnotationColourEnum" + }, + "imageAnnotationMarks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.Annotations.ImageAnnotationMarkViewModel" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.Annotations.PathCoordinatesViewModel": { + "type": "object", + "properties": { + "x": { + "type": "integer", + "format": "int32" + }, + "y": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.AttachmentViewModel": { + "type": "object", + "properties": { + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.FileViewModel" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.BlockCollectionViewModel": { + "type": "object", + "properties": { + "blocks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockViewModel" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.BlockType": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Resource.Blocks.BlockViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "order": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "blockType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockType" + }, + "textBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.TextBlockViewModel" + }, + "wholeSlideImageBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.WholeSlideImageBlockViewModel" + }, + "mediaBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.MediaBlockViewModel" + }, + "questionBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.QuestionBlockViewModel" + }, + "imageCarouselBlock": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.ImageCarouselBlockViewModel" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.ImageCarouselBlockViewModel": { + "type": "object", + "properties": { + "block": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockViewModel" + }, + "imageBlockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockCollectionViewModel" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.MediaBlockViewModel": { + "type": "object", + "properties": { + "mediaType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.MediaType" + }, + "attachment": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.AttachmentViewModel" + }, + "image": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.MediaImageViewModel" + }, + "video": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.MediaVideoViewModel" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.MediaImageViewModel": { + "type": "object", + "properties": { + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.FileViewModel" + }, + "altText": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.Annotations.ImageAnnotationViewModel" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.MediaVideoViewModel": { + "type": "object", + "properties": { + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.FileViewModel" + }, + "videoFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.VideoFileViewModel" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.QuestionBlockViewModel": { + "type": "object", + "properties": { + "questionBlockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockCollectionViewModel" + }, + "feedbackBlockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockCollectionViewModel" + }, + "questionType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.QuestionTypeEnum" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.QuestionAnswerViewModel" + }, + "nullable": true + }, + "allowReveal": { + "type": "boolean", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.TextBlockViewModel": { + "type": "object", + "properties": { + "content": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.WholeSlideImageBlockItemViewModel": { + "type": "object", + "properties": { + "order": { + "type": "integer", + "format": "int32" + }, + "wholeSlideImage": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.WholeSlideImageViewModel" + }, + "placeholderText": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.WholeSlideImageBlockViewModel": { + "type": "object", + "properties": { + "wholeSlideImageBlockItems": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.WholeSlideImageBlockItemViewModel" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Blocks.WholeSlideImageViewModel": { + "type": "object", + "properties": { + "title": { + "type": "string", + "nullable": true + }, + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.FileViewModel" + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.Annotations.ImageAnnotationViewModel" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.CaseViewModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "blockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockCollectionViewModel" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.AudioUpdateRequestViewModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "durationInMilliseconds": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.AuthorDeleteRequestModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "authorId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.DuplicateBlocksRequestModel": { + "type": "object", + "properties": { + "sourceResourceId": { + "type": "integer", + "format": "int32" + }, + "blockIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "destinationResourceId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "assessmentType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.AssessmentTypeEnum" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.DuplicateResourceRequestModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "resourceCatalogueId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.FileChunkDetailDeleteRequestModel": { + "type": "object", + "properties": { + "fileChunkDetailId": { + "type": "integer", + "format": "int32" + }, + "amendUserId": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.FileChunkDetailViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "fileName": { + "type": "string", + "nullable": true + }, + "chunkCount": { + "type": "integer", + "format": "int32" + }, + "filePath": { + "type": "string", + "nullable": true + }, + "fileSizeKb": { + "type": "integer", + "format": "int32" + }, + "createUserId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.FileCreateRequestViewModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "fileTypeId": { + "type": "integer", + "format": "int32" + }, + "resourceType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceTypeEnum" + }, + "fileName": { + "type": "string", + "nullable": true + }, + "filePath": { + "type": "string", + "nullable": true + }, + "fileSize": { + "type": "integer", + "format": "int32" + }, + "replacedFileId": { + "type": "integer", + "format": "int32" + }, + "attachedFileType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.AttachedFileTypeEnum" + }, + "fileChunkDetailId": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.FileDeleteRequestModel": { + "type": "object", + "properties": { + "resourceType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceTypeEnum" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "fileId": { + "type": "integer", + "format": "int32" + }, + "attachedFileType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.AttachedFileTypeEnum" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.GenericFileUpdateRequestViewModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "scormAiccContent": { + "type": "boolean" + }, + "authoredYear": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "authoredMonth": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "authoredDayOfMonth": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "esrLinkType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.EsrLinkType" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.HtmlResourceUpdateRequestViewModel": { + "type": "object", + "properties": { + "useDefaultPopupWindowSize": { + "type": "boolean" + }, + "popupWidth": { + "type": "integer", + "format": "int32" + }, + "popupHeight": { + "type": "integer", + "format": "int32" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "esrLinkType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.EsrLinkType" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.ImageUpdateRequestViewModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "altTag": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.KeywordDeleteRequestModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "keywordId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.ResourceViewModel": { + "type": "object", + "properties": { + "resourceId": { + "type": "integer", + "format": "int32" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "resourceType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.ResourceTypeEnum" + }, + "currentResourceVersionId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "duplicatedFromResourceVersionId": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.ScormUpdateRequestViewModel": { + "type": "object", + "properties": { + "useDefaultPopupWindowSize": { + "type": "boolean" + }, + "popupWidth": { + "type": "integer", + "format": "int32" + }, + "popupHeight": { + "type": "integer", + "format": "int32" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "esrLinkType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.EsrLinkType" + }, + "canDownload": { + "type": "boolean" + }, + "clearSuspendData": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Contribute.VideoUpdateRequestViewModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "durationInMilliseconds": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.FileTypeViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "defaultResourceTypeId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "extension": { + "type": "string", + "nullable": true + }, + "notAllowed": { + "type": "boolean" + }, + "icon": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.FileViewModel": { + "type": "object", + "properties": { + "fileId": { + "type": "integer", + "format": "int32" + }, + "fileName": { + "type": "string", + "nullable": true + }, + "fileSizeKb": { + "type": "integer", + "format": "int64" + }, + "filePath": { + "type": "string", + "nullable": true + }, + "fileType": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.FileTypeViewModel" + }, + "fileTypeId": { + "type": "integer", + "format": "int32" + }, + "fileChunkDetailId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "partialFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.PartialFileViewModel" + }, + "videoFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.VideoFileViewModel" + }, + "wholeSlideImageFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.WholeSlideImageFileViewModel" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Files.CaptionsFileViewModel": { + "type": "object", + "properties": { + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.FileViewModel" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Files.PartialFilePostProcessingOptions": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Resource.Files.PartialFileViewModel": { + "type": "object", + "properties": { + "fileId": { + "type": "integer", + "format": "int32" + }, + "fileName": { + "type": "string", + "nullable": true + }, + "filePath": { + "type": "string", + "nullable": true + }, + "postProcessingOptions": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.PartialFilePostProcessingOptions" + }, + "totalFileSize": { + "type": "integer", + "format": "int64" + }, + "uploadedFileSize": { + "type": "integer", + "format": "int64" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Files.TranscriptFileViewModel": { + "type": "object", + "properties": { + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.FileViewModel" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Files.VideoFileStatus": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Resource.Files.VideoFileViewModel": { + "type": "object", + "properties": { + "file": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.FileViewModel" + }, + "captionsFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.CaptionsFileViewModel" + }, + "transcriptFile": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.TranscriptFileViewModel" + }, + "status": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.VideoFileStatus" + }, + "processingErrorMessage": { + "type": "string", + "nullable": true + }, + "azureAssetOutputFilePath": { + "type": "string", + "nullable": true + }, + "locatorUri": { + "type": "string", + "nullable": true + }, + "encodeJobName": { + "type": "string", + "nullable": true + }, + "durationInMilliseconds": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.Files.WholeSlideImageFileStatus": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "format": "int32" + }, + "LearningHub.Nhs.Models.Resource.Files.WholeSlideImageFileViewModel": { + "type": "object", + "properties": { + "status": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Files.WholeSlideImageFileStatus" + }, + "processingErrorMessage": { + "type": "string", + "nullable": true + }, + "width": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "height": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "deepZoomTileSize": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "deepZoomOverlap": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "layers": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.PublishViewModel": { + "type": "object", + "properties": { + "publisherAction": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.PublisherActionEnum" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "isMajorRevision": { + "type": "boolean" + }, + "notes": { + "type": "string", + "nullable": true + }, + "userId": { + "type": "integer", + "format": "int32" + }, + "publicationDate": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.QuestionAnswerViewModel": { + "type": "object", + "properties": { + "order": { + "type": "integer", + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "status": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.QuestionAnswerStatus" + }, + "blockCollection": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Resource.Blocks.BlockCollectionViewModel" + }, + "imageAnnotationOrder": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.ResourceAuthorViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "authorUserId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "authorName": { + "type": "string", + "nullable": true + }, + "organisation": { + "type": "string", + "nullable": true + }, + "role": { + "type": "string", + "nullable": true + }, + "isContributor": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.ResourceKeywordViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "keyword": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.ResourceVersionFlagViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "details": { + "type": "string", + "nullable": true + }, + "isActive": { + "type": "boolean" + }, + "flaggedByUserId": { + "type": "integer", + "format": "int32" + }, + "flaggedByName": { + "type": "string", + "nullable": true + }, + "resolution": { + "type": "string", + "nullable": true } - } - } - }, - "/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." + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.UnpublishViewModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" + }, + "setFlag": { + "type": "boolean" + }, + "details": { + "type": "string", + "nullable": true } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceReferenceWithResourceDetailsViewModel" - } - } - } - } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Resource.WebLinkViewModel": { + "type": "object", + "properties": { + "resourceVersionId": { + "type": "integer", + "format": "int32" }, - "400": { - "description": "Bad request: The activityStatusId provided is not valid." + "displayText": { + "type": "string", + "nullable": true }, - "401": { - "description": "Unauthorized: User Id required." + "url": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.Search.AllCatalogueSearchRequestModel": { + "type": "object", + "properties": { + "searchId": { + "type": "integer", + "format": "int32" }, - "403": { - "description": "Forbidden: The activityStatusId is not defined within ActivityStatusEnum or is in the list of activityStatusIdsNotInUseInDB." + "searchText": { + "type": "string", + "nullable": true }, - "500": { - "description": "Internal server error: An unexpected error occurred while processing the request." + "totalNumberOfHits": { + "type": "integer", + "format": "int32" + }, + "pageIndex": { + "type": "integer", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "format": "int32" + }, + "eventTypeEnum": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Enums.EventTypeEnum" + }, + "groupId": { + "type": "string", + "format": "uuid" } - } - } - }, - "/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" - } - } - } - } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.User.UserCreateViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" }, - "401": { - "description": "Unauthorized: User Id required." + "userName": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.Models.User.UserUpdateViewModel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" }, - "500": { - "description": "Internal server error: An unexpected error occurred while processing the request." + "userName": { + "type": "string", + "nullable": true } - } - } - } - }, - "components": { - "schemas": { - "BulkResourceReferenceViewModel": { + }, + "additionalProperties": false + }, + "LearningHub.Nhs.OpenApi.Models.ViewModels.BulkCatalogueViewModel": { + "type": "object", + "properties": { + "catalogues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.OpenApi.Models.ViewModels.CatalogueViewModel" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "LearningHub.Nhs.OpenApi.Models.ViewModels.BulkResourceReferenceViewModel": { "type": "object", "properties": { "resourceReferences": { "type": "array", "items": { - "$ref": "#/components/schemas/ResourceReferenceWithResourceDetailsViewModel" + "$ref": "#/components/schemas/LearningHub.Nhs.OpenApi.Models.ViewModels.ResourceReferenceWithResourceDetailsViewModel" }, - "nullable": false, - "readOnly": true + "nullable": true }, "unmatchedResourceReferenceIds": { "type": "array", @@ -391,227 +13973,168 @@ "type": "integer", "format": "int32" }, - "nullable": false, - "readOnly": true, - "description": "A list of Resource Reference IDs included in the request that could not be matched to Learning Hub resources" + "nullable": true } }, "additionalProperties": false }, - "CatalogueViewModel": { + "LearningHub.Nhs.OpenApi.Models.ViewModels.CatalogueViewModel": { "type": "object", "properties": { "id": { "type": "integer", - "format": "int32", - "description": "The unique ID of the catalogue." + "format": "int32" }, "name": { "type": "string", - "nullable": false, - "description": "The name of the Learning Hub catalogue." + "nullable": true }, "isRestricted": { - "type": "boolean", - "description": "Indicates whether this is a restricted catalogue. Restricted catalogues are only available to specific Learning Hub users." - } - }, - "additionalProperties": false - }, - "BulkCatalogueViewModel": { - "type": "object", - "properties": { - "catalogues": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CatalogueViewModel" - }, - "nullable": false, - "readOnly": true + "type": "boolean" } }, "additionalProperties": false }, - "ResourceMetadataViewModel": { + "LearningHub.Nhs.OpenApi.Models.ViewModels.ResourceMetadataViewModel": { "type": "object", "properties": { "resourceId": { "type": "integer", - "format": "int32", - "description": "Unique identifier for the resource. Because a resource can exist in multiple catalogues, Resource Reference ID (which uniquely identifies a resource within a catalogue) should be used as an identifier in preference and is, therefore, used when calling the Resource and Resource/Bulk endpoints." + "format": "int32" }, "title": { "type": "string", - "nullable": false, - "description": "The name of the learning resource." + "nullable": true }, "description": { "type": "string", - "nullable": false, - "description": "Describes the learning resource. This is an html formatted string." + "nullable": true }, "references": { "type": "array", "items": { - "$ref": "#/components/schemas/ResourceReferenceViewModel" + "$ref": "#/components/schemas/LearningHub.Nhs.OpenApi.Models.ViewModels.ResourceReferenceViewModel" }, - "nullable": false + "nullable": true }, "resourceType": { "type": "string", - "nullable": false, - "description": "The type of learning resource, a valid Learning Hub resource type e.g. web link, article, video" + "nullable": true + }, + "majorVersion": { + "type": "integer", + "format": "int32", + "nullable": true }, "rating": { "type": "number", - "format": "double", - "description": "Users are able to rate Learning Hub resources on a scale of 1-5. This is mean Learning Hub user rating for this resource. It will be a number between 0 and 5." + "format": "double" + }, + "userSummaryActivityStatuses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.MajorVersionIdActivityStatusDescription" + }, + "nullable": true } }, "additionalProperties": false }, - "ResourceReferenceViewModel": { + "LearningHub.Nhs.OpenApi.Models.ViewModels.ResourceReferenceViewModel": { "type": "object", "properties": { "refId": { "type": "integer", - "format": "int32", - "description": "The Resource Reference ID. Resource Reference ID uniquely identifies a resource within a single Learning Hub catalogue (resources can be in multiple catalogues). This is used to call the Resource and Resource/Bulk endpoints." + "format": "int32" }, "catalogue": { - "$ref": "#/components/schemas/CatalogueViewModel" + "$ref": "#/components/schemas/LearningHub.Nhs.OpenApi.Models.ViewModels.CatalogueViewModel" }, "link": { "type": "string", - "nullable": false, - "description": "URL to access the learning resource within its catalogue on the Learning Hub." + "nullable": true } }, "additionalProperties": false }, - "ResourceReferenceWithResourceDetailsViewModel": { + "LearningHub.Nhs.OpenApi.Models.ViewModels.ResourceReferenceWithResourceDetailsViewModel": { "type": "object", "properties": { "resourceId": { "type": "integer", - "format": "int32", - "readOnly": true, - "description": "Unique identifier for the resource. Because a resource can exist in multiple catalogues, Resource Reference ID (which uniquely identifies a resource within a catalogue) should be used as an identifier in preference and is, therefore, used when calling the Resource and Resource/Bulk endpoints." + "format": "int32" }, "refId": { "type": "integer", - "format": "int32", - "readOnly": true, - "description": "The Resource Reference ID. Resource Reference ID uniquely identifies a resource within a single Learning Hub catalogue (resources can be in multiple catalogues). This is used to call the Resource and Resource/Bulk endpoints." + "format": "int32" }, "title": { "type": "string", - "nullable": false, - "readOnly": true, - "description": "The name of the learning resource." + "nullable": true }, "description": { "type": "string", - "nullable": false, - "readOnly": true, - "description": "Describes the learning resource. This is an html formatted string." + "nullable": true }, "catalogue": { - "$ref": "#/components/schemas/CatalogueViewModel" + "$ref": "#/components/schemas/LearningHub.Nhs.OpenApi.Models.ViewModels.CatalogueViewModel" }, "resourceType": { "type": "string", - "nullable": false, - "readOnly": true, - "description": "The type of learning resource, a valid Learning Hub resource type e.g. web link, article, video" + "nullable": true + }, + "majorVersion": { + "type": "integer", + "format": "int32", + "nullable": true }, "rating": { "type": "number", - "format": "double", - "readOnly": true, - "description": "Users are able to rate Learning Hub resources on a scale of 1-5. This is mean Learning Hub user rating for this resource. It will be a number between 0 and 5." + "format": "double" }, "link": { "type": "string", - "nullable": false, - "readOnly": true, - "description": "URL to access the learning resource within its catalogue on the Learning Hub." + "nullable": true + }, + "userSummaryActivityStatuses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LearningHub.Nhs.Models.Entities.Activity.MajorVersionIdActivityStatusDescription" + }, + "nullable": true } }, "additionalProperties": false }, - "ResourceSearchResultViewModel": { + "LearningHub.Nhs.OpenApi.Models.ViewModels.ResourceSearchResultViewModel": { "type": "object", "properties": { "results": { "type": "array", "items": { - "$ref": "#/components/schemas/ResourceMetadataViewModel" + "$ref": "#/components/schemas/LearningHub.Nhs.OpenApi.Models.ViewModels.ResourceMetadataViewModel" }, - "nullable": false, - "description": "An array of learning resources matching the search criteria specified. The number in the array will not exceed the limit specified in the request." + "nullable": true }, "offset": { "type": "integer", - "format": "int32", - "description": "The number of items skipped before collecting the result set returned." + "format": "int32" }, "totalNumResources": { "type": "integer", - "format": "int32", - "description": "The total number of learning resources matching the search criteria found in the Learning Hub repository" + "format": "int32" } }, "additionalProperties": false }, - "UserBookmarkViewModel": { + "System.Collections.Generic.KeyValuePair`2[System.String,System.Int32]": { "type": "object", "properties": { - "id": { - "type": "integer", - "format": "int32" - }, - "bookmarkTypeId": { - "type": "integer", - "format": "int32" - }, - "parentId": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "userId": { - "type": "integer", - "format": "int32" - }, - "resourceReferenceId": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "resourceTypeId": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "nodeId": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "link": { + "key": { "type": "string", "nullable": true }, - "position": { - "type": "integer", - "format": "int32" - }, - "childrenCount": { + "value": { "type": "integer", "format": "int32" } @@ -620,12 +14143,12 @@ } }, "securitySchemes": { - "ApiKey": { - "type": "apiKey", - "name": "X-API-KEY", - "in": "header", - "description": "Application API key required to authorise all API requests. This must be obtained from the Learning Hub team at NHS England, Technology Enhanced Learning, contact us at [england.tel@nhs.net](england.tel@nhs.net)" - }, + "ApiKey": { + "type": "apiKey", + "name": "X-API-KEY", + "in": "header", + "description": "Application API key required to authorise all API requests. This must be obtained from the Learning Hub team at NHS England, Technology Enhanced Learning, contact us at [england.tel@nhs.net](england.tel@nhs.net)" + }, "OAuth": { "type": "oauth2", "flows": { @@ -648,4 +14171,4 @@ "OAuth": [] } ] -} +} \ No newline at end of file From 067cd6d407512c7c921a1c93c105c9a6738bf46c Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 14 Nov 2025 14:03:42 +0000 Subject: [PATCH 002/106] Azure AI search implementation, updated OpenAPI --- .../Configuration/AzureSearchConfig.cs | 48 + .../LearningHub.Nhs.OpenApi.Models.csproj | 1 + .../AzureSearch/CacheableFacetResult.cs | 19 + .../AzureSearch/SearchDocument.cs | 135 +++ .../Services/ISearchService.cs | 10 +- .../Helpers/AzureSearchClientFactory.cs | 47 + .../Helpers/Search/AzureSearchFacetHelper.cs | 144 +++ .../Helpers/Search/LuceneQueryBuilder.cs | 44 + .../Helpers/Search/SearchFilterBuilder.cs | 90 ++ .../Helpers/Search/SearchOptionsBuilder.cs | 88 ++ .../LearningHub.Nhs.OpenApi.Services.csproj | 1 + .../AzureSearch/AzureSearchService.cs | 920 ++++++++++++++++++ .../Services/BaseService.cs | 22 +- .../Findwise/NullFindwiseApiFacade.cs | 59 ++ .../Services/Messaging/MessageService.cs | 7 +- .../Services/NotificationTemplateService.cs | 6 +- .../Services/SearchService.cs | 10 +- .../Startup.cs | 31 +- .../Configuration/ConfigurationExtensions.cs | 7 + OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs | 2 +- .../LearningHub.Nhs.OpenApi/appsettings.json | 13 + 21 files changed, 1675 insertions(+), 29 deletions(-) create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/CacheableFacetResult.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/AzureSearchClientFactory.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/AzureSearchFacetHelper.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/LuceneQueryBuilder.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/Findwise/NullFindwiseApiFacade.cs diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs new file mode 100644 index 000000000..109a970c0 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs @@ -0,0 +1,48 @@ +namespace LearningHub.Nhs.OpenApi.Models.Configuration +{ + /// + /// The Azure AI Search configuration settings. + /// + public class AzureSearchConfig + { + /// + /// Gets or sets the Azure Search service endpoint URL. + /// + public string ServiceEndpoint { get; set; } = null!; + + /// + /// Gets or sets the Azure Search admin API key. + /// + public string AdminApiKey { get; set; } = null!; + + /// + /// Gets or sets the Azure Search query API key. + /// + public string QueryApiKey { get; set; } = null!; + + /// + /// Gets or sets the index name. + /// + public string IndexName { get; set; } = null!; + + /// + /// Gets or sets the default item limit for search results. + /// + public int DefaultItemLimitForSearch { get; set; } = 10; + + /// + /// Gets or sets the description length limit. + /// + public int DescriptionLengthLimit { get; set; } = 3000; + + /// + /// Gets or sets the maximum description length. + /// + public int MaximumDescriptionLength { get; set; } = 150; + + /// + /// Gets or sets the suggester name for auto-complete and suggestions. + /// + public string SuggesterName { get; set; } = "test-search-suggester"; + } +} 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 cecf1b22a..9e3b45435 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj @@ -16,6 +16,7 @@ + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/CacheableFacetResult.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/CacheableFacetResult.cs new file mode 100644 index 000000000..d10ab70cb --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/CacheableFacetResult.cs @@ -0,0 +1,19 @@ +namespace LearningHub.Nhs.OpenApi.Models.ServiceModels.AzureSearch +{ + /// + /// A cacheable representation of Azure Search FacetResult. + /// This DTO is used to cache facet data without serialization issues with Azure SDK types. + /// + public class CacheableFacetResult + { + /// + /// Gets or sets the facet value. + /// + public object? Value { get; set; } + + /// + /// Gets or sets the count of documents matching this facet value. + /// + public long Count { get; set; } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs new file mode 100644 index 000000000..e30e34004 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs @@ -0,0 +1,135 @@ +namespace LearningHub.Nhs.OpenApi.Models.ServiceModels.AzureSearch +{ + using System; + using System.Collections.Generic; + using System.Text.Json.Serialization; + using System.Text.RegularExpressions; + using Azure.Search.Documents.Indexes; + + /// + /// Represents a search document for Azure AI Search integration. + /// + public class SearchDocument + { + private string _description = string.Empty; + + /// + /// Gets or sets the unique identifier. + /// + [SearchableField(IsKey = true)] + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the title. + /// + [SearchableField(IsSortable = true)] + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the description. + /// + [SearchableField] + [JsonPropertyName("description")] + public string Description + { + get => _description; + set => _description = StripParagraphTags(value); + } + + /// + /// Gets or sets the resource type. + /// + [SearchableField(IsFilterable = true, IsFacetable = true)] + [JsonPropertyName("resource_collection")] + public string ResourceCollection { get; set; } = string.Empty; + + /// + /// Gets or sets the manual tag JSON. + /// + [SearchableField(IsFilterable = true, IsFacetable = true)] + [JsonPropertyName("manual_tag")] + public string ManualTagJson { get; set; } = string.Empty; + + /// + /// Gets or sets the manual tags. + /// + [SearchableField(IsFilterable = true, IsFacetable = true)] + [JsonPropertyName("manualTags")] + public List ManualTags { get; set; } = new List(); + + /// + /// Gets or sets the content type. + /// + [SearchableField(IsFilterable = true, IsFacetable = true)] + [JsonPropertyName("resource_type")] + public string? ResourceType { get; set; } = string.Empty; + + /// + /// Gets or sets the date authored. + /// + [SimpleField(IsFilterable = true, IsSortable = true)] + [JsonPropertyName("date_authored")] + public DateTime? DateAuthored { get; set; } + + /// + /// Gets or sets the rating. + /// + [SimpleField(IsFilterable = true, IsSortable = true)] + [JsonPropertyName("rating")] + public double? Rating { get; set; } + + /// + /// Gets or sets the provider IDs. + /// + [SearchableField] + [JsonPropertyName("provider_ids")] + public string ProviderIds { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether this is statutory mandatory. + /// + [SimpleField(IsFilterable = true, IsFacetable = true)] + [JsonPropertyName("statutory_mandatory")] + public bool? StatutoryMandatory { get; set; } + + /// + /// Gets or sets the author. + /// + [SearchableField] + [JsonPropertyName("author")] + public string Author { get; set; } = string.Empty; + + /// + /// Strips paragraph tags from input string. + /// + /// The input string. + /// The cleaned string. + private static string StripParagraphTags(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return string.Empty; + + return Regex.Replace(input, @"<\/?p[^>]*>", string.Empty, RegexOptions.IgnoreCase); + } + + /// + /// Parses the ManualTagJson into ManualTags list. + /// + public void ParseManualTags() + { + if (!string.IsNullOrEmpty(ManualTagJson)) + { + try + { + ManualTags = System.Text.Json.JsonSerializer.Deserialize>(ManualTagJson) ?? new List(); + } + catch + { + ManualTags = new List(); + } + } + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs index 194264b15..2a238edf5 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs @@ -1,5 +1,6 @@ namespace LearningHub.Nhs.OpenApi.Services.Interface.Services { + using System.Threading; using System.Threading.Tasks; using LearningHub.Nhs.Models.Search; using LearningHub.Nhs.Models.Search.SearchClick; @@ -17,8 +18,9 @@ public interface ISearchService /// /// The catalog search request model. /// The user id. + /// Cancellation token. /// The . - Task GetSearchResultAsync(SearchRequestModel searchRequestModel, int userId); + Task GetSearchResultAsync(SearchRequestModel searchRequestModel, int userId, CancellationToken cancellationToken = default); /// /// The Get Catalogue Search Result Async method. @@ -29,10 +31,11 @@ public interface ISearchService /// /// The user id. /// + /// Cancellation token. /// /// The . /// - Task GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId); + Task GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId, CancellationToken cancellationToken = default); /// /// The create resource search action async. @@ -152,8 +155,9 @@ public interface ISearchService /// The Get Auto suggestion Results Async method. /// /// The term. + /// Cancellation token. /// The . - Task GetAutoSuggestionResultsAsync(string term); + Task GetAutoSuggestionResultsAsync(string term, CancellationToken cancellationToken = default); /// /// The Send AutoSuggestion Event Async. diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/AzureSearchClientFactory.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/AzureSearchClientFactory.cs new file mode 100644 index 000000000..ad0faee6c --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/AzureSearchClientFactory.cs @@ -0,0 +1,47 @@ +namespace LearningHub.Nhs.OpenApi.Services.Helpers +{ + using System; + using Azure; + using Azure.Search.Documents; + using LearningHub.Nhs.OpenApi.Models.Configuration; + + /// + /// Factory for creating Azure Search clients. + /// + public static class AzureSearchClientFactory + { + /// + /// Creates a SearchClient for querying the Azure Search index. + /// + /// The Azure Search configuration. + /// A configured SearchClient instance. + public static SearchClient CreateQueryClient(AzureSearchConfig config) + { + if (config == null) + throw new ArgumentNullException(nameof(config)); + + var credential = new AzureKeyCredential(config.QueryApiKey); + return new SearchClient( + new Uri(config.ServiceEndpoint), + config.IndexName, + credential); + } + + /// + /// Creates a SearchClient with admin credentials for indexing operations. + /// + /// The Azure Search configuration. + /// A configured SearchClient instance with admin credentials. + public static SearchClient CreateAdminClient(AzureSearchConfig config) + { + if (config == null) + throw new ArgumentNullException(nameof(config)); + + var adminCredential = new AzureKeyCredential(config.AdminApiKey); + return new SearchClient( + new Uri(config.ServiceEndpoint), + config.IndexName, + adminCredential); + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/AzureSearchFacetHelper.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/AzureSearchFacetHelper.cs new file mode 100644 index 000000000..6818b0ecb --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/AzureSearchFacetHelper.cs @@ -0,0 +1,144 @@ +namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search +{ + using Azure.Search.Documents.Models; + using LearningHub.Nhs.Models.Search; + using LearningHub.Nhs.OpenApi.Models.ServiceModels.AzureSearch; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + /// + /// Helper class for Azure Search facet operations including conversion and caching. + /// + public static class AzureSearchFacetHelper + { + private static readonly ConstructorInfo FacetResultCtor = + typeof(FacetResult) + .GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic) + .FirstOrDefault(c => + { + var parameters = c.GetParameters(); + return parameters.Length == 2 && + parameters[0].ParameterType == typeof(long?) && + typeof(IReadOnlyDictionary).IsAssignableFrom(parameters[1].ParameterType); + }); + + /// + /// Converts Azure Search FacetResult dictionary to cacheable DTO. + /// + /// The facets from Azure Search. + /// A cacheable dictionary of facets. + public static IDictionary> ConvertToCacheable( + IDictionary>? facets) + { + if (facets == null) + return new Dictionary>(); + + return facets.ToDictionary( + kvp => kvp.Key, + kvp => (IList)kvp.Value + .Select(f => new CacheableFacetResult + { + Value = f.Value, + Count = f.Count ?? 0 + }) + .ToList()); + } + + /// + /// Converts cacheable DTO back to Azure Search FacetResult dictionary. + /// + /// The cached facets. + /// A dictionary of FacetResult. + public static IDictionary> ConvertFromCacheable( + IDictionary>? cacheableFacets) + { + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + if (cacheableFacets == null || FacetResultCtor == null) + return result; + + foreach (var kvp in cacheableFacets) + { + var facetResults = new List(); + + foreach (var cf in kvp.Value) + { + // Build the additionalProperties dictionary just like Azure returns in JSON + var additionalProps = new Dictionary + { + ["value"] = cf.Value ?? string.Empty + }; + + // Use the internal constructor via reflection + var facet = (FacetResult)FacetResultCtor.Invoke(new object?[] { cf.Count, additionalProps }); + + facetResults.Add(facet); + } + + result[kvp.Key] = facetResults; + } + + return result; + } + + + /// + /// Merges filtered and unfiltered facets to maintain visibility of all filter options. + /// + /// Facets from the filtered search. + /// Facets from the unfiltered search. + /// The currently applied filters. + /// An array of merged facets. + public static Facet[] MergeFacets( + IDictionary> filteredFacets, + IDictionary> unfilteredFacets, + Dictionary> appliedFilters) + { + if (unfilteredFacets == null || !unfilteredFacets.Any()) + { + return Array.Empty(); + } + + var facets = new Facet[unfilteredFacets.Count]; + var index = 0; + + foreach (var facetGroup in unfilteredFacets) + { + var facetKey = facetGroup.Key; + var hasAppliedFilter = appliedFilters?.ContainsKey(facetKey) == true; + var appliedValues = hasAppliedFilter ? appliedFilters[facetKey] : new List(); + + // Get filtered facet values if available + var filteredFacetValues = filteredFacets?.ContainsKey(facetKey) == true + ? filteredFacets[facetKey].ToDictionary(f => f.Value?.ToString()?.ToLower() ?? "", f => (int)f.Count) + : new Dictionary(); + + facets[index++] = new Facet + { + Id = facetKey, + Filters = facetGroup.Value.Select(f => + { + var displayName = f.Value?.ToString()?.ToLower() ?? ""; + var isSelected = appliedValues.Any(av => av.Equals(f.Value?.ToString(), StringComparison.OrdinalIgnoreCase)); + + // Use filtered count if available and filter is not selected, otherwise use unfiltered count + var count = !isSelected && filteredFacetValues.ContainsKey(displayName) + ? filteredFacetValues[displayName] + : (int)f.Count; + + return new Filter + { + DisplayName = displayName, + Count = count, + Selected = isSelected + }; + }).ToArray() + }; + } + + return facets; + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/LuceneQueryBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/LuceneQueryBuilder.cs new file mode 100644 index 000000000..529cbbb18 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/LuceneQueryBuilder.cs @@ -0,0 +1,44 @@ +namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search +{ + using System; + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Helper class for building Lucene queries for Azure Search. + /// + public static class LuceneQueryBuilder + { + /// + /// Builds a Lucene query from the search text. + /// + /// The search text. + /// The Lucene query string. + public static string BuildLuceneQuery(string? searchText) + { + if (string.IsNullOrWhiteSpace(searchText)) + return "*"; + + var tokens = searchText + .Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Select(EscapeLuceneSpecialCharacters) + .Where(t => !string.IsNullOrWhiteSpace(t)); + + return string.Join(" AND ", tokens); + } + + /// + /// Escapes Lucene special characters in the input string. + /// + /// The input string. + /// The escaped string. + public static string EscapeLuceneSpecialCharacters(string? input) + { + if (string.IsNullOrEmpty(input)) + return input ?? string.Empty; + + var pattern = @"([+\-!(){}[\]^\""?~*:\\/])"; + return Regex.Replace(input, pattern, "\\$1"); + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs new file mode 100644 index 000000000..f1709c8db --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs @@ -0,0 +1,90 @@ +namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Web; + + /// + /// Helper class for building Azure Search filter expressions. + /// + public static class SearchFilterBuilder + { + /// + /// Builds a filter expression from a dictionary of filters. + /// + /// The filters to apply. + /// The filter expression string. + public static string BuildFilterExpression(Dictionary>? filters) + { + if (filters == null || !filters.Any()) + return string.Empty; + + var filterExpressions = new List(); + + foreach (var filter in filters) + { + if (filter.Value?.Any() == true) + { + var values = string.Join(",", filter.Value); + filterExpressions.Add($"search.in({filter.Key}, '{values}')"); + } + } + + return filterExpressions.Any() ? string.Join(" and ", filterExpressions) : string.Empty; + } + + /// + /// Parses filter parameters from a query string. + /// + /// The query string to parse. + /// A dictionary of filter names and their values. + public static Dictionary> ParseFiltersFromQuery(string? queryString) + { + var filters = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + if (string.IsNullOrWhiteSpace(queryString)) + return filters; + + // Remove '?' if present + if (queryString.StartsWith("?")) + queryString = queryString.Substring(1); + + // Parse using HttpUtility + var parsed = HttpUtility.ParseQueryString(queryString); + + foreach (string? key in parsed.AllKeys) + { + if (key == null) + continue; // skip null keys + + var values = parsed.GetValues(key); + if (values != null) + { + // Add to dictionary (support multiple values per key) + if (!filters.ContainsKey(key)) + filters[key] = new List(); + + filters[key].AddRange(values); + } + } + + return filters; + } + + /// + /// Normalizes resource type filter values by capitalizing the first letter. + /// + /// The filters dictionary to normalize. + public static void NormalizeResourceTypeFilters(Dictionary>? filters) + { + if (filters == null || !filters.ContainsKey("resource_type")) + return; + + filters["resource_type"] = filters["resource_type"] + .Where(v => !string.IsNullOrEmpty(v)) + .Select(v => char.ToUpper(v[0]) + v.Substring(1)) + .ToList(); + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs new file mode 100644 index 000000000..3e36710e2 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs @@ -0,0 +1,88 @@ +namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Azure.Search.Documents; + using Azure.Search.Documents.Models; + + /// + /// Helper class for building Azure Search options. + /// + public static class SearchOptionsBuilder + { + /// + /// Builds search options for Azure Search queries. + /// + /// The type of search query. + /// The number of results to skip. + /// The number of results to return. + /// The filters to apply. + /// The sort to apply. + /// Whether to include facets. + /// The configured search options. + public static SearchOptions BuildSearchOptions( + SearchQueryType searchQueryType, + int offset, + int pageSize, + Dictionary>? filters, + Dictionary? sortBy, + bool includeFacets) + { + var searchOptions = new SearchOptions + { + Skip = offset, + Size = pageSize, + IncludeTotalCount = true, + ScoringProfile = "boostExactTitle" + }; + + string? columns = sortBy?.Keys.FirstOrDefault(); + string? directions = sortBy?.Values.FirstOrDefault(); + string sortDirection = "asc"; + + string sortColumn = columns ?? "relevance"; + if (directions?.StartsWith("desc", StringComparison.OrdinalIgnoreCase) ?? false) + { + sortDirection = "desc"; + } + + // Configure query type + if (searchQueryType == SearchQueryType.Semantic) + { + searchOptions.QueryType = SearchQueryType.Semantic; + searchOptions.SemanticSearch = new SemanticSearchOptions + { + SemanticConfigurationName = "default" + }; + } + else if (searchQueryType == SearchQueryType.Simple) + { + searchOptions.QueryType = SearchQueryType.Simple; + searchOptions.SearchMode = SearchMode.Any; + searchOptions.OrderBy.Add($"{sortColumn} {sortDirection}"); + } + else + { + searchOptions.QueryType = SearchQueryType.Full; + searchOptions.OrderBy.Add($"{sortColumn} {sortDirection}"); + } + + // Add facets + if (includeFacets) + { + searchOptions.Facets.Add("resource_type"); + searchOptions.Facets.Add("resource_collection"); + searchOptions.Facets.Add("provider_ids"); + } + + // Apply filters + if (filters?.Any() == true) + { + searchOptions.Filter = SearchFilterBuilder.BuildFilterExpression(filters); + } + + return searchOptions; + } + } +} 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 e2e064738..2c8c04990 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -23,6 +23,7 @@ + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs new file mode 100644 index 000000000..1661bb9ad --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -0,0 +1,920 @@ +namespace LearningHub.Nhs.OpenApi.Services.Services.AzureSearch +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using AutoMapper; + using Azure.Search.Documents; + using Azure.Search.Documents.Models; + using LearningHub.Nhs.Models.Entities.Activity; + using LearningHub.Nhs.Models.Entities.Resource; + using LearningHub.Nhs.Models.Enums; + using LearningHub.Nhs.Models.Search; + using LearningHub.Nhs.Models.Search.SearchClick; + using LearningHub.Nhs.Models.Validation; + using LearningHub.Nhs.Models.ViewModels.Helpers; + using LearningHub.Nhs.OpenApi.Models.Configuration; + using LearningHub.Nhs.OpenApi.Models.ServiceModels.AzureSearch; + using LearningHub.Nhs.OpenApi.Models.ServiceModels.Resource; + using LearningHub.Nhs.OpenApi.Models.ViewModels; + using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; + using LearningHub.Nhs.OpenApi.Services.Helpers; + using LearningHub.Nhs.OpenApi.Services.Helpers.Search; + using LearningHub.Nhs.OpenApi.Services.Interface.Services; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using Newtonsoft.Json; + using Event = LearningHub.Nhs.Models.Entities.Analytics.Event; + + + /// + /// The Azure AI Search service implementation. + /// Provides search functionality with facet caching and parallel query execution. + /// + public class AzureSearchService : ISearchService + { + /// + /// Default cache expiration time in minutes for facet results. + /// + private const int DefaultFacetCacheExpirationMinutes = 5; + private readonly IEventService eventService; + private readonly ILearningHubService learningHubService; + private readonly IResourceRepository resourceRepository; + private readonly ICachingService cachingService; + private readonly SearchClient searchClient; + private readonly ILogger logger; + private readonly AzureSearchConfig azureSearchConfig; + private readonly IMapper mapper; + + /// + /// Initializes a new instance of the class. + /// + /// The learning hub service. + /// The event service. + /// The Azure Search configuration. + /// The resource repository. + /// The caching service. + /// The logger. + /// The mapper. + public AzureSearchService( + ILearningHubService learningHubService, + IEventService eventService, + IOptions azureSearchConfig, + IResourceRepository resourceRepository, + ICachingService cachingService, + ILogger logger, + IMapper mapper) + { + this.learningHubService = learningHubService; + this.eventService = eventService; + this.resourceRepository = resourceRepository; + this.cachingService = cachingService; + this.logger = logger; + this.mapper = mapper; + this.azureSearchConfig = azureSearchConfig.Value; + + this.searchClient = AzureSearchClientFactory.CreateQueryClient(this.azureSearchConfig); + } + + /// + public async Task GetSearchResultAsync(SearchRequestModel searchRequestModel, int userId, CancellationToken cancellationToken = default) + { + var viewmodel = new SearchResultModel(); + + try + { + var searchQueryType = SearchQueryType.Semantic; + var pageSize = searchRequestModel.PageSize; + var offset = searchRequestModel.PageIndex * pageSize; + + // Build base filters + var filters = new Dictionary> + { + { "resource_collection", new List { "Resource" } } + }; + + // Parse and merge additional filters from query string + var parsedFilters = SearchFilterBuilder.ParseFiltersFromQuery(searchRequestModel.FilterText); + foreach (var kvp in parsedFilters) + { + if (filters.ContainsKey(kvp.Key)) + filters[kvp.Key].AddRange(kvp.Value); + else + filters.Add(kvp.Key, kvp.Value); + } + + // Normalize resource_type filter values + SearchFilterBuilder.NormalizeResourceTypeFilters(filters); + + // Build query string + var query = searchQueryType == SearchQueryType.Full + ? LuceneQueryBuilder.BuildLuceneQuery(searchRequestModel.SearchText) + : searchRequestModel.SearchText; + + Dictionary sortBy = new Dictionary() + { + { searchRequestModel.SortColumn, searchRequestModel.SortDirection } + }; + + var searchOptions = SearchOptionsBuilder.BuildSearchOptions(searchQueryType, offset, pageSize, filters, sortBy, true); + SearchResults filteredResponse = await this.searchClient.SearchAsync(query, searchOptions, cancellationToken); + + // Execute filtered search and unfiltered facet query in parallel + var filteredSearchTask1 = ExecuteSearchAsync( + query, + searchQueryType, + offset, + pageSize, + filters, + sortBy, + includeFacets: true, + cancellationToken); + + //var unfilteredFacetsTask = GetUnfilteredFacetsAsync( + // searchRequestModel.SearchText, + // searchQueryType, + // cancellationToken); + + // await Task.WhenAll(filteredSearchTask); + + // var filteredResponse = await filteredSearchTask; + //var unfilteredFacets = await unfilteredFacetsTask; + + var unfilteredFacets = await GetUnfilteredFacetsAsync( + searchRequestModel.SearchText, + filteredResponse.Facets, + cancellationToken); + + // Map documents + var documents = filteredResponse.GetResults() + .Select(result => + { + var doc = result.Document; + doc.ParseManualTags(); + + return new Document + { + Id = doc.Id?.Substring(1), + Title = doc.Title, + Description = doc.Description, + ResourceType = doc.ResourceType?.Contains("SCORM e-learning resource") == true ? "scorm" : doc.ResourceType, + ProviderIds = doc.ProviderIds?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(), + CatalogueIds = new List(1), + Rating = Convert.ToDecimal(doc.Rating), + Author = doc.Author, + Authors = doc.Author?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(a => a.Trim()).ToList(), + AuthoredDate = doc.DateAuthored?.ToString(), + Click = new SearchClickModel { Payload = new SearchClickPayloadModel { HitNumber = 1 }, Url = "binon" } + }; + }) + .ToList(); + + viewmodel.DocumentList = new Documentlist + { + Documents = documents.ToArray() + }; + + // Merge facets from filtered and unfiltered results + viewmodel.Facets = AzureSearchFacetHelper.MergeFacets(filteredResponse.Facets, unfilteredFacets, filters); + + var count = Convert.ToInt32(filteredResponse.TotalCount); + viewmodel.Stats = new Stats + { + TotalHits = count + }; + searchRequestModel.TotalNumberOfHits = viewmodel.Stats.TotalHits; + + return viewmodel; + } + catch (Exception ex) + { + this.logger.LogError(ex, "Azure Search query failed for search text: {SearchText}", searchRequestModel.SearchText); + throw; + } + } + + /// + /// Executes a search query with the specified parameters. + /// + /// The search query text. + /// The type of search query. + /// The number of results to skip. + /// The number of results to return. + /// The filters to apply. + /// The sort to apply. + /// Whether to include facets in the results. + /// Cancellation token. + /// The search results. + private async Task> ExecuteSearchAsync( + string query, + SearchQueryType searchQueryType, + int offset, + int pageSize, + Dictionary> filters, + Dictionary searchBy, + bool includeFacets, + CancellationToken cancellationToken) + { + var searchOptions = BuildSearchOptions(searchQueryType, offset, pageSize, filters, searchBy, includeFacets); + return await this.searchClient.SearchAsync(query, searchOptions, cancellationToken); + } + + /// + /// Builds search options for Azure Search queries. + /// + /// The type of search query. + /// The number of results to skip. + /// The number of results to return. + /// The filters to apply. + /// The sort to apply. + /// Whether to include facets. + /// The configured search options. + private SearchOptions BuildSearchOptions( + SearchQueryType searchQueryType, + int offset, + int pageSize, + Dictionary> filters, + Dictionary sortBy, + bool includeFacets) + { + return SearchOptionsBuilder.BuildSearchOptions( + searchQueryType, + offset, + pageSize, + filters, + sortBy, + includeFacets); + } + + /// + /// Gets unfiltered facets for a search term, using caching. + /// + /// The search text. + /// The facet results. + /// Cancellation token. + /// The unfiltered facet results. + private async Task>> GetUnfilteredFacetsAsync( + string searchText, + IDictionary> facets, + CancellationToken cancellationToken) + { + var cacheKey = $"AllFacets_{searchText?.ToLowerInvariant() ?? "*"}"; + var cacheResponse = await this.cachingService.GetAsync>>(cacheKey); + + if (cacheResponse.ResponseEnum == CacheReadResponseEnum.Found) + { + // Convert cached DTO back to FacetResult dictionary + return AzureSearchFacetHelper.ConvertFromCacheable(cacheResponse.Item); + } + + if (facets != null) + { + // Convert to cacheable DTO before caching + var cacheableFacets = AzureSearchFacetHelper.ConvertToCacheable(facets); + await this.cachingService.SetAsync(cacheKey, cacheableFacets, DefaultFacetCacheExpirationMinutes, slidingExpiration: true); + } + + return facets ?? new Dictionary>(); + } + + /// + public async Task GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId, CancellationToken cancellationToken = default) + { + var viewmodel = new SearchCatalogueResultModel(); + + try + { + var offset = catalogSearchRequestModel.PageIndex * catalogSearchRequestModel.PageSize; + + // Build filters for catalogue search + var filters = new Dictionary> + { + { "resource_collection", new List { "Catalogue" } } + }; + + var searchOptions = new SearchOptions + { + Skip = offset, + Size = catalogSearchRequestModel.PageSize, + IncludeTotalCount = true, + Filter = SearchFilterBuilder.BuildFilterExpression(filters) + }; + + SearchResults response = await this.searchClient.SearchAsync( + catalogSearchRequestModel.SearchText, + searchOptions, + cancellationToken); + + var documentList = new CatalogueDocumentList + { + Documents = response.GetResults() + .Select(result => + { + var doc = result.Document; + doc.ParseManualTags(); + + return new CatalogueDocument + { + Id = doc.Id?.Substring(1), + Name = doc.Title, + Description = doc.Description, + Click = new SearchClickModel { Payload = new SearchClickPayloadModel { HitNumber = 1 }, Url = "binon" } + }; + }) + .ToArray() + }; + + viewmodel.DocumentList = documentList; + viewmodel.Stats = new Stats + { + TotalHits = Convert.ToInt32(response.TotalCount) + }; + catalogSearchRequestModel.TotalNumberOfHits = viewmodel.Stats.TotalHits; + + var remainingItems = catalogSearchRequestModel.TotalNumberOfHits - offset; + var resultsPerPage = remainingItems >= catalogSearchRequestModel.PageSize ? catalogSearchRequestModel.PageSize : remainingItems; + var validationResult = await this.CreateCatalogueSearchTerm(catalogSearchRequestModel, resultsPerPage, userId); + + viewmodel.SearchId = validationResult.CreatedId ?? 0; + + return viewmodel; + } + catch (Exception ex) + { + this.logger.LogError(ex, "Azure Search catalogue query failed for search text: {SearchText}", catalogSearchRequestModel.SearchText); + throw; + } + } + + /// + public async Task CreateResourceSearchActionAsync(SearchActionResourceModel searchActionResourceModel, int userId) + { + var jsonobj = new + { + searchActionResourceModel.SearchText, + searchActionResourceModel.NodePathId, + searchActionResourceModel.ItemIndex, + searchActionResourceModel.NumberOfHits, + searchActionResourceModel.TotalNumberOfHits, + searchActionResourceModel.ResourceReferenceId, + }; + + var json = JsonConvert.SerializeObject(jsonobj); + + var eventEntity = new Event + { + EventTypeEnum = EventTypeEnum.SearchLaunchResource, + JsonData = json, + UserId = userId, + GroupId = searchActionResourceModel.GroupId, + }; + + return await this.eventService.CreateAsync(userId, eventEntity); + } + + /// + public async Task CreateCatalogueSearchActionAsync(SearchActionCatalogueModel searchActionCatalogueModel, int userId) + { + var jsonobj = new + { + searchActionCatalogueModel.SearchText, + searchActionCatalogueModel.NodePathId, + searchActionCatalogueModel.ItemIndex, + searchActionCatalogueModel.NumberOfHits, + searchActionCatalogueModel.TotalNumberOfHits, + searchActionCatalogueModel.CatalogueId, + }; + + var json = JsonConvert.SerializeObject(jsonobj); + + var eventEntity = new Event + { + EventTypeEnum = EventTypeEnum.SearchLaunchCatalogue, + JsonData = json, + UserId = userId, + GroupId = searchActionCatalogueModel.GroupId, + }; + + return await this.eventService.CreateAsync(userId, eventEntity); + } + + /// + public async Task CreateCatalogueResourceSearchActionAsync(SearchActionResourceModel searchActionResourceModel, int userId) + { + var jsonobj = new + { + searchActionResourceModel.SearchText, + searchActionResourceModel.NodePathId, + searchActionResourceModel.ItemIndex, + searchActionResourceModel.ResourceReferenceId, + searchActionResourceModel.NumberOfHits, + searchActionResourceModel.TotalNumberOfHits, + }; + + var json = JsonConvert.SerializeObject(jsonobj); + + var eventEntity = new Event + { + EventTypeEnum = EventTypeEnum.LaunchCatalogueResource, + JsonData = json, + UserId = userId, + GroupId = searchActionResourceModel.GroupId, + }; + + return await this.eventService.CreateAsync(userId, eventEntity); + } + + /// + public async Task SubmitFeedbackAsync(SearchFeedBackModel searchFeedbackModel, int userId) + { + var jsonobj = new + { + searchFeedbackModel.SearchText, + searchFeedbackModel.Feedback, + searchFeedbackModel.TotalNumberOfHits, + }; + + var json = JsonConvert.SerializeObject(jsonobj); + + var eventEntity = new Event + { + EventTypeEnum = EventTypeEnum.SearchSubmitFeedback, + JsonData = json, + UserId = userId, + GroupId = searchFeedbackModel.GroupId, + }; + + return await this.eventService.CreateAsync(userId, eventEntity); + } + + /// + public async Task CreateSearchTermEvent(SearchRequestModel searchRequestModel, int userId) + { + var pageSize = searchRequestModel.PageSize; + var offset = searchRequestModel.PageIndex * pageSize; + + var remainingItems = searchRequestModel.TotalNumberOfHits - offset; + var resultsPerPage = remainingItems >= pageSize ? pageSize : remainingItems; + + var searchEventModel = this.mapper.Map(searchRequestModel); + searchEventModel.ItemsViewed = resultsPerPage; + var json = JsonConvert.SerializeObject(searchEventModel); + + var eventEntity = new Event + { + EventTypeEnum = searchRequestModel.EventTypeEnum, + JsonData = json, + UserId = userId, + GroupId = searchRequestModel.GroupId, + }; + + return await this.eventService.CreateAsync(userId, eventEntity); + } + + /// + public async Task CreateCatalogueSearchTermEvent(CatalogueSearchRequestModel catalogueSearchRequestModel, int userId) + { + var offset = catalogueSearchRequestModel.PageIndex * catalogueSearchRequestModel.PageSize; + + var remainingItems = catalogueSearchRequestModel.TotalNumberOfHits - offset; + var resultsPerPage = remainingItems >= catalogueSearchRequestModel.PageSize ? catalogueSearchRequestModel.PageSize : remainingItems; + + var searchCatalogueEventModel = this.mapper.Map(catalogueSearchRequestModel); + searchCatalogueEventModel.ItemsViewed = resultsPerPage; + var json = JsonConvert.SerializeObject(searchCatalogueEventModel); + var eventEntity = new Event + { + EventTypeEnum = catalogueSearchRequestModel.EventTypeEnum, + JsonData = json, + UserId = userId, + GroupId = catalogueSearchRequestModel.GroupId, + }; + + return await this.eventService.CreateAsync(userId, eventEntity); + } + + /// + public async Task SendResourceSearchEventClickAsync(SearchActionResourceModel searchActionResourceModel) + { + // Azure Search doesn't need click tracking like Findwise + // Log the event but return true + this.logger.LogInformation($"Search click event logged for resource {searchActionResourceModel.ResourceReferenceId}"); + return await Task.FromResult(true); + } + + /// + public async Task CreateCatalogueSearchTerm(CatalogueSearchRequestModel catalogueSearchRequestModel, int resultsPerPage, int userId) + { + var searchCatalogueEventModel = this.mapper.Map(catalogueSearchRequestModel); + searchCatalogueEventModel.ItemsViewed = resultsPerPage; + var json = JsonConvert.SerializeObject(searchCatalogueEventModel); + var eventEntity = new Event + { + EventTypeEnum = catalogueSearchRequestModel.EventTypeEnum, + JsonData = json, + UserId = userId, + GroupId = catalogueSearchRequestModel.GroupId, + }; + + return await this.eventService.CreateAsync(userId, eventEntity); + } + + /// + public async Task Search(ResourceSearchRequest query, int? currentUserId) + { + try + { + var searchOptions = new SearchOptions + { + Skip = query.Offset, + Size = query.Limit, + IncludeTotalCount = true, + }; + + SearchResults response = await this.searchClient.SearchAsync( + query.SearchText, + searchOptions); + + var documentsFound = response.GetResults().Select(r => r.Document).ToList(); + var findwiseResourceIds = documentsFound.Select(d => int.Parse(d.Id)).ToList(); + + if (!findwiseResourceIds.Any()) + { + return new ResourceSearchResultModel( + new List(), + LearningHub.Nhs.OpenApi.Models.ServiceModels.Findwise.FindwiseRequestStatus.Success, + 0); + } + + var resourceMetadataViewModels = await this.GetResourceMetadataViewModels(findwiseResourceIds, currentUserId); + + var totalHits = (int)(response.TotalCount ?? 0); + + return new ResourceSearchResultModel( + resourceMetadataViewModels, + LearningHub.Nhs.OpenApi.Models.ServiceModels.Findwise.FindwiseRequestStatus.Success, + totalHits); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Azure Search query failed"); + return ResourceSearchResultModel.FailedWithStatus( + LearningHub.Nhs.OpenApi.Models.ServiceModels.Findwise.FindwiseRequestStatus.Timeout); + } + } + + /// + public async Task RemoveResourceFromSearchAsync(int resourceId) + { + try + { + var adminClient = AzureSearchClientFactory.CreateAdminClient(this.azureSearchConfig); + + await adminClient.DeleteDocumentsAsync("id", new[] { resourceId.ToString() }); + this.logger.LogInformation($"Resource {resourceId} removed from Azure Search index"); + } + catch (Exception ex) + { + this.logger.LogError(ex, $"Failed to remove resource {resourceId} from Azure Search"); + throw new Exception($"Removal of resource from search failed: {resourceId} : {ex.Message}"); + } + } + + /// + public async Task SendResourceForSearchAsync(SearchResourceRequestModel searchResourceRequestModel, int userId, int? iterations) + { + try + { + var adminClient = AzureSearchClientFactory.CreateAdminClient(this.azureSearchConfig); + + // Map SearchResourceRequestModel to PocSearchDocument + var document = new Models.ServiceModels.AzureSearch.SearchDocument + { + Id = searchResourceRequestModel.Id.ToString(), + Title = searchResourceRequestModel.Title ?? string.Empty, + // TODO: [BY] + //Description = searchResourceRequestModel.Description ?? string.Empty, + //ResourceType = searchResourceRequestModel.ResourceType ?? string.Empty, + //ContentType = searchResourceRequestModel.ContentType, + //DateAuthored = searchResourceRequestModel.DateAuthored, + //Rating = searchResourceRequestModel.Rating, + //Author = searchResourceRequestModel.Author ?? string.Empty, + }; + + await adminClient.IndexDocumentsAsync(IndexDocumentsBatch.Upload(new[] { document })); + this.logger.LogInformation($"Resource {searchResourceRequestModel.Id} added to Azure Search index"); + return true; + } + catch (Exception ex) + { + this.logger.LogError(ex, $"Failed to add resource {searchResourceRequestModel.Id} to Azure Search"); + throw new Exception($"Posting of resource to search failed: {searchResourceRequestModel.Id} : {ex.Message}"); + } + } + + /// + public async Task SendCatalogueSearchEventAsync(SearchActionCatalogueModel searchActionCatalogueModel) + { + // Azure Search doesn't need click tracking like Findwise + // Log the event but return true + this.logger.LogInformation($"Catalogue search click event logged for catalogue {searchActionCatalogueModel.CatalogueId}"); + return await Task.FromResult(true); + } + + /// + public async Task GetAllCatalogueSearchResultsAsync(AllCatalogueSearchRequestModel catalogSearchRequestModel) + { + var viewmodel = new SearchAllCatalogueResultModel(); + CancellationToken cancellationToken = default; + try + { + var offset = catalogSearchRequestModel.PageIndex * catalogSearchRequestModel.PageSize; + var filters = new Dictionary> + { + { "resource_collection", new List { "Catalogue" } } + }; + + var searchOptions = new SearchOptions + { + Skip = offset, + Size = catalogSearchRequestModel.PageSize, + IncludeTotalCount = true, + Filter = SearchFilterBuilder.BuildFilterExpression(filters) + }; + + SearchResults response = await this.searchClient.SearchAsync( + catalogSearchRequestModel.SearchText, searchOptions, cancellationToken); + + var documentList = new CatalogueDocumentList + { + Documents = response.GetResults() + .Select(result => + { + var doc = result.Document; + doc.ParseManualTags(); + + return new CatalogueDocument + { + Id = doc.Id?.Substring(1), + Name = doc.Title, + Description = doc.Description, + Click = new SearchClickModel { Payload = new SearchClickPayloadModel { HitNumber = 1 }, Url = "binon" } + }; + }) + .ToArray() + }; + + viewmodel.DocumentList = documentList; + viewmodel.Stats = new Stats + { + TotalHits = Convert.ToInt32(response.TotalCount) + }; + catalogSearchRequestModel.TotalNumberOfHits = viewmodel.Stats.TotalHits; + + return viewmodel; + } + catch (Exception ex) + { + this.logger.LogError(ex, "Azure Search all catalogue query failed"); + throw; + } + } + + /// + public async Task GetAutoSuggestionResultsAsync(string term, CancellationToken cancellationToken = default) + { + var viewmodel = new AutoSuggestionModel(); + + try + { + var searchOptions = new SearchOptions + { + Size = 10, + }; + + var response = await this.searchClient.SearchAsync( + term, + searchOptions, + cancellationToken); + + var suggestOptions = new SuggestOptions + { + Size = 50, + UseFuzzyMatching = true + }; + suggestOptions.SearchFields.Add("title"); + suggestOptions.SearchFields.Add("description"); + suggestOptions.SearchFields.Add("manual_tag"); + suggestOptions.Select.Add("id"); + suggestOptions.Select.Add("title"); + suggestOptions.Select.Add("description"); + suggestOptions.Select.Add("manual_tag"); + suggestOptions.Select.Add("resource_type"); + suggestOptions.Select.Add("resource_collection"); + + var autoOptions = new AutocompleteOptions + { + Mode = AutocompleteMode.OneTermWithContext, + Size = 50 + }; + + var searchText = LuceneQueryBuilder.EscapeLuceneSpecialCharacters(term); + var suggesterName = this.azureSearchConfig.SuggesterName; + + // Fire both requests in parallel for performance + var suggestTask = this.searchClient.SuggestAsync(searchText, suggesterName, suggestOptions, cancellationToken); + var autoTask = this.searchClient.AutocompleteAsync(searchText, suggesterName, autoOptions, cancellationToken); + + await Task.WhenAll(suggestTask, autoTask); + + var suggestResponse = await suggestTask; + var autoResponse = await autoTask; + + var suggestResults = suggestResponse.Value.Results + .Where(r => !string.IsNullOrEmpty(r.Document?.Title)) + .Select(r => new + { + Id = r.Document.Id, + Text = r.Document.Title.Trim(), + Type = r.Document.ResourceCollection ?? "Suggestion" + }); + + var autoResults = autoResponse.Value.Results + .Where(r => !string.IsNullOrWhiteSpace(r.Text)) + .Select((r, index) => new + { + Id = "A" + (index + 1), + Text = r.Text.Trim(), + Type = "AutoComplete" + }); + + var combined = suggestResults + .Concat(autoResults) + .GroupBy(r => r.Text, StringComparer.OrdinalIgnoreCase) + .Select(g => g.First()) + .ToList(); + + viewmodel.Stats = new Stats + { + TotalHits = combined.Count + }; + + var autoSuggestionResource = new AutoSuggestionResource + { + TotalHits = suggestResults.Count(), + ResourceDocumentList = suggestResults + .Where(a => a.Type == "Resource") + .Select(item => new AutoSuggestionResourceDocument + { + Id = item.Id?.Substring(1), + Title = item.Text, + Click = new AutoSuggestionClickModel { Payload = new AutoSuggestionClickPayloadModel { HitNumber = 1 }, Url = "binon" } + }) + .Take(3) + .ToList() + }; + + var autoSuggestionCatalogue = new AutoSuggestionCatalogue + { + TotalHits = suggestResults.Count(), + CatalogueDocumentList = suggestResults + .Where(a => a.Type == "Catalogue") + .Select(item => new AutoSuggestionCatalogueDocument + { + Id = item.Id?.Substring(1), + Name = item.Text, + Click = new AutoSuggestionClickModel { Payload = new AutoSuggestionClickPayloadModel { HitNumber = 1 }, Url = "binon" } + }) + .Take(3) + .ToList() + }; + + var autoSuggestionConcept = new AutoSuggestionConcept + { + TotalHits = autoResults.Count(), + ConceptDocumentList = autoResults + .Select(item => new AutoSuggestionConceptDocument + { + Id = item.Id?.Substring(1), + Concept = item.Text, + Title = item.Text, + Click = new AutoSuggestionClickModel { Payload = new AutoSuggestionClickPayloadModel { HitNumber = 1 }, Url = "binon" } + }) + .ToList() + }; + + viewmodel.ResourceDocument = autoSuggestionResource; + viewmodel.CatalogueDocument = autoSuggestionCatalogue; + viewmodel.ConceptDocument = autoSuggestionConcept; + + return viewmodel; + } + catch (Exception ex) + { + this.logger.LogError(ex, "Azure Search auto-suggestion query failed for term: {Term}", term); + throw; + } + } + + /// + public async Task SendAutoSuggestionEventAsync(AutoSuggestionClickPayloadModel clickPayloadModel) + { + // Azure Search doesn't need click tracking like Findwise + // Log the event but return true + this.logger.LogInformation("Auto-suggestion click event logged"); + return await Task.FromResult(true); + } + + private async Task> GetResourceMetadataViewModels( + List findwiseResourceIds, int? currentUserId) + { + List resourceActivities = new List(); + List resourceMetadataViewModels = new List(); + + if (!findwiseResourceIds.Any()) + { + return new List(); + } + + var resourcesFound = await this.resourceRepository.GetResourcesFromIds(findwiseResourceIds); + + if (currentUserId.HasValue) + { + List resourceIds = resourcesFound.Select(x => x.Id).ToList(); + List userIds = new List { currentUserId.Value }; + + resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(resourceIds, userIds))?.ToList() ?? new List(); + } + + resourceMetadataViewModels = resourcesFound.Select(resource => this.MapToViewModel(resource, resourceActivities.Where(x => x.ResourceId == resource.Id).ToList())) + .OrderBySequence(findwiseResourceIds) + .ToList(); + + var unmatchedResources = findwiseResourceIds + .Except(resourceMetadataViewModels.Select(r => r.ResourceId)).ToList(); + + if (unmatchedResources.Any()) + { + var unmatchedResourcesIdsString = string.Join(", ", unmatchedResources); + this.logger.LogWarning( + "Azure Search returned documents that were not found in the database with IDs: " + + unmatchedResourcesIdsString); + } + + return resourceMetadataViewModels; + } + + 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( + $"Resource with id {resource.Id} is missing a current resource version"); + } + + if (!hasRating) + { + this.logger.LogInformation( + $"Resource with id {resource.Id} is missing a ResourceVersionRatingSummary"); + } + + var resourceTypeNameOrEmpty = resource.GetResourceTypeNameOrEmpty(); + if (resourceTypeNameOrEmpty == string.Empty) + { + 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?.MajorVersion ?? 0, + resource.CurrentResourceVersion?.ResourceVersionRatingSummary?.AverageRating ?? 0.0m, + majorVersionIdActivityStatusDescription); + } + + private ResourceReferenceViewModel GetResourceReferenceViewModel( + ResourceReference resourceReference) + { + return new ResourceReferenceViewModel( + resourceReference.OriginalResourceReferenceId, + resourceReference.GetCatalogue(), + this.learningHubService.GetResourceLaunchUrl(resourceReference.OriginalResourceReferenceId)); + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/BaseService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/BaseService.cs index 612ae06ca..ac1eb38ba 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/BaseService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/BaseService.cs @@ -16,20 +16,20 @@ public abstract class BaseService /// private readonly ILogger logger; - /// - /// The Find Wise HTTP Client. - /// - private IFindwiseClient findwiseClient; + ///// + ///// The Find Wise HTTP Client. + ///// + //private IFindwiseClient findwiseClient; /// /// Initializes a new instance of the class. /// The base service. /// - /// The Find Wise http client. + ///// The Find Wise http client. /// The logger. - protected BaseService(IFindwiseClient findwiseClient, ILogger logger) + protected BaseService(ILogger logger) { - this.findwiseClient = findwiseClient; + //this.findwiseClient = findwiseClient; this.logger = logger; } @@ -44,9 +44,9 @@ protected ILogger Logger /// /// Gets the Find Wise HTTP Client. /// - protected IFindwiseClient FindwiseClient - { - get { return this.findwiseClient; } - } + //protected IFindwiseClient FindwiseClient + //{ + // get { return this.findwiseClient; } + //} } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/Findwise/NullFindwiseApiFacade.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/Findwise/NullFindwiseApiFacade.cs new file mode 100644 index 000000000..ef5f8d974 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/Findwise/NullFindwiseApiFacade.cs @@ -0,0 +1,59 @@ +namespace LearningHub.Nhs.OpenApi.Services.Services.Findwise +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using LearningHub.Nhs.Models.Search; + using LearningHub.Nhs.OpenApi.Services.Interface.Services; + using Microsoft.Extensions.Logging; + + /// + /// Null implementation of IFindwiseApiFacade for use when Azure Search is enabled. + /// This implementation performs no operations and is used to avoid Findwise calls when using Azure Search. + /// + public class NullFindwiseApiFacade : IFindwiseApiFacade + { + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public NullFindwiseApiFacade(ILogger logger) + { + this.logger = logger; + } + + /// + /// No-op implementation. Does not add or replace catalogues in Findwise. + /// + /// The catalogues to add/replace in the index. + /// The task. + public Task AddOrReplaceAsync(List catalogues) + { + this.logger.LogDebug("NullFindwiseApiFacade: Skipping AddOrReplaceAsync for {Count} catalogues (Azure Search is enabled)", catalogues?.Count ?? 0); + return Task.CompletedTask; + } + + /// + /// No-op implementation. Does not add or replace resources in Findwise. + /// + /// The resources to add/replace in the index. + /// The task. + public Task AddOrReplaceAsync(List resources) + { + this.logger.LogDebug("NullFindwiseApiFacade: Skipping AddOrReplaceAsync for {Count} resources (Azure Search is enabled)", resources?.Count ?? 0); + return Task.CompletedTask; + } + + /// + /// No-op implementation. Does not remove resources from Findwise. + /// + /// The resources to remove from Findwise. + /// The task. + public Task RemoveAsync(List resources) + { + this.logger.LogDebug("NullFindwiseApiFacade: Skipping RemoveAsync for {Count} resources (Azure Search is enabled)", resources?.Count ?? 0); + return Task.CompletedTask; + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/Messaging/MessageService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/Messaging/MessageService.cs index 815257e27..e6eb0a644 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/Messaging/MessageService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/Messaging/MessageService.cs @@ -23,14 +23,13 @@ public class MessageService : BaseService, IMessageService /// /// Initializes a new instance of the class. /// - /// The findwiseHttpClient. + // /// The findwiseHttpClient. /// The logger. /// The message repository. - public MessageService( - IFindwiseClient findwiseClient, + public MessageService( ILogger logger, IMessageRepository messageRepository) - : base(findwiseClient, logger) + : base(logger) { this.messageRepository = messageRepository; } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationTemplateService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationTemplateService.cs index 74e66bd44..fcd6548d7 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationTemplateService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationTemplateService.cs @@ -21,14 +21,14 @@ public class NotificationTemplateService : BaseService, IN /// /// Initializes a new instance of the class. /// - /// The findwise client. + ///// The findwise client. /// The logger. /// The notification template repository. public NotificationTemplateService( - IFindwiseClient fwClient, + // IFindwiseClient fwClient, ILogger logger, INotificationTemplateRepository notificationTemplateRepository) - : base(fwClient, logger) + : base(logger) { this.notificationTemplateRepository = notificationTemplateRepository; } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs index 82c9a52ca..d3eb98d67 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs @@ -5,6 +5,7 @@ namespace LearningHub.Nhs.OpenApi.Services.Services using System.Linq; using System.Net.Http; using System.Text; + using System.Threading; using System.Threading.Tasks; using System.Web; using AutoMapper; @@ -92,8 +93,9 @@ public SearchService( /// /// The search request model. /// The user id. + /// Cancellation token. /// The . - public async Task GetSearchResultAsync(SearchRequestModel searchRequestModel, int userId) + public async Task GetSearchResultAsync(SearchRequestModel searchRequestModel, int userId, CancellationToken cancellationToken = default) { SearchResultModel viewmodel = new SearchResultModel(); @@ -170,10 +172,11 @@ public async Task GetSearchResultAsync(SearchRequestModel sea /// /// The user id. /// + /// Cancellation token. /// /// The . /// - public async Task GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId) + public async Task GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId, CancellationToken cancellationToken = default) { var viewmodel = new SearchCatalogueResultModel(); @@ -641,8 +644,9 @@ public async Task GetAllCatalogueSearchResultsAsy /// The Get Auto suggestion Results Async method. /// /// The term. + /// Cancellation token. /// The . - public async Task GetAutoSuggestionResultsAsync(string term) + public async Task GetAutoSuggestionResultsAsync(string term, CancellationToken cancellationToken = default) { var viewmodel = new AutoSuggestionModel(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs index 87c94f64e..12c9e5139 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs @@ -6,9 +6,11 @@ namespace LearningHub.Nhs.OpenApi.Services using LearningHub.Nhs.OpenApi.Services.Interface.Services; using LearningHub.Nhs.OpenApi.Services.Interface.Services.Messaging; using LearningHub.Nhs.OpenApi.Services.Services; + using LearningHub.Nhs.OpenApi.Services.Services.AzureSearch; using LearningHub.Nhs.OpenApi.Services.Services.Findwise; using LearningHub.Nhs.OpenApi.Services.Services.Messaging; using LearningHub.Nhs.Services; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; /// @@ -20,11 +22,23 @@ public static class Startup /// Registers the implementations in the project with ASP.NET DI. /// /// The IServiceCollection. - public static void AddServices(this IServiceCollection services) + /// The configuration. + public static void AddServices(this IServiceCollection services, IConfiguration configuration) { - services.AddScoped(); + // Register search service based on feature flag + var useAzureSearch = configuration.GetValue("FeatureFlags:UseAzureSearch", false); + + if (useAzureSearch) + { + services.AddScoped(); + } + else + { + services.AddScoped(); + services.AddScoped(); + } + services.AddHttpClient(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -53,7 +67,6 @@ public static void AddServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddTransient(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -67,6 +80,16 @@ public static void AddServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + + // Register IFindwiseApiFacade based on feature flag + if (useAzureSearch) + { + services.AddScoped(); + } + else + { + services.AddScoped(); + } } } } \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs index 7d5a61795..8385b0211 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs @@ -25,6 +25,11 @@ public static class ConfigurationExtensions /// public const string FindwiseSectionName = "Findwise"; + /// + /// The AzureSearchSectionName. + /// + public const string AzureSearchSectionName = "AzureSearch"; + /// /// The LearningHubSectionName. /// @@ -58,6 +63,8 @@ public static void AddConfig(this IServiceCollection services, IConfiguration co services.AddOptions().Bind(config.GetSection(FindwiseSectionName)); + services.AddOptions().Bind(config.GetSection(AzureSearchSectionName)); + services.AddOptions().Bind(config.GetSection(LearningHubSectionName)); services.AddOptions().Bind(config.GetSection(LearningHubApiSectionName)); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs index 68981d051..f64665e78 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs @@ -83,7 +83,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddRepositories(this.Configuration); - services.AddServices(); + services.AddServices(this.Configuration); services.AddApplicationInsightsTelemetry(); services.AddControllers(options => { diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index bec2f2309..4f04ca015 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -49,6 +49,19 @@ }, "AzureStorageQueueConnectionString": "" }, + "FeatureFlags": { + "UseAzureSearch": false + }, + "AzureSearch": { + "ServiceEndpoint": "", + "AdminApiKey": "", + "QueryApiKey": "", + "IndexName": "", + "SuggesterName": "", + "DefaultItemLimitForSearch": 10, + "DescriptionLengthLimit": 3000, + "MaximumDescriptionLength": 150 + }, "FindWise": { "IndexUrl": "", "SearchBaseUrl": "", From bc12dde1264e44c27750d4b852ca738fd53d3dd1 Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 17 Nov 2025 11:38:00 +0000 Subject: [PATCH 003/106] Added new search filter and search result page and added feature flag in webui peroject --- LearningHub.Nhs.WebUI/Helpers/FeatureFlags.cs | 5 + .../Views/Search/Index.cshtml | 36 ++- .../Views/Search/_SearchFilter.cshtml | 207 ++++++++++++++++++ .../Views/Search/_SearchResult.cshtml | 145 ++++++++++++ LearningHub.Nhs.WebUI/appsettings.json | 3 +- .../Helpers/Search/SearchFilterBuilder.cs | 36 ++- .../AzureSearch/AzureSearchService.cs | 18 +- 7 files changed, 421 insertions(+), 29 deletions(-) create mode 100644 LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml create mode 100644 LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml diff --git a/LearningHub.Nhs.WebUI/Helpers/FeatureFlags.cs b/LearningHub.Nhs.WebUI/Helpers/FeatureFlags.cs index 7f019e336..43ce54c78 100644 --- a/LearningHub.Nhs.WebUI/Helpers/FeatureFlags.cs +++ b/LearningHub.Nhs.WebUI/Helpers/FeatureFlags.cs @@ -19,5 +19,10 @@ public static class FeatureFlags /// The EnableMoodle. /// public const string EnableMoodle = "EnableMoodle"; + + /// + /// The AzureSearch. + /// + public const string AzureSearch = "AzureSearch"; } } diff --git a/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml index 4b8e0e6e3..46cfcd84c 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml @@ -1,19 +1,22 @@ -@model LearningHub.Nhs.WebUI.Models.Search.SearchResultViewModel +@using Microsoft.FeatureManagement +@inject IFeatureManager FeatureManager +@model LearningHub.Nhs.WebUI.Models.Search.SearchResultViewModel @{ ViewData["Title"] = "Search"; + var azureSearchEnabled = await FeatureManager.IsEnabledAsync("AzureSearch"); } -@section styles{ +@section styles { }
-
+
@if (Model.DidYouMeanEnabled) {

- No results were found for @(Model.SearchString) please change your search term or explore the suggestions below + No results were found for @(Model.SearchString) please change your search term or explore the suggestions below

} else @@ -22,14 +25,14 @@ Search results @(!string.IsNullOrEmpty(Model.SearchString) ? "for " + Model.SearchString : string.Empty) } - +
@await Html.PartialAsync("_SearchBar", @Model.SearchString)
- @if (Model.CatalogueSearchResult?.TotalHits > 0) + @if (Model.CatalogueSearchResult?.TotalHits > 0 && !azureSearchEnabled) { @if (Model.DidYouMeanEnabled) { @@ -52,18 +55,33 @@ Showing results for @(Model.SuggestedResource)

} - @await Html.PartialAsync("_ResourceFilter", Model) + @if (azureSearchEnabled) + { + @await Html.PartialAsync("_SearchFilter", Model) + } + else + { + @await Html.PartialAsync("_ResourceFilter", Model) + } +
- @await Html.PartialAsync("_ResourceSearchResult", Model) + @if (azureSearchEnabled) + { + @await Html.PartialAsync("_SearchResult", Model) + } + else + { + @await Html.PartialAsync("_ResourceSearchResult", Model) + } @await Html.PartialAsync("_ResourcePagination", Model)
} - + @if ((Model.CatalogueSearchResult?.TotalHits ?? 0) == 0 && (Model.ResourceSearchResult?.TotalHits ?? 0) == 0) {
diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml new file mode 100644 index 000000000..b8bbc8807 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml @@ -0,0 +1,207 @@ +@using Microsoft.AspNetCore.WebUtilities +@model LearningHub.Nhs.WebUI.Models.Search.SearchResultViewModel + +@{ + var resourceResult = Model.ResourceSearchResult; + var filtersApplied = resourceResult.SortItemSelected.Value != string.Empty + || resourceResult.SearchFilters.Any(f => f.Selected) || resourceResult.SearchResourceAccessLevelFilters.Any(f => f.Selected) + || resourceResult.SearchProviderFilters.Any(f =>f.Selected); + + var queryParams = QueryHelpers.ParseQuery(Context.Request.QueryString.ToString().ToLower()); + queryParams["actiontype"] = "sort-filter"; + var pageUrl = Model.CatalogueId > 0 ? "/catalogue/" + Model.CatalogueUrl +"/search" : "/search/results"; + var actionUrl = QueryHelpers.AddQueryString(pageUrl, queryParams); + var pageFragment = "#search-filters"; + + string FilterSummary() + { + string summary = $"Sorted by {resourceResult.SortItemSelected.Name}"; + var filters = resourceResult.SearchFilters + .Where(f => f.Selected) + .Select(f => $"{f.DisplayName}"); + + var resourceAccessLevelFilters = resourceResult.SearchResourceAccessLevelFilters + .Where(f => f.Selected) + .Select(f => $"{char.ToUpper(f.DisplayName[0])}{f.DisplayName[1..]}"); + + if (resourceAccessLevelFilters.Any()) + { + summary += $" and Filtered by Audience access level {string.Join(" ", resourceAccessLevelFilters)}"; + } + + + var providerfilters = resourceResult.SearchProviderFilters.Where(f => f.Selected).Select(f => $"{f.DisplayName}"); + + if (providerfilters.Any()) + { + summary += $" and Filtered by Provider {string.Join(" ", providerfilters)}"; + } + + if (filters.Any()) + { + summary += $" and Filtered by Type {string.Join(" ", filters)}"; + } + return summary; + } +} + +

+ @($"Showing {resourceResult.TotalHits} result{(resourceResult.TotalHits == 1 ? string.Empty : "s")}") +

+ +@if (resourceResult.TotalHits > 0) +{ +
+
+ + + + +
+ + + + Sort and filter results + + +
+ +
+
+ @Html.Raw(FilterSummary()) +
+ @if (filtersApplied) + { + + } +
+
+ +
+ + @if (this.ViewBag.SelectFilterError == true) + { + + Error: You must update the sort or filter before applying changes + + } + +
+
+ +

Sort by:

+
+ +
+ @foreach (var sortItem in resourceResult.SortItemList) + { +
+
+ + +
+
+ } + +
+
+
+ + @if (resourceResult.SearchResourceAccessLevelFilters != null && resourceResult.SearchResourceAccessLevelFilters.Any()) + { +
+ +
+
+ +

Filter by audience access level:

+
+ +
+ @foreach (var filter in resourceResult.SearchResourceAccessLevelFilters) + { +
+
+ + +
+
+ } +
+
+
+ } + + @if (resourceResult.SearchProviderFilters.Count > 0) + { +
+ +
+
+ +

Filter by provider:

+
+ +
+ @foreach (var filter in resourceResult.SearchProviderFilters) + { +
+ +
+ + +
+
+ } +
+
+
+ } + +
+ +
+
+ +

Filter by:

+
+ +
+ @foreach (var filter in resourceResult.SearchFilters) + { +
+ +
+ + +
+
+ } + +
+
+
+ + +
+ +
+
+
+} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml new file mode 100644 index 000000000..d2717d244 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml @@ -0,0 +1,145 @@ +@model LearningHub.Nhs.WebUI.Models.Search.SearchResultViewModel +@inject LearningHub.Nhs.WebUI.Interfaces.IMoodleApiService moodleApiService; + +@using System.Linq; +@using System.Web; +@using LearningHub.Nhs.WebUI.Helpers; +@using LearningHub.Nhs.Models.Search; +@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); + + 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(searchSignal?.Query)); + + 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}"; + } + + string GetMoodleCourseUrl(string courseIdWithPrefix) + { + const string prefix = "M"; + + if (string.IsNullOrWhiteSpace(courseIdWithPrefix)) + { + return string.Empty; + } + + if (!courseIdWithPrefix.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return string.Empty; + } + + var courseIdPart = courseIdWithPrefix.Substring(prefix.Length); + + if (int.TryParse(courseIdPart, out int courseId)) + { + return moodleApiService.GetCourseUrl(courseId); + } + else + { + return string.Empty; + } + } + + bool showCatalogueFieldsInResources = ViewBag.ShowCatalogueFieldsInResources == null || ViewBag.ShowCatalogueFieldsInResources == true; + bool resourceAccessLevelFilterSelected = resourceResult.SearchResourceAccessLevelFilters.Any(f => f.Selected); +} + +@foreach (var item in resourceResult.DocumentModel) +{ + var provider = item.Providers?.FirstOrDefault(); + +
+

+ @if (item.ResourceType == "moodle") + { + @item.Title + } + else + { + @item.Title + } +

+ + @if (provider != null) + { +
+
+ + @ProviderHelper.GetProviderString(provider.Name) +
+
+ } + @if (item.CatalogueRestrictedAccess && !Model.HideRestrictedBadge && showCatalogueFieldsInResources) + { +

+ @((item.CatalogueHasAccess || this.User.IsInRole("Administrator")) ? "Access Granted" : "Access restricted") +

+ } + + @if (!resourceAccessLevelFilterSelected) + { +
+
+ Audience access level: + @ResourceAccessLevelHelper.GetResourceAccessLevelText((ResourceAccessibilityEnum)item.ResourceAccessLevel) +
+
+ } + + +
+
+ Type: + @UtilityHelper.GetPrettifiedResourceTypeName(UtilityHelper.ToEnum(item.ResourceType), 0) +
+
+ @if (item.ResourceType != "moodle") + { + @await Html.PartialAsync("../Shared/_StarRating.cshtml", item.Rating) + } +
+
+ +

+ @item.Description +

+ +
+ @if (!string.IsNullOrWhiteSpace(item.CatalogueBadgeUrl) && showCatalogueFieldsInResources) + { + Provider's catalogue badge + } + + @if (!string.IsNullOrEmpty(item.CatalogueName) && !this.Model.CatalogueId.HasValue && showCatalogueFieldsInResources) + { + + } + +
+ @UtilityHelper.GetAttribution(item.Authors) + @if (!string.IsNullOrEmpty(item.AuthoredDate)) + { + @UtilityHelper.GetInOn(item.AuthoredDate) + @: @item.AuthoredDate + } +
+
+
+ index++; +} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/appsettings.json b/LearningHub.Nhs.WebUI/appsettings.json index c1875cc2b..fe4afe2f0 100644 --- a/LearningHub.Nhs.WebUI/appsettings.json +++ b/LearningHub.Nhs.WebUI/appsettings.json @@ -167,7 +167,8 @@ "FeatureManagement": { "ContributeAudioVideoResource": true, "DisplayAudioVideoResource": true, - "EnableMoodle": false + "EnableMoodle": false, + "AzureSearch": false }, "IpRateLimiting": { "EnableEndpointRateLimiting": true, diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs index f1709c8db..31e79e985 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs @@ -10,6 +10,38 @@ namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search /// public static class SearchFilterBuilder { + + public static Dictionary> CombineAndNormaliseFilters(string requestTypeFilterText, string? providerFilterText) + { + var filters = new Dictionary> + { + // { "resource_collection", new List { "Resource" } } + }; + + // Parse and merge additional filters from query string + var requestTypeFilters = ParseQueryStringFilters(requestTypeFilterText); + var providerFilters = ParseQueryStringFilters(providerFilterText); + + // Merge filters from both sources + MergeFilterDictionary(filters, requestTypeFilters); + // MergeFilterDictionary(filters, providerFilters); + + NormalizeResourceTypeFilters(filters); + + return filters; + } + + private static void MergeFilterDictionary(Dictionary> target, Dictionary> source) + { + foreach (var kvp in source) + { + if (!target.ContainsKey(kvp.Key)) + target[kvp.Key] = new List(); + + target[kvp.Key].AddRange(kvp.Value); + } + } + /// /// Builds a filter expression from a dictionary of filters. /// @@ -39,7 +71,7 @@ public static string BuildFilterExpression(Dictionary>? fil /// /// The query string to parse. /// A dictionary of filter names and their values. - public static Dictionary> ParseFiltersFromQuery(string? queryString) + public static Dictionary> ParseQueryStringFilters(string? queryString) { var filters = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -55,7 +87,7 @@ public static Dictionary> ParseFiltersFromQuery(string? que foreach (string? key in parsed.AllKeys) { - if (key == null) + if (key == null) continue; // skip null keys var values = parsed.GetValues(key); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 1661bb9ad..316db67de 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -89,24 +89,8 @@ public async Task GetSearchResultAsync(SearchRequestModel sea var pageSize = searchRequestModel.PageSize; var offset = searchRequestModel.PageIndex * pageSize; - // Build base filters - var filters = new Dictionary> - { - { "resource_collection", new List { "Resource" } } - }; - - // Parse and merge additional filters from query string - var parsedFilters = SearchFilterBuilder.ParseFiltersFromQuery(searchRequestModel.FilterText); - foreach (var kvp in parsedFilters) - { - if (filters.ContainsKey(kvp.Key)) - filters[kvp.Key].AddRange(kvp.Value); - else - filters.Add(kvp.Key, kvp.Value); - } - // Normalize resource_type filter values - SearchFilterBuilder.NormalizeResourceTypeFilters(filters); + var filters = SearchFilterBuilder.CombineAndNormaliseFilters(searchRequestModel.FilterText, searchRequestModel.ProviderFilterText); // Build query string var query = searchQueryType == SearchQueryType.Full From 912d9e0039de12b79452267434e13294e9542384 Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 17 Nov 2025 17:52:05 +0000 Subject: [PATCH 004/106] Added resource collection filter --- .../Controllers/SearchController.cs | 3 +- .../Models/Search/SearchRequestViewModel.cs | 5 + .../Models/Search/SearchResultViewModel.cs | 5 + .../Views/Search/_SearchFilter.cshtml | 59 +++- .../Views/Search/_SearchResult.cshtml | 251 +++++++++--------- 5 files changed, 187 insertions(+), 136 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs index a7b53fb11..0b5c1b1ef 100644 --- a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs @@ -109,10 +109,11 @@ public async Task Index(SearchRequestViewModel search, bool noSor /// The search group id. /// The search id. /// The action type. + /// The show filter. /// The feedback. /// The actionResult. [HttpPost("results")] - public async Task IndexPost([FromQuery] SearchRequestViewModel search, int resourceCount, [FromForm] IEnumerable filters, [FromForm] int? resourceAccessLevelId, [FromForm] IEnumerable providerfilters, [FromForm] int? sortby, [FromForm] string groupId, [FromForm] int searchId, [FromQuery] string actionType, string feedback) + public async Task IndexPost([FromQuery] SearchRequestViewModel search, int resourceCount, [FromForm] IEnumerable filters, [FromForm] int? resourceAccessLevelId, [FromForm] IEnumerable providerfilters, [FromForm] int? sortby, [FromForm] string groupId, [FromForm] int searchId, [FromQuery] string actionType, [FromQuery] string resourceCollectionFilter, string feedback) { if (actionType == "feedback") { diff --git a/LearningHub.Nhs.WebUI/Models/Search/SearchRequestViewModel.cs b/LearningHub.Nhs.WebUI/Models/Search/SearchRequestViewModel.cs index 8b4d5de04..9874218a8 100644 --- a/LearningHub.Nhs.WebUI/Models/Search/SearchRequestViewModel.cs +++ b/LearningHub.Nhs.WebUI/Models/Search/SearchRequestViewModel.cs @@ -75,5 +75,10 @@ public class SearchRequestViewModel /// [FromQuery] public IEnumerable ProviderFilters { get; set; } + + /// + /// Gets or sets the show filter (all, catalogues, courses, resources). + /// + public string ResourceCollectionFilter { get; set; } } } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Models/Search/SearchResultViewModel.cs b/LearningHub.Nhs.WebUI/Models/Search/SearchResultViewModel.cs index 543a50e5f..64602de72 100644 --- a/LearningHub.Nhs.WebUI/Models/Search/SearchResultViewModel.cs +++ b/LearningHub.Nhs.WebUI/Models/Search/SearchResultViewModel.cs @@ -93,5 +93,10 @@ public class SearchResultViewModel /// Gets or sets Suggested Resource name. /// public string SuggestedResource { get; set; } + + /// + /// Gets or sets the show filter (all, catalogues, courses, resources). + /// + public string ResourceCollectionFilter { get; set; } } } diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml index b8bbc8807..def3d511a 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml @@ -4,12 +4,12 @@ @{ var resourceResult = Model.ResourceSearchResult; var filtersApplied = resourceResult.SortItemSelected.Value != string.Empty - || resourceResult.SearchFilters.Any(f => f.Selected) || resourceResult.SearchResourceAccessLevelFilters.Any(f => f.Selected) - || resourceResult.SearchProviderFilters.Any(f =>f.Selected); + || resourceResult.SearchFilters.Any(f => f.Selected) || resourceResult.SearchResourceAccessLevelFilters.Any(f => f.Selected) + || resourceResult.SearchProviderFilters.Any(f => f.Selected); var queryParams = QueryHelpers.ParseQuery(Context.Request.QueryString.ToString().ToLower()); queryParams["actiontype"] = "sort-filter"; - var pageUrl = Model.CatalogueId > 0 ? "/catalogue/" + Model.CatalogueUrl +"/search" : "/search/results"; + var pageUrl = Model.CatalogueId > 0 ? "/catalogue/" + Model.CatalogueUrl + "/search" : "/search/results"; var actionUrl = QueryHelpers.AddQueryString(pageUrl, queryParams); var pageFragment = "#search-filters"; @@ -25,10 +25,10 @@ .Select(f => $"{char.ToUpper(f.DisplayName[0])}{f.DisplayName[1..]}"); if (resourceAccessLevelFilters.Any()) - { + { summary += $" and Filtered by Audience access level {string.Join(" ", resourceAccessLevelFilters)}"; } - + var providerfilters = resourceResult.SearchProviderFilters.Where(f => f.Selected).Select(f => $"{f.DisplayName}"); @@ -57,12 +57,51 @@ +
+
+ + Show + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
- - Sort and filter results - +
+ + Sort and filter results + + + Sorted by: @resourceResult.SortItemSelected.Name + +

@@ -128,7 +167,7 @@
+ value="@filter.Value" checked="@filter.Selected" class="@(filter.Count > 0 ? "" : "disabled")"> @@ -157,7 +196,7 @@
+ value="@filter.Value" checked="@filter.Selected" class="@(filter.Count > 0 ? "" : "disabled")"> diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml index d2717d244..190f7eeb9 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml @@ -1,5 +1,5 @@ @model LearningHub.Nhs.WebUI.Models.Search.SearchResultViewModel -@inject LearningHub.Nhs.WebUI.Interfaces.IMoodleApiService moodleApiService; +@inject LearningHub.Nhs.WebUI.Interfaces.IMoodleApiService moodleApiService; @using System.Linq; @using System.Web; @@ -11,135 +11,136 @@ @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); - - 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(searchSignal?.Query)); - - return $@"/search/record-resource-click?url=/Resource/{resourceReferenceId}&nodePathId={nodePathId}&itemIndex={payload?.HitNumber} + var resourceResult = Model.ResourceSearchResult; + var pagingModel = Model.ResourceResultPaging; + var index = pagingModel.CurrentPage * pagingModel.PageSize; + var searchString = HttpUtility.UrlEncode(Model.SearchString); + + 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(searchSignal?.Query)); + + 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}"; - } - - string GetMoodleCourseUrl(string courseIdWithPrefix) - { - const string prefix = "M"; - - if (string.IsNullOrWhiteSpace(courseIdWithPrefix)) - { - return string.Empty; - } - - if (!courseIdWithPrefix.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return string.Empty; - } - - var courseIdPart = courseIdWithPrefix.Substring(prefix.Length); - - if (int.TryParse(courseIdPart, out int courseId)) - { - return moodleApiService.GetCourseUrl(courseId); - } - else - { - return string.Empty; - } - } - - bool showCatalogueFieldsInResources = ViewBag.ShowCatalogueFieldsInResources == null || ViewBag.ShowCatalogueFieldsInResources == true; - bool resourceAccessLevelFilterSelected = resourceResult.SearchResourceAccessLevelFilters.Any(f => f.Selected); + } + + string GetMoodleCourseUrl(string courseIdWithPrefix) + { + const string prefix = "M"; + + if (string.IsNullOrWhiteSpace(courseIdWithPrefix)) + { + return string.Empty; + } + + if (!courseIdWithPrefix.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return string.Empty; + } + + var courseIdPart = courseIdWithPrefix.Substring(prefix.Length); + + if (int.TryParse(courseIdPart, out int courseId)) + { + return moodleApiService.GetCourseUrl(courseId); + } + else + { + return string.Empty; + } + } + + bool showCatalogueFieldsInResources = ViewBag.ShowCatalogueFieldsInResources == null || ViewBag.ShowCatalogueFieldsInResources == true; + bool resourceAccessLevelFilterSelected = resourceResult.SearchResourceAccessLevelFilters.Any(f => f.Selected); } @foreach (var item in resourceResult.DocumentModel) { - var provider = item.Providers?.FirstOrDefault(); - -
-

- @if (item.ResourceType == "moodle") - { - @item.Title - } - else - { - @item.Title - } -

- - @if (provider != null) - { -
-
- - @ProviderHelper.GetProviderString(provider.Name) -
-
- } - @if (item.CatalogueRestrictedAccess && !Model.HideRestrictedBadge && showCatalogueFieldsInResources) - { -

- @((item.CatalogueHasAccess || this.User.IsInRole("Administrator")) ? "Access Granted" : "Access restricted") -

- } - - @if (!resourceAccessLevelFilterSelected) - { -
-
- Audience access level: - @ResourceAccessLevelHelper.GetResourceAccessLevelText((ResourceAccessibilityEnum)item.ResourceAccessLevel) -
-
- } - - -
-
- Type: - @UtilityHelper.GetPrettifiedResourceTypeName(UtilityHelper.ToEnum(item.ResourceType), 0) -
-
- @if (item.ResourceType != "moodle") - { - @await Html.PartialAsync("../Shared/_StarRating.cshtml", item.Rating) - } -
-
- -

- @item.Description -

- -
- @if (!string.IsNullOrWhiteSpace(item.CatalogueBadgeUrl) && showCatalogueFieldsInResources) - { - Provider's catalogue badge - } - - @if (!string.IsNullOrEmpty(item.CatalogueName) && !this.Model.CatalogueId.HasValue && showCatalogueFieldsInResources) - { - - } - -
- @UtilityHelper.GetAttribution(item.Authors) - @if (!string.IsNullOrEmpty(item.AuthoredDate)) - { - @UtilityHelper.GetInOn(item.AuthoredDate) - @: @item.AuthoredDate - } -
-
-
- index++; + var provider = item.Providers?.FirstOrDefault(); + +
+

+ @if (item.ResourceType == "moodle") + { + @item.Title + } + else + { + @item.Title + } +

+ + @if (provider != null) + { +
+
+ + @ProviderHelper.GetProviderString(provider.Name) +
+
+ } + @if (item.CatalogueRestrictedAccess && !Model.HideRestrictedBadge && showCatalogueFieldsInResources) + { +

+ @((item.CatalogueHasAccess || this.User.IsInRole("Administrator")) ? "Access Granted" : "Access restricted") +

+ } + + @if (!resourceAccessLevelFilterSelected) + { +
+
+ Audience access level: + @ResourceAccessLevelHelper.GetResourceAccessLevelText((ResourceAccessibilityEnum)item.ResourceAccessLevel) +
+
+ } + + @if (!string.IsNullOrEmpty(item.ResourceType)) + { +
+
+ Type: + @UtilityHelper.GetPrettifiedResourceTypeName(UtilityHelper.ToEnum(item.ResourceType), 0) +
+
+ @if (item.ResourceType != "moodle") + { + @await Html.PartialAsync("../Shared/_StarRating.cshtml", item.Rating) + } +
+
+ } +

+ @item.Description +

+ +
+ @if (!string.IsNullOrWhiteSpace(item.CatalogueBadgeUrl) && showCatalogueFieldsInResources) + { + Provider's catalogue badge + } + + @if (!string.IsNullOrEmpty(item.CatalogueName) && !this.Model.CatalogueId.HasValue && showCatalogueFieldsInResources) + { + + } + +
+ @UtilityHelper.GetAttribution(item.Authors) + @if (!string.IsNullOrEmpty(item.AuthoredDate)) + { + @UtilityHelper.GetInOn(item.AuthoredDate) + @: @item.AuthoredDate + } +
+
+
+ index++; } \ No newline at end of file From d23b44faf03d3abe6c7c67e4a78b67affc88326c Mon Sep 17 00:00:00 2001 From: Binon Date: Tue, 18 Nov 2025 15:45:39 +0000 Subject: [PATCH 005/106] Get the resource collection filter working completely --- .../Controllers/SearchController.cs | 12 ++- .../Models/Search/SearchRequestViewModel.cs | 2 +- .../Models/Search/SearchResultViewModel.cs | 3 +- .../Services/SearchService.cs | 76 ++++++++++++++++++- .../Views/Search/_SearchFilter.cshtml | 17 +++-- .../AzureSearch/SearchDocument.cs | 21 ++++- .../Helpers/Search/SearchFilterBuilder.cs | 6 +- .../AzureSearch/AzureSearchService.cs | 11 +-- 8 files changed, 126 insertions(+), 22 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs index 0b5c1b1ef..a53981310 100644 --- a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs @@ -113,7 +113,7 @@ public async Task Index(SearchRequestViewModel search, bool noSor /// The feedback. /// The actionResult. [HttpPost("results")] - public async Task IndexPost([FromQuery] SearchRequestViewModel search, int resourceCount, [FromForm] IEnumerable filters, [FromForm] int? resourceAccessLevelId, [FromForm] IEnumerable providerfilters, [FromForm] int? sortby, [FromForm] string groupId, [FromForm] int searchId, [FromQuery] string actionType, [FromQuery] string resourceCollectionFilter, string feedback) + public async Task IndexPost([FromQuery] SearchRequestViewModel search, int resourceCount, [FromForm] IEnumerable filters, [FromForm] int? resourceAccessLevelId, [FromForm] IEnumerable providerfilters, [FromForm] int? sortby, [FromForm] string groupId, [FromForm] int searchId, [FromQuery] string actionType, [FromForm] IEnumerable resourceCollectionFilter, string feedback) { if (actionType == "feedback") { @@ -145,14 +145,17 @@ public async Task IndexPost([FromQuery] SearchRequestViewModel se var existingProviderFilters = (search.ProviderFilters ?? new List()).OrderBy(t => t); var newProviderFilters = providerfilters.OrderBy(t => t); var filterProviderUpdated = !newProviderFilters.SequenceEqual(existingProviderFilters); + var existingResourceCollectionFilter = (search.ResourceCollectionFilter ?? new List()).OrderBy(t => t); + var newResourceCollectionFilter = resourceCollectionFilter.OrderBy(t => t); + var filterResourceCollectionUpdated = !newResourceCollectionFilter.SequenceEqual(existingResourceCollectionFilter); - // No sort or resource type filter updated or resource access level filter updated or provider filter applied - if ((search.Sortby ?? 0) == sortby && !filterUpdated && !resourceAccessLevelFilterUpdated && !filterProviderUpdated) + // No sort or resource type filter updated or resource access level filter updated or provider filter applied or resource collection filter applied + if ((search.Sortby ?? 0) == sortby && !filterUpdated && !resourceAccessLevelFilterUpdated && !filterProviderUpdated && !filterResourceCollectionUpdated) { return await this.Index(search, noSortFilterError: true); } - if (search.ResourcePageIndex > 0 && (filterUpdated || resourceAccessLevelFilterUpdated || filterProviderUpdated)) + if (search.ResourcePageIndex > 0 && (filterUpdated || resourceAccessLevelFilterUpdated || filterProviderUpdated || filterResourceCollectionUpdated)) { search.ResourcePageIndex = null; } @@ -164,6 +167,7 @@ public async Task IndexPost([FromQuery] SearchRequestViewModel se search.GroupId = groupId; search.SearchId = searchId; search.ResourceAccessLevelId = resourceAccessLevelId; + search.ResourceCollectionFilter = resourceCollectionFilter; var routeValues = new RouteValueDictionary(search) { diff --git a/LearningHub.Nhs.WebUI/Models/Search/SearchRequestViewModel.cs b/LearningHub.Nhs.WebUI/Models/Search/SearchRequestViewModel.cs index 9874218a8..ebe9174ba 100644 --- a/LearningHub.Nhs.WebUI/Models/Search/SearchRequestViewModel.cs +++ b/LearningHub.Nhs.WebUI/Models/Search/SearchRequestViewModel.cs @@ -79,6 +79,6 @@ public class SearchRequestViewModel /// /// Gets or sets the show filter (all, catalogues, courses, resources). /// - public string ResourceCollectionFilter { get; set; } + public IEnumerable ResourceCollectionFilter { get; set; } } } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Models/Search/SearchResultViewModel.cs b/LearningHub.Nhs.WebUI/Models/Search/SearchResultViewModel.cs index 64602de72..336be2e17 100644 --- a/LearningHub.Nhs.WebUI/Models/Search/SearchResultViewModel.cs +++ b/LearningHub.Nhs.WebUI/Models/Search/SearchResultViewModel.cs @@ -1,6 +1,7 @@ namespace LearningHub.Nhs.WebUI.Models.Search { using System; + using System.Collections.Generic; using LearningHub.Nhs.Models.Paging; using LearningHub.Nhs.Models.Search; @@ -97,6 +98,6 @@ public class SearchResultViewModel /// /// Gets or sets the show filter (all, catalogues, courses, resources). /// - public string ResourceCollectionFilter { get; set; } + public List ResourceCollectionFilter { get; set; } } } diff --git a/LearningHub.Nhs.WebUI/Services/SearchService.cs b/LearningHub.Nhs.WebUI/Services/SearchService.cs index 333848a30..a10c1db1c 100644 --- a/LearningHub.Nhs.WebUI/Services/SearchService.cs +++ b/LearningHub.Nhs.WebUI/Services/SearchService.cs @@ -75,7 +75,7 @@ public async Task PerformSearch(IPrincipal user, SearchRe { SearchId = searchRequest.SearchId.Value, SearchText = searchString, - FilterText = searchRequest.Filters?.Any() == true ? $"&resource_type={string.Join("&resource_type=", searchRequest.Filters)}" : string.Empty, + FilterText = this.BuildFilterText(searchRequest.Filters, searchRequest.ResourceCollectionFilter), ProviderFilterText = searchRequest.ProviderFilters?.Any() == true ? $"&provider_ids={string.Join("&provider_ids=", searchRequest.ProviderFilters)}" : string.Empty, SortColumn = selectedSortItem.Value, SortDirection = selectedSortItem?.SortDirection, @@ -98,6 +98,7 @@ public async Task PerformSearch(IPrincipal user, SearchRe SearchViewModel resourceResult = null; SearchCatalogueViewModel catalogueResult = null; + var resourceCollectionFilter = new List(); if (searchString != string.Empty) { @@ -203,6 +204,28 @@ public async Task PerformSearch(IPrincipal user, SearchRe } } } + + // Process resource_collection facets + var collectionFacet = resourceResult.Facets.FirstOrDefault(x => x.Id == "resource_collection"); + if (collectionFacet != null && collectionFacet.Filters != null) + { + foreach (var filteritem in collectionFacet.Filters.Select(x => x.DisplayName).Distinct()) + { + var filter = collectionFacet.Filters.Where(x => x.DisplayName == filteritem).FirstOrDefault(); + + if (filter != null && !string.IsNullOrEmpty(filter.DisplayName)) + { + var searchfilter = new SearchFilterModel() + { + DisplayName = filter.DisplayName, + Count = filter.Count, + Value = filter.DisplayName.ToLower(), + Selected = searchRequest.ResourceCollectionFilter?.Contains(filter.DisplayName, StringComparer.OrdinalIgnoreCase) ?? false, + }; + resourceCollectionFilter.Add(searchfilter); + } + } + } } resourceResult.SortItemList = searchSortItemList; @@ -222,6 +245,7 @@ public async Task PerformSearch(IPrincipal user, SearchRe ResourceSearchResult = resourceResult, CatalogueSearchResult = catalogueResult, + // SearchCollectionFilters = resourceCollectionFilters, ResourceResultPaging = new SearchResultPagingModel { CurrentPage = searchRequest.ResourcePageIndex ?? 0, @@ -240,6 +264,8 @@ public async Task PerformSearch(IPrincipal user, SearchRe SuggestedResource = suggestedResource, }; + searchResultViewModel.ResourceCollectionFilter = resourceCollectionFilter; + return searchResultViewModel; } @@ -271,7 +297,7 @@ public async Task RegisterSearchEventsAsync(SearchRequestViewModel search, GroupId = Guid.Parse(search.GroupId), SortDirection = "descending", SortColumn = sortBy.ToString(), - FilterText = search.Filters?.Any() == true ? string.Join(",", search.Filters) : null, + FilterText = this.BuildAnalyticsFilterText(search.Filters, search.ResourceCollectionFilter), ResourceAccessLevelFilterText = search.ResourceAccessLevelId.HasValue ? search.ResourceAccessLevelId.Value.ToString() : null, ProviderFilterText = search.ProviderFilters?.Any() == true ? string.Join(",", allproviders.Where(n => search.ProviderFilters.Contains(n.Id.ToString())).Select(x => x.Name).ToList()) : null, }; @@ -737,6 +763,52 @@ public async Task SendAutoSuggestionClickActionAsync(AutoSuggestionClickPayloadM } } + /// + /// Builds the filter text combining resource type and resource collection filters. + /// + /// The resource type filters. + /// The resource collection filters. + /// The combined filter text. + private string BuildFilterText(IEnumerable resourceTypeFilter, IEnumerable resourceCollectionFilter) + { + var filterParts = new List(); + + if (resourceTypeFilter?.Any() == true) + { + filterParts.Add($"&resource_type={string.Join("&resource_type=", resourceTypeFilter)}"); + } + + if (resourceCollectionFilter?.Any() == true) + { + filterParts.Add($"&resource_collection={string.Join("&resource_collection=", resourceCollectionFilter)}"); + } + + return string.Join(string.Empty, filterParts); + } + + /// + /// Builds the analytics filter text combining resource type and resource collection filters for logging. + /// + /// The resource type filters. + /// The resource collection filters. + /// The combined filter text for analytics. + private string BuildAnalyticsFilterText(IEnumerable resourceTypeFilter, IEnumerable resourceCollectionFilter) + { + var filterParts = new List(); + + if (resourceTypeFilter?.Any() == true) + { + filterParts.AddRange(resourceTypeFilter); + } + + if (resourceCollectionFilter?.Any() == true) + { + filterParts.AddRange(resourceCollectionFilter.Select(f => $"collection:{f}")); + } + + return filterParts.Any() ? string.Join(",", filterParts) : null; + } + /// /// The RemoveHtmlTags. /// diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml index def3d511a..834ab8281 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml @@ -12,6 +12,7 @@ var pageUrl = Model.CatalogueId > 0 ? "/catalogue/" + Model.CatalogueUrl + "/search" : "/search/results"; var actionUrl = QueryHelpers.AddQueryString(pageUrl, queryParams); var pageFragment = "#search-filters"; + var selectedResourceCollection = "all"; string FilterSummary() { @@ -62,27 +63,33 @@ Show + @foreach (var rc in Model.ResourceCollectionFilter) + { + if (rc.Selected) + selectedResourceCollection = rc.Value; + }
- + -
+
- +
- +
- + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs index e30e34004..7cae9f8b4 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs @@ -18,7 +18,26 @@ public class SearchDocument ///
[SearchableField(IsKey = true)] [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + public string PrefixedId { get; set; } = string.Empty; + + /// + /// Gets the numeric ID extracted from the PrefixedId. + /// + [JsonIgnore] + public string Id + { + get + { + if (string.IsNullOrWhiteSpace(PrefixedId)) + return "0"; + + var parts = PrefixedId.Split('_'); + if (parts.Length != 2) + return "0"; + + return int.TryParse(parts[1], out int id) ? id.ToString() : "0"; + } + } /// /// Gets or sets the title. diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs index 31e79e985..10868b817 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs @@ -26,7 +26,7 @@ public static Dictionary> CombineAndNormaliseFilters(string MergeFilterDictionary(filters, requestTypeFilters); // MergeFilterDictionary(filters, providerFilters); - NormalizeResourceTypeFilters(filters); + NormaliseFilters(filters); return filters; } @@ -105,10 +105,10 @@ public static Dictionary> ParseQueryStringFilters(string? q } /// - /// Normalizes resource type filter values by capitalizing the first letter. + /// Normalizes resource type and resource collection filter values by capitalizing the first letter. /// /// The filters dictionary to normalize. - public static void NormalizeResourceTypeFilters(Dictionary>? filters) + public static void NormaliseFilters(Dictionary>? filters) { if (filters == null || !filters.ContainsKey("resource_type")) return; diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 316db67de..6f7188f4a 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -140,7 +140,7 @@ public async Task GetSearchResultAsync(SearchRequestModel sea return new Document { - Id = doc.Id?.Substring(1), + Id = doc.Id, Title = doc.Title, Description = doc.Description, ResourceType = doc.ResourceType?.Contains("SCORM e-learning resource") == true ? "scorm" : doc.ResourceType, @@ -301,7 +301,7 @@ public async Task GetCatalogueSearchResultAsync(Cata return new CatalogueDocument { - Id = doc.Id?.Substring(1), + Id = doc.Id, Name = doc.Title, Description = doc.Description, Click = new SearchClickModel { Payload = new SearchClickPayloadModel { HitNumber = 1 }, Url = "binon" } @@ -576,9 +576,10 @@ public async Task SendResourceForSearchAsync(SearchResourceRequestModel se // Map SearchResourceRequestModel to PocSearchDocument var document = new Models.ServiceModels.AzureSearch.SearchDocument { - Id = searchResourceRequestModel.Id.ToString(), - Title = searchResourceRequestModel.Title ?? string.Empty, // TODO: [BY] + // Id = searchResourceRequestModel.Id.ToString(), + // Title = searchResourceRequestModel.Title ?? string.Empty, + //Description = searchResourceRequestModel.Description ?? string.Empty, //ResourceType = searchResourceRequestModel.ResourceType ?? string.Empty, //ContentType = searchResourceRequestModel.ContentType, @@ -641,7 +642,7 @@ public async Task GetAllCatalogueSearchResultsAsy return new CatalogueDocument { - Id = doc.Id?.Substring(1), + Id = doc.Id, Name = doc.Title, Description = doc.Description, Click = new SearchClickModel { Payload = new SearchClickPayloadModel { HitNumber = 1 }, Url = "binon" } From a4d9c971504f89308714c1f03b23fb010479cdd4 Mon Sep 17 00:00:00 2001 From: Binon Date: Wed, 19 Nov 2025 15:14:21 +0000 Subject: [PATCH 006/106] Updated the resource collection filter from radio button to check boxes --- .../Controllers/SearchController.cs | 28 ++- .../Interfaces/ISearchService.cs | 9 + .../Services/SearchService.cs | 222 +++++++++++++++++- .../Views/Search/_SearchFilter.cshtml | 82 ++++--- 4 files changed, 301 insertions(+), 40 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs index a53981310..ee1cae636 100644 --- a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs @@ -8,6 +8,7 @@ namespace LearningHub.Nhs.WebUI.Controllers using LearningHub.Nhs.Models.Search; using LearningHub.Nhs.Models.Search.SearchClick; using LearningHub.Nhs.WebUI.Filters; + using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.Models.Search; using Microsoft.AspNetCore.Authorization; @@ -16,6 +17,7 @@ namespace LearningHub.Nhs.WebUI.Controllers using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using Microsoft.FeatureManagement; using Settings = LearningHub.Nhs.WebUI.Configuration.Settings; /// @@ -28,6 +30,7 @@ public class SearchController : BaseController { private readonly ISearchService searchService; private readonly IFileService fileService; + private readonly IFeatureManager featureManager; /// /// Initializes a new instance of the class. @@ -38,17 +41,20 @@ public class SearchController : BaseController /// The searchService. /// The logger. /// The fileService. + /// The Feature flag manager. public SearchController( IHttpClientFactory httpClientFactory, IWebHostEnvironment hostingEnvironment, IOptions settings, ISearchService searchService, ILogger logger, - IFileService fileService) + IFileService fileService, + IFeatureManager featureManager) : base(hostingEnvironment, httpClientFactory, logger, settings.Value) { this.searchService = searchService; this.fileService = fileService; + this.featureManager = featureManager; } /// @@ -65,7 +71,18 @@ public async Task Index(SearchRequestViewModel search, bool noSor search.SearchId ??= 0; search.GroupId = !string.IsNullOrWhiteSpace(search.GroupId) && Guid.TryParse(search.GroupId, out Guid groupId) ? groupId.ToString() : Guid.NewGuid().ToString(); - var searchResult = await this.searchService.PerformSearch(this.User, search); + // Fix: Ensure an instance of IFeatureManager is injected and used + var azureSearchEnabled = Task.Run(() => this.featureManager.IsEnabledAsync(FeatureFlags.AzureSearch)).Result; + SearchResultViewModel searchResult = new SearchResultViewModel(); + + if (azureSearchEnabled) + { + searchResult = await this.searchService.PerformSearch(this.User, search); + } + else + { + searchResult = await this.searchService.PerformSearchInFindwise(this.User, search); + } if (search.SearchId == 0 && searchResult.ResourceSearchResult != null) { @@ -73,10 +90,13 @@ public async Task Index(SearchRequestViewModel search, bool noSor search, SearchFormActionTypeEnum.BasicSearch, searchResult.ResourceSearchResult.TotalHits, - searchResult.CatalogueSearchResult.TotalHits); + searchResult.CatalogueSearchResult != null ? searchResult.CatalogueSearchResult.TotalHits : 0); searchResult.ResourceSearchResult.SearchId = searchId; - searchResult.CatalogueSearchResult.SearchId = searchId; + if (searchResult.CatalogueSearchResult != null) + { + searchResult.CatalogueSearchResult.SearchId = searchId; + } } if (filterApplied) diff --git a/LearningHub.Nhs.WebUI/Interfaces/ISearchService.cs b/LearningHub.Nhs.WebUI/Interfaces/ISearchService.cs index 3f083bc38..7c9d7b59f 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/ISearchService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/ISearchService.cs @@ -20,6 +20,15 @@ public interface ISearchService /// A representing the result of the asynchronous operation. Task PerformSearch(IPrincipal user, SearchRequestViewModel searchRequest); + /// + /// Performs a search - either a combined resource and catalogue search, or just a resource search if + /// searching within a catalogue. + /// + /// User. + /// The SearchRequestViewModel. + /// A representing the result of the asynchronous operation. + Task PerformSearchInFindwise(IPrincipal user, SearchRequestViewModel searchRequest); + /// /// Records the analytics events associated with a search. /// diff --git a/LearningHub.Nhs.WebUI/Services/SearchService.cs b/LearningHub.Nhs.WebUI/Services/SearchService.cs index a10c1db1c..ef7a31707 100644 --- a/LearningHub.Nhs.WebUI/Services/SearchService.cs +++ b/LearningHub.Nhs.WebUI/Services/SearchService.cs @@ -1,4 +1,4 @@ -namespace LearningHub.Nhs.WebUI.Services +namespace LearningHub.Nhs.WebUI.Services { using System; using System.Collections.Generic; @@ -86,6 +86,226 @@ public async Task PerformSearch(IPrincipal user, SearchRe ResourceAccessLevelFilterText = searchRequest.ResourceAccessLevelId.HasValue && searchRequest.ResourceAccessLevelId != (int)ResourceAccessibilityEnum.None ? $"&resource_access_level={searchRequest.ResourceAccessLevelId.Value}" : string.Empty, }; + SearchViewModel resourceResult = null; + var resourceCollectionFilter = new List(); + + if (searchString != string.Empty) + { + var resourceResultTask = this.GetSearchResultAsync(resourceSearchRequestModel); + + if (searchRequest.CatalogueId.HasValue) + { + // Search within a catalogue - resources only. + await resourceResultTask; + resourceResult = resourceResultTask.Result; + } + else + { + await Task.WhenAll(resourceResultTask); + + resourceResult = resourceResultTask.Result; + + // Did you mean suggestion when no hits found + if (resourceResult?.TotalHits == 0 && (resourceResult?.Spell?.Suggestions?.Count > 0)) + { + didYouMeanEnabled = true; + + // pass the spell suggestion as new search text - resources + if (resourceResult?.Spell?.Suggestions?.Count > 0) + { + resourceSearchRequestModel.SearchText = Regex.Replace(resourceResult?.Spell?.Suggestions?.FirstOrDefault().ToString(), "<.*?>", string.Empty); + suggestedResource = resourceSearchRequestModel.SearchText; + + // calling findwise endpoint with new search text - resources + resourceResultTask = this.GetSearchResultAsync(resourceSearchRequestModel); + } + + await Task.WhenAll(resourceResultTask); + + resourceResult = resourceResultTask.Result; + } + } + + var searchfilters = new List(); + var resourceAccessLevelFilters = new List(); + + var providerfilters = new List(); + + if (resourceResult != null && resourceResult.Facets != null && resourceResult.Facets.Length > 0) + { + var filters = resourceResult.Facets.Where(x => x.Id == "resource_type").First().Filters; + + foreach (var filteritem in filters.Select(x => x.DisplayName.ToLower()).Distinct()) + { + var filter = filters.Where(x => x.DisplayName == filteritem).FirstOrDefault(); + + if (filter != null && UtilityHelper.FindwiseResourceTypeDict.ContainsKey(filter.DisplayName)) + { + var resourceTypeEnum = UtilityHelper.FindwiseResourceTypeDict[filter.DisplayName]; + var searchfilter = new SearchFilterModel() { DisplayName = UtilityHelper.GetPrettifiedResourceTypeName(resourceTypeEnum), Count = filter.Count, Value = filteritem, Selected = searchRequest.Filters?.Contains(filter.DisplayName) ?? false }; + searchfilters.Add(searchfilter); + } + } + + if (user.IsInRole("BasicUser")) + { + var accessLevelFilters = resourceResult.Facets.Where(x => x.Id == "resource_access_level").First().Filters; + + var generalAccessValue = (int)ResourceAccessibilityEnum.GeneralAccess; + var basicUserAudienceFilterItem = accessLevelFilters.Where(x => x.DisplayName == generalAccessValue.ToString()).FirstOrDefault(); + var basicResourceAccesslevelCount = basicUserAudienceFilterItem?.Count ?? 0; + var basicUserAudienceFilter = new SearchFilterModel() { DisplayName = ResourceAccessLevelHelper.GetPrettifiedResourceAccessLevelOptionDisplayName(ResourceAccessibilityEnum.GeneralAccess), Count = basicResourceAccesslevelCount, Value = generalAccessValue.ToString(), Selected = (searchRequest.ResourceAccessLevelId ?? 0) == generalAccessValue }; + resourceAccessLevelFilters.Add(basicUserAudienceFilter); + } + + filters = resourceResult.Facets.Where(x => x.Id == "provider_ids").First().Filters; + + if (filters.Length > 0) + { + var providers = await this.providerService.GetProviders(); + var provider_ids = providers.Select(n => n.Id).ToList(); + + foreach (var filteritem in filters.Select(x => x.DisplayName.ToLower()).Distinct()) + { + var filter = filters.Where(x => x.DisplayName == filteritem).FirstOrDefault(); + + if (filter != null && provider_ids.Contains(Convert.ToInt32(filter.DisplayName))) + { + var provider = providers.Where(n => n.Id == Convert.ToInt32(filter.DisplayName)).FirstOrDefault(); + + var searchfilter = new SearchFilterModel() { DisplayName = provider.Name, Count = filter.Count, Value = filteritem, Selected = searchRequest.ProviderFilters?.Contains(filter.DisplayName) ?? false }; + providerfilters.Add(searchfilter); + } + } + } + + // Process resource_collection facets + var collectionFacet = resourceResult.Facets.FirstOrDefault(x => x.Id == "resource_collection"); + if (collectionFacet != null && collectionFacet.Filters != null) + { + foreach (var filteritem in collectionFacet.Filters.Select(x => x.DisplayName).Distinct()) + { + var filter = collectionFacet.Filters.Where(x => x.DisplayName == filteritem).FirstOrDefault(); + + if (filter != null && !string.IsNullOrEmpty(filter.DisplayName)) + { + var searchfilter = new SearchFilterModel() + { + DisplayName = filter.DisplayName, + Count = filter.Count, + Value = filter.DisplayName.ToLower(), + Selected = searchRequest.ResourceCollectionFilter?.Contains(filter.DisplayName, StringComparer.OrdinalIgnoreCase) ?? false, + }; + resourceCollectionFilter.Add(searchfilter); + } + } + + if (resourceCollectionFilter.Any()) + { + // Sum of all counts + var allCount = resourceCollectionFilter.Sum(x => x.Count); + + // If none selected ➜ All is selected + // If ALL child filters are selected → All is selected + var allSelected = + !resourceCollectionFilter.Any(x => x.Selected) || + resourceCollectionFilter.All(x => x.Selected); + + // Create the ALL filter + var allFilter = new SearchFilterModel + { + DisplayName = "All", + Value = "all", + Count = allCount, + Selected = allSelected, + }; + + // Insert at top + resourceCollectionFilter.Insert(0, allFilter); + } + } + } + + resourceResult.SortItemList = searchSortItemList; + resourceResult.SortItemSelected = selectedSortItem; + resourceResult.SearchFilters = searchfilters; + resourceResult.SearchResourceAccessLevelFilters = resourceAccessLevelFilters; + resourceResult.SearchProviderFilters = providerfilters; + } + + var searchResultViewModel = new SearchResultViewModel + { + SearchString = searchString, + GroupId = groupId, + FeedbackSubmitted = searchRequest.FeedbackSubmitted ?? false, + ResourceCurrentPageIndex = searchRequest.ResourcePageIndex ?? 0, + CatalogueCurrentPageIndex = searchRequest.CataloguePageIndex ?? 0, + ResourceSearchResult = resourceResult, + + // SearchCollectionFilters = resourceCollectionFilters, + ResourceResultPaging = new SearchResultPagingModel + { + CurrentPage = searchRequest.ResourcePageIndex ?? 0, + PageSize = resourceSearchPageSize, + TotalItems = resourceResult?.TotalHits ?? 0, + }, + + CatalogueResultPaging = new SearchResultPagingModel + { + CurrentPage = searchRequest.CataloguePageIndex ?? 0, + PageSize = catalogueSearchPageSize, + }, + DidYouMeanEnabled = didYouMeanEnabled, + SuggestedCatalogue = suggestedCatalogue, + SuggestedResource = suggestedResource, + }; + + searchResultViewModel.ResourceCollectionFilter = resourceCollectionFilter; + + return searchResultViewModel; + } + + /// + /// Performs a search - either a combined resource and catalogue search, or just a resource search if + /// searching within a catalogue. + /// + /// user. + /// The SearchRequestViewModel. + /// A representing the result of the asynchronous operation. + public async Task PerformSearchInFindwise(IPrincipal user, SearchRequestViewModel searchRequest) + { + var searchSortType = 0; + if (searchRequest.Sortby.HasValue && Enum.IsDefined(typeof(SearchSortTypeEnum), searchRequest.Sortby)) + { + searchSortType = searchRequest.Sortby.Value; + } + + var searchString = searchRequest.Term?.Trim() ?? string.Empty; + var searchSortItemList = SearchHelper.GetSearchSortList(); + var selectedSortItem = searchSortItemList.Where(x => x.SearchSortType == (SearchSortTypeEnum)searchSortType).FirstOrDefault(); + var groupId = Guid.Parse(searchRequest.GroupId); + bool didYouMeanEnabled = false; + var suggestedCatalogue = string.Empty; + var suggestedResource = string.Empty; + + var resourceSearchPageSize = this.settings.FindwiseSettings.ResourceSearchPageSize; + var catalogueSearchPageSize = this.settings.FindwiseSettings.CatalogueSearchPageSize; + + var resourceSearchRequestModel = new SearchRequestModel + { + SearchId = searchRequest.SearchId.Value, + SearchText = searchString, + FilterText = this.BuildFilterText(searchRequest.Filters, searchRequest.ResourceCollectionFilter), + ProviderFilterText = searchRequest.ProviderFilters?.Any() == true ? $"&provider_ids={string.Join("&provider_ids=", searchRequest.ProviderFilters)}" : string.Empty, + SortColumn = selectedSortItem.Value, + SortDirection = selectedSortItem?.SortDirection, + PageIndex = searchRequest.ResourcePageIndex ?? 0, + PageSize = resourceSearchPageSize, + GroupId = groupId, + CatalogueId = searchRequest.CatalogueId, + ResourceAccessLevelFilterText = searchRequest.ResourceAccessLevelId.HasValue && searchRequest.ResourceAccessLevelId != (int)ResourceAccessibilityEnum.None ? $"&resource_access_level={searchRequest.ResourceAccessLevelId.Value}" : string.Empty, + }; + var catalogueSearchRequestModel = new CatalogueSearchRequestModel() { SearchId = searchRequest.SearchId.Value, diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml index 834ab8281..592f2620f 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml @@ -12,7 +12,6 @@ var pageUrl = Model.CatalogueId > 0 ? "/catalogue/" + Model.CatalogueUrl + "/search" : "/search/results"; var actionUrl = QueryHelpers.AddQueryString(pageUrl, queryParams); var pageFragment = "#search-filters"; - var selectedResourceCollection = "all"; string FilterSummary() { @@ -58,42 +57,55 @@ -
-
- - Show +
+
+ +

Show

- @foreach (var rc in Model.ResourceCollectionFilter) - { - if (rc.Selected) - selectedResourceCollection = rc.Value; + + @{ + // Get MULTIPLE selected values + var selectedResourceCollections = Model.ResourceCollectionFilter + .Where(x => x.Selected) + .Select(x => x.Value) + .ToList(); } -
-
- - -
-
- - -
-
- - -
-
- - -
+ +
+ @foreach (var option in new[] + { + new { Id = "all", Label = "All" }, + new { Id = "catalogue", Label = "Catalogues" }, + new { Id = "course", Label = "Courses" }, + new { Id = "resource", Label = "Learning Resources" } + }) + { + // Look up count from your real model + var filter = Model.ResourceCollectionFilter + .FirstOrDefault(x => x.Value == option.Id); + + var count = filter?.Count ?? 0; + +
+
+ + + + + +
+
+ }
From c877dbd6e4b9676ac7ce45f490aed84e7e3e0188 Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 24 Nov 2025 11:59:12 +0000 Subject: [PATCH 007/106] Refactored some of the codes, fixed sorting, and new catalogue layout --- .../LearningHub.Nhs.AdminUI.csproj | 2 +- ...rningHub.Nhs.WebUI.AutomatedUiTests.csproj | 2 +- .../Helpers/UtilityHelper.cs | 7 + .../Helpers/ViewActivityHelper.cs | 3 + .../LearningHub.Nhs.WebUI.csproj | 2 +- .../Views/Search/Index.cshtml | 13 +- .../Views/Search/_ResourceFilter.cshtml | 2 +- .../Search/_SearchCatalogueResult.cshtml | 62 +++++++ .../Views/Search/_SearchResult.cshtml | 157 +++++++++--------- .../LearningHub.Nhs.OpenApi.Models.csproj | 2 +- .../AzureSearch/SearchDocument.cs | 23 ++- ....Nhs.OpenApi.Repositories.Interface.csproj | 2 +- ...earningHub.Nhs.OpenApi.Repositories.csproj | 2 +- ...gHub.Nhs.OpenApi.Services.Interface.csproj | 2 +- .../Helpers/Search/SearchOptionsBuilder.cs | 63 +++++-- .../LearningHub.Nhs.OpenApi.Services.csproj | 2 +- .../AzureSearch/AzureSearchService.cs | 145 +++++++++------- .../LearningHub.Nhs.OpenApi.Tests.csproj | 2 +- .../Controllers/SearchController.cs | 8 +- .../LearningHub.NHS.OpenAPI.csproj | 2 +- 20 files changed, 311 insertions(+), 192 deletions(-) create mode 100644 LearningHub.Nhs.WebUI/Views/Search/_SearchCatalogueResult.cshtml diff --git a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj index 39e4b25da..b3a8d9b6b 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/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj index 695e93e2d..d846dfde5 100644 --- a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs index 762695dd9..337ced840 100644 --- a/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs @@ -25,13 +25,16 @@ public static class UtilityHelper { "article", ResourceTypeEnum.Article }, { "case", ResourceTypeEnum.Case }, { "weblink", ResourceTypeEnum.WebLink }, + { "web link", ResourceTypeEnum.WebLink }, { "audio", ResourceTypeEnum.Audio }, { "scorm", ResourceTypeEnum.Scorm }, { "assessment", ResourceTypeEnum.Assessment }, { "genericfile", ResourceTypeEnum.GenericFile }, + { "file", ResourceTypeEnum.GenericFile }, { "image", ResourceTypeEnum.Image }, { "html", ResourceTypeEnum.Html }, { "moodle", ResourceTypeEnum.Moodle }, + { "catalogue", ResourceTypeEnum.Catalogue }, }; /// @@ -146,6 +149,8 @@ public static string GetPrettifiedResourceTypeName(ResourceTypeEnum resourceType return "HTML"; case ResourceTypeEnum.Moodle: return "Course"; + case ResourceTypeEnum.Catalogue: + return "Catalogue"; default: return "File"; } @@ -186,6 +191,8 @@ public static string GetPrettifiedResourceTypeName(ResourceTypeEnum resourceType return "HTML"; case ResourceTypeEnum.Moodle: return "Course"; + case ResourceTypeEnum.Catalogue: + return "Catalogue"; default: return "File"; } diff --git a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs index a357fa371..70f813020 100644 --- a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs @@ -116,6 +116,9 @@ public static string GetResourceTypeDesc(ResourceTypeEnum resourceType) case ResourceTypeEnum.Moodle: typeText = "Course"; break; + case ResourceTypeEnum.Catalogue: + typeText = "Catalogue"; + break; default: typeText = string.Empty; break; diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index 39df64b2a..09cb454c3 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -113,7 +113,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml index 46cfcd84c..9783090ba 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/Index.cshtml @@ -56,15 +56,14 @@

} - @if (azureSearchEnabled) + @* @if (azureSearchEnabled) // [BY] Use this when we create quick filters for Azure Search { @await Html.PartialAsync("_SearchFilter", Model) - } - else - { - @await Html.PartialAsync("_ResourceFilter", Model) - } - + } *@ + + @await Html.PartialAsync("_ResourceFilter", Model) + +
diff --git a/LearningHub.Nhs.WebUI/Views/Search/_ResourceFilter.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_ResourceFilter.cshtml index 429eae962..b8bbc8807 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_ResourceFilter.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_ResourceFilter.cshtml @@ -46,7 +46,7 @@ }

- @($"{resourceResult.TotalHits} resource{(resourceResult.TotalHits == 1 ? string.Empty : "s")}") + @($"Showing {resourceResult.TotalHits} result{(resourceResult.TotalHits == 1 ? string.Empty : "s")}")

@if (resourceResult.TotalHits > 0) diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchCatalogueResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchCatalogueResult.cshtml new file mode 100644 index 000000000..f4eb7ab92 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchCatalogueResult.cshtml @@ -0,0 +1,62 @@ +@using System.Web; +@using LearningHub.Nhs.WebUI.Extensions +@using LearningHub.Nhs.Models.Search.SearchClick; + +@model LearningHub.Nhs.Models.Search.Document + +@{ + var item = Model; + var searchString = string.Empty;// HttpUtility.UrlEncode(Model.se); + var suggestedSearchString = string.Empty; /* Model.DidYouMeanEnabled ? HttpUtility.UrlEncode(Model.SuggestedCatalogue) : HttpUtility.UrlEncode (Model.SearchString);*/ + + string GetUrl(string catalogueUrl, int? nodePathId, int itemIndex, int catalogueId, SearchClickPayloadModel payload) + { + + var searchSignal = payload?.SearchSignal; + var pagingModel = "1";// Model.CatalogueResultPaging; + string encodedCatalogueUrl = HttpUtility.UrlEncode("/Catalogue/" + catalogueUrl); + string groupId = "binongid";// 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}&totalNumberOfHits={payload?.SearchSignal?.Stats?.TotalHits}&searchText={suggestedSearchString}&catalogueId={catalogueId} +&GroupId={groupId}&searchId={searchSignal?.SearchId}&timeOfSearch={searchSignal?.TimeOfSearch}&userQuery={HttpUtility.UrlEncode(searchSignal?.UserQuery)} +&query={searchSignalQueryEncoded}&name={payload?.DocumentFields?.Name}"; + return url; + } + // var catalogueResult = Model.ResourceSearchResult; + // var pagingModel = Model.CatalogueResultPaging; + // var searchString = HttpUtility.UrlEncode(Model.SearchString); + // var suggestedSearchString = Model.DidYouMeanEnabled ? HttpUtility.UrlEncode(Model.SuggestedCatalogue) : 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={suggestedSearchString}&catalogueId={catalogueId} + // &GroupId={groupId}&searchId={searchSignal?.SearchId}&timeOfSearch={searchSignal?.TimeOfSearch}&userQuery={HttpUtility.UrlEncode(searchSignal?.UserQuery)} + // &query={searchSignalQueryEncoded}&name={payload?.DocumentFields?.Name}"; + // return url; +} + +
+ +
+ +
Catalogue
+ +

+ @item.Title + +

+ +

+ @item.Description +

+ +
+
\ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml index 190f7eeb9..017579f06 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml @@ -30,21 +30,8 @@ string GetMoodleCourseUrl(string courseIdWithPrefix) { - const string prefix = "M"; - - if (string.IsNullOrWhiteSpace(courseIdWithPrefix)) - { - return string.Empty; - } - - if (!courseIdWithPrefix.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return string.Empty; - } - - var courseIdPart = courseIdWithPrefix.Substring(prefix.Length); - - if (int.TryParse(courseIdPart, out int courseId)) + + if (int.TryParse(courseIdWithPrefix, out int courseId)) { return moodleApiService.GetCourseUrl(courseId); } @@ -60,87 +47,95 @@ @foreach (var item in resourceResult.DocumentModel) { - var provider = item.Providers?.FirstOrDefault(); -
-

- @if (item.ResourceType == "moodle") - { - @item.Title - } - else - { - @item.Title - } -

- - @if (provider != null) - { -
-
- - @ProviderHelper.GetProviderString(provider.Name) -
-
- } - @if (item.CatalogueRestrictedAccess && !Model.HideRestrictedBadge && showCatalogueFieldsInResources) - { -

- @((item.CatalogueHasAccess || this.User.IsInRole("Administrator")) ? "Access Granted" : "Access restricted") -

- } + @if (item.ResourceType == "catalogue") + { + @await Html.PartialAsync("_SearchCatalogueResult", item) + } + else + { + var provider = item.Providers?.FirstOrDefault(); - @if (!resourceAccessLevelFilterSelected) - { -
-
- Audience access level: - @ResourceAccessLevelHelper.GetResourceAccessLevelText((ResourceAccessibilityEnum)item.ResourceAccessLevel) -
-
- } +
+

+ @if (item.ResourceType == "moodle") + { + @item.Title + } + else + { + @item.Title + } +

- @if (!string.IsNullOrEmpty(item.ResourceType)) - { -
-
- Type: - @UtilityHelper.GetPrettifiedResourceTypeName(UtilityHelper.ToEnum(item.ResourceType), 0) -
+ @if (provider != null) + {
- @if (item.ResourceType != "moodle") - { - @await Html.PartialAsync("../Shared/_StarRating.cshtml", item.Rating) - } +
+ + @ProviderHelper.GetProviderString(provider.Name) +
-
- } -

- @item.Description -

+ } + @if (item.CatalogueRestrictedAccess && !Model.HideRestrictedBadge && showCatalogueFieldsInResources) + { +

+ @((item.CatalogueHasAccess || this.User.IsInRole("Administrator")) ? "Access Granted" : "Access restricted") +

+ } -
- @if (!string.IsNullOrWhiteSpace(item.CatalogueBadgeUrl) && showCatalogueFieldsInResources) + @if (!resourceAccessLevelFilterSelected) { - Provider's catalogue badge +
+
+ Audience access level: + @ResourceAccessLevelHelper.GetResourceAccessLevelText((ResourceAccessibilityEnum)item.ResourceAccessLevel) +
+
} - @if (!string.IsNullOrEmpty(item.CatalogueName) && !this.Model.CatalogueId.HasValue && showCatalogueFieldsInResources) + @if (!string.IsNullOrEmpty(item.ResourceType)) { -
- @item.CatalogueName +
+
+ Type: + @UtilityHelper.GetPrettifiedResourceTypeName(UtilityHelper.ToEnum(item.ResourceType), 0) +
+
+ @if (item.ResourceType != "moodle") + { + @await Html.PartialAsync("../Shared/_StarRating.cshtml", item.Rating) + } +
} +

+ @item.Description +

-
- @UtilityHelper.GetAttribution(item.Authors) - @if (!string.IsNullOrEmpty(item.AuthoredDate)) +
+ @if (!string.IsNullOrWhiteSpace(item.CatalogueBadgeUrl) && showCatalogueFieldsInResources) { - @UtilityHelper.GetInOn(item.AuthoredDate) - @: @item.AuthoredDate + Provider's catalogue badge } + + @if (!string.IsNullOrEmpty(item.CatalogueName) && !this.Model.CatalogueId.HasValue && showCatalogueFieldsInResources) + { + + } + +
+ @UtilityHelper.GetAttribution(item.Authors) + @if (!string.IsNullOrEmpty(item.AuthoredDate)) + { + @UtilityHelper.GetInOn(item.AuthoredDate) + @: @item.AuthoredDate + } +
-
- index++; + index++; + } } \ No newline at end of file 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 9e3b45435..1735b0b62 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs index 7cae9f8b4..30b4bbe7c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs @@ -16,7 +16,6 @@ public class SearchDocument /// /// Gets or sets the unique identifier. /// - [SearchableField(IsKey = true)] [JsonPropertyName("id")] public string PrefixedId { get; set; } = string.Empty; @@ -31,7 +30,7 @@ public string Id if (string.IsNullOrWhiteSpace(PrefixedId)) return "0"; - var parts = PrefixedId.Split('_'); + var parts = PrefixedId.Split('-'); if (parts.Length != 2) return "0"; @@ -42,14 +41,12 @@ public string Id /// /// Gets or sets the title. /// - [SearchableField(IsSortable = true)] [JsonPropertyName("title")] public string Title { get; set; } = string.Empty; /// /// Gets or sets the description. /// - [SearchableField] [JsonPropertyName("description")] public string Description { @@ -60,63 +57,63 @@ public string Description /// /// Gets or sets the resource type. /// - [SearchableField(IsFilterable = true, IsFacetable = true)] [JsonPropertyName("resource_collection")] public string ResourceCollection { get; set; } = string.Empty; + /// + /// gets or sets the catalogue identifier. + /// + [JsonPropertyName("catalogue_id")] + public string CatalogueId { get; set; } = string.Empty; + + [JsonPropertyName("resource_reference_id")] + public string ResourceReferenceId { get; set; } = string.Empty; + /// /// Gets or sets the manual tag JSON. /// - [SearchableField(IsFilterable = true, IsFacetable = true)] [JsonPropertyName("manual_tag")] public string ManualTagJson { get; set; } = string.Empty; /// /// Gets or sets the manual tags. /// - [SearchableField(IsFilterable = true, IsFacetable = true)] [JsonPropertyName("manualTags")] public List ManualTags { get; set; } = new List(); /// /// Gets or sets the content type. /// - [SearchableField(IsFilterable = true, IsFacetable = true)] [JsonPropertyName("resource_type")] public string? ResourceType { get; set; } = string.Empty; /// /// Gets or sets the date authored. /// - [SimpleField(IsFilterable = true, IsSortable = true)] [JsonPropertyName("date_authored")] public DateTime? DateAuthored { get; set; } /// /// Gets or sets the rating. /// - [SimpleField(IsFilterable = true, IsSortable = true)] [JsonPropertyName("rating")] public double? Rating { get; set; } /// /// Gets or sets the provider IDs. /// - [SearchableField] [JsonPropertyName("provider_ids")] public string ProviderIds { get; set; } = string.Empty; /// /// Gets or sets a value indicating whether this is statutory mandatory. /// - [SimpleField(IsFilterable = true, IsFacetable = true)] [JsonPropertyName("statutory_mandatory")] public bool? StatutoryMandatory { get; set; } /// /// Gets or sets the author. /// - [SearchableField] [JsonPropertyName("author")] public string Author { get; set; } = string.Empty; diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj index a52fd6bf1..eb117ebe3 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj index 46722121a..c6d370145 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj @@ -24,7 +24,7 @@ - + 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 f6e92a8ce..ceef6ce67 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 @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs index 3e36710e2..7f0f2fedc 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs @@ -1,4 +1,4 @@ -namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search +namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search { using System; using System.Collections.Generic; @@ -37,15 +37,7 @@ public static SearchOptions BuildSearchOptions( ScoringProfile = "boostExactTitle" }; - string? columns = sortBy?.Keys.FirstOrDefault(); - string? directions = sortBy?.Values.FirstOrDefault(); - string sortDirection = "asc"; - - string sortColumn = columns ?? "relevance"; - if (directions?.StartsWith("desc", StringComparison.OrdinalIgnoreCase) ?? false) - { - sortDirection = "desc"; - } + string sortByFinal = GetSortOption(sortBy); // Configure query type if (searchQueryType == SearchQueryType.Semantic) @@ -60,12 +52,12 @@ public static SearchOptions BuildSearchOptions( { searchOptions.QueryType = SearchQueryType.Simple; searchOptions.SearchMode = SearchMode.Any; - searchOptions.OrderBy.Add($"{sortColumn} {sortDirection}"); + searchOptions.OrderBy.Add(sortByFinal); } else { searchOptions.QueryType = SearchQueryType.Full; - searchOptions.OrderBy.Add($"{sortColumn} {sortDirection}"); + searchOptions.OrderBy.Add(sortByFinal); } // Add facets @@ -84,5 +76,52 @@ public static SearchOptions BuildSearchOptions( return searchOptions; } + + private static string GetSortOption(Dictionary? sortBy) + { + // If null or empty → Azure Search will default to relevance + if (sortBy == null || sortBy.Count == 0) + return string.Empty; + + // Extract key/value (only first pair used) + string? uiSortKey = sortBy.Keys.FirstOrDefault(); + string? directionInput = sortBy.Values.FirstOrDefault(); + + // Handle empty key → no sorting + if (string.IsNullOrWhiteSpace(uiSortKey)) + return string.Empty; + + // Determine direction safely + string sortDirection = + directionInput != null && + directionInput.StartsWith("desc", StringComparison.OrdinalIgnoreCase) + ? "desc" + : "asc"; + + // Map UI values to search fields + string? sortColumn = uiSortKey.Trim().ToLowerInvariant() switch + { + "avgrating" => "rating", + "rating" => "rating", + + "authored_date" => "date_authored", + "authoreddate" => "date_authored", + "authoredDate" => "date_authored", + + "title" => "title", + "atoz" => "title", + "alphabetical" => "title", + "ztoa" => "title", + + _ => null // unknown sort → ignore + }; + + // No valid mapping → fall back to relevance + if (string.IsNullOrWhiteSpace(sortColumn)) + return string.Empty; + + return $"{sortColumn} {sortDirection}"; + } + } } 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 2c8c04990..d485036ba 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -31,7 +31,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 6f7188f4a..6db310df7 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -85,7 +85,7 @@ public async Task GetSearchResultAsync(SearchRequestModel sea try { - var searchQueryType = SearchQueryType.Semantic; + var searchQueryType = SearchQueryType.Full; var pageSize = searchRequestModel.PageSize; var offset = searchRequestModel.PageIndex * pageSize; @@ -106,31 +106,26 @@ public async Task GetSearchResultAsync(SearchRequestModel sea SearchResults filteredResponse = await this.searchClient.SearchAsync(query, searchOptions, cancellationToken); // Execute filtered search and unfiltered facet query in parallel - var filteredSearchTask1 = ExecuteSearchAsync( - query, - searchQueryType, - offset, - pageSize, - filters, - sortBy, - includeFacets: true, - cancellationToken); + //var filteredSearchTask1 = ExecuteSearchAsync( + // query, + // searchQueryType, + // offset, + // pageSize, + // filters, + // sortBy, + // includeFacets: true, + // cancellationToken); //var unfilteredFacetsTask = GetUnfilteredFacetsAsync( // searchRequestModel.SearchText, // searchQueryType, // cancellationToken); - // await Task.WhenAll(filteredSearchTask); + // await Task.WhenAll(filteredSearchTask); - // var filteredResponse = await filteredSearchTask; + // var filteredResponse = await filteredSearchTask; //var unfilteredFacets = await unfilteredFacetsTask; - - var unfilteredFacets = await GetUnfilteredFacetsAsync( - searchRequestModel.SearchText, - filteredResponse.Facets, - cancellationToken); - + // Map documents var documents = filteredResponse.GetResults() .Select(result => @@ -143,13 +138,26 @@ public async Task GetSearchResultAsync(SearchRequestModel sea Id = doc.Id, Title = doc.Title, Description = doc.Description, - ResourceType = doc.ResourceType?.Contains("SCORM e-learning resource") == true ? "scorm" : doc.ResourceType, + ResourceType = MapToResourceType(doc.ResourceType), ProviderIds = doc.ProviderIds?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(), - CatalogueIds = new List(1), + CatalogueIds = + doc.ResourceType == "catalogue" + ? new List { Convert.ToInt32(doc.Id) } // convert single id → List + : ( + doc.CatalogueId? + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(id => int.TryParse(id, out var val) ? val : 0) + .ToList() + ?? new List() + ), + // CatalogueIds = doc.CatalogueId?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(id => int.TryParse(id, out var val) ? val : 0).ToList() ?? new List(), + //CatalogueIds = doc.CatalogueId?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(), + //CatalogueIds = new List(1), Rating = Convert.ToDecimal(doc.Rating), Author = doc.Author, Authors = doc.Author?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(a => a.Trim()).ToList(), AuthoredDate = doc.DateAuthored?.ToString(), + ResourceReferenceId = int.TryParse(doc.ResourceReferenceId, out var id) ? id : 0, Click = new SearchClickModel { Payload = new SearchClickPayloadModel { HitNumber = 1 }, Url = "binon" } }; }) @@ -160,6 +168,11 @@ public async Task GetSearchResultAsync(SearchRequestModel sea Documents = documents.ToArray() }; + var unfilteredFacets = await GetUnfilteredFacetsAsync( + searchRequestModel.SearchText, + filteredResponse.Facets, + cancellationToken); + // Merge facets from filtered and unfiltered results viewmodel.Facets = AzureSearchFacetHelper.MergeFacets(filteredResponse.Facets, unfilteredFacets, filters); @@ -179,6 +192,28 @@ public async Task GetSearchResultAsync(SearchRequestModel sea } } + private string MapToResourceType(string resourceType) + { + if (string.IsNullOrWhiteSpace(resourceType)) + return ResourceTypeEnum.Undefined.ToString(); + + string cleanedResourceType = resourceType + .Trim() + .ToLower() + .Replace(" ", "") + .Replace("_", "") + .Replace("-", ""); + + if (cleanedResourceType.StartsWith("scorm")) + cleanedResourceType = ResourceTypeEnum.Scorm.ToString(); + else if (cleanedResourceType.StartsWith("web")) + cleanedResourceType = ResourceTypeEnum.WebLink.ToString(); + else if (cleanedResourceType.Contains("file")) + cleanedResourceType = ResourceTypeEnum.GenericFile.ToString(); + + return cleanedResourceType; + } + /// /// Executes a search query with the specified parameters. /// @@ -191,19 +226,19 @@ public async Task GetSearchResultAsync(SearchRequestModel sea /// Whether to include facets in the results. /// Cancellation token. /// The search results. - private async Task> ExecuteSearchAsync( - string query, - SearchQueryType searchQueryType, - int offset, - int pageSize, - Dictionary> filters, - Dictionary searchBy, - bool includeFacets, - CancellationToken cancellationToken) - { - var searchOptions = BuildSearchOptions(searchQueryType, offset, pageSize, filters, searchBy, includeFacets); - return await this.searchClient.SearchAsync(query, searchOptions, cancellationToken); - } + //private async Task> ExecuteSearchAsync( + // string query, + // SearchQueryType searchQueryType, + // int offset, + // int pageSize, + // Dictionary> filters, + // Dictionary searchBy, + // bool includeFacets, + // CancellationToken cancellationToken) + //{ + // var searchOptions = BuildSearchOptions(searchQueryType, offset, pageSize, filters, searchBy, includeFacets); + // return await this.searchClient.SearchAsync(query, searchOptions, cancellationToken); + //} /// /// Builds search options for Azure Search queries. @@ -261,7 +296,7 @@ private async Task>> GetUnfilteredFacetsA } return facets ?? new Dictionary>(); - } + } /// public async Task GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId, CancellationToken cancellationToken = default) @@ -554,10 +589,10 @@ public async Task RemoveResourceFromSearchAsync(int resourceId) { try { - var adminClient = AzureSearchClientFactory.CreateAdminClient(this.azureSearchConfig); + // We are not currently implementing delete in Azure Search, it is handled via data source indexer + // var adminClient = AzureSearchClientFactory.CreateAdminClient(this.azureSearchConfig); - await adminClient.DeleteDocumentsAsync("id", new[] { resourceId.ToString() }); - this.logger.LogInformation($"Resource {resourceId} removed from Azure Search index"); + // await adminClient.DeleteDocumentsAsync("id", new[] { resourceId.ToString() }); } catch (Exception ex) { @@ -571,25 +606,11 @@ public async Task SendResourceForSearchAsync(SearchResourceRequestModel se { try { - var adminClient = AzureSearchClientFactory.CreateAdminClient(this.azureSearchConfig); - - // Map SearchResourceRequestModel to PocSearchDocument - var document = new Models.ServiceModels.AzureSearch.SearchDocument - { - // TODO: [BY] - // Id = searchResourceRequestModel.Id.ToString(), - // Title = searchResourceRequestModel.Title ?? string.Empty, - - //Description = searchResourceRequestModel.Description ?? string.Empty, - //ResourceType = searchResourceRequestModel.ResourceType ?? string.Empty, - //ContentType = searchResourceRequestModel.ContentType, - //DateAuthored = searchResourceRequestModel.DateAuthored, - //Rating = searchResourceRequestModel.Rating, - //Author = searchResourceRequestModel.Author ?? string.Empty, - }; + // We are not currently implementing in Azure Search, it is handled via data source indexer - await adminClient.IndexDocumentsAsync(IndexDocumentsBatch.Upload(new[] { document })); - this.logger.LogInformation($"Resource {searchResourceRequestModel.Id} added to Azure Search index"); + // var adminClient = AzureSearchClientFactory.CreateAdminClient(this.azureSearchConfig); + // var document = new Models.ServiceModels.AzureSearch.SearchDocument{}; + // await adminClient.IndexDocumentsAsync(IndexDocumentsBatch.Upload(new[] { document })); return true; } catch (Exception ex) @@ -602,9 +623,7 @@ public async Task SendResourceForSearchAsync(SearchResourceRequestModel se /// public async Task SendCatalogueSearchEventAsync(SearchActionCatalogueModel searchActionCatalogueModel) { - // Azure Search doesn't need click tracking like Findwise - // Log the event but return true - this.logger.LogInformation($"Catalogue search click event logged for catalogue {searchActionCatalogueModel.CatalogueId}"); + // We are not currently implementing in Azure Search, it is handled via data source indexer return await Task.FromResult(true); } @@ -618,7 +637,7 @@ public async Task GetAllCatalogueSearchResultsAsy var offset = catalogSearchRequestModel.PageIndex * catalogSearchRequestModel.PageSize; var filters = new Dictionary> { - { "resource_collection", new List { "Catalogue" } } + { "resource_collection", new List { "catalogue" } } }; var searchOptions = new SearchOptions @@ -750,7 +769,7 @@ public async Task GetAutoSuggestionResultsAsync(string term { TotalHits = suggestResults.Count(), ResourceDocumentList = suggestResults - .Where(a => a.Type == "Resource") + .Where(a => a.Type == "resource") .Select(item => new AutoSuggestionResourceDocument { Id = item.Id?.Substring(1), @@ -765,7 +784,7 @@ public async Task GetAutoSuggestionResultsAsync(string term { TotalHits = suggestResults.Count(), CatalogueDocumentList = suggestResults - .Where(a => a.Type == "Catalogue") + .Where(a => a.Type == "catalogue") .Select(item => new AutoSuggestionCatalogueDocument { Id = item.Id?.Substring(1), @@ -806,9 +825,7 @@ public async Task GetAutoSuggestionResultsAsync(string term /// public async Task SendAutoSuggestionEventAsync(AutoSuggestionClickPayloadModel clickPayloadModel) { - // Azure Search doesn't need click tracking like Findwise - // Log the event but return true - this.logger.LogInformation("Auto-suggestion click event logged"); + // We are not currently implementing in Azure Search, it is handled via data source indexer return await Task.FromResult(true); } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj index 35c780917..ac9eaae79 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/SearchController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/SearchController.cs index 2b463125b..d90ab47c0 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/SearchController.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/SearchController.cs @@ -271,7 +271,7 @@ private async Task GetSearchResults(SearchRequestModel searchRe { var results = await this.searchService.GetSearchResultAsync(searchRequestModel, this.CurrentUserId.GetValueOrDefault()); var documents = results.DocumentList.Documents.ToList(); - var catalogueIds = results.DocumentList.Documents.Select(x => x.CatalogueIds.FirstOrDefault()).Where(x => x != 0).ToHashSet().ToList(); + var catalogueIds = results.DocumentList.Documents.Select(x => x.CatalogueIds?.FirstOrDefault() ?? 0).Where(id => id != 0).ToHashSet().ToList(); var catalogues = this.catalogueService.GetCataloguesByNodeId(catalogueIds); var allProviders = await this.providerService.GetAllAsync(); @@ -282,12 +282,12 @@ private async Task GetSearchResults(SearchRequestModel searchRe document.Providers = allProviders.Where(n => document.ProviderIds.Contains(n.Id)).ToList(); } - if (document.CatalogueIds.Any(x => x == 1)) + if (document.CatalogueIds?.Any(x => x == 1) == true) { continue; } - var catalogue = catalogues.SingleOrDefault(x => x.NodeId == document.CatalogueIds.SingleOrDefault()); + var catalogue = catalogues.SingleOrDefault(x => x.NodeId == document.CatalogueIds?.SingleOrDefault() == true); if (catalogue == null) { @@ -345,7 +345,7 @@ private async Task GetSearchResults(SearchRequestModel searchRe var relatedCatalogueIds = new List(); foreach (var document in results.DocumentList.Documents) { - foreach (int catalogueId in document.CatalogueIds) + foreach (int catalogueId in document.CatalogueIds ?? Enumerable.Empty()) { if (relatedCatalogueIds.IndexOf(catalogueId) == -1) { diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj index f23f74ba3..34579b31b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj @@ -19,7 +19,7 @@ - + From d6ffff191d6f799df9a8524985b1b66f157ce411 Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 24 Nov 2025 15:45:15 +0000 Subject: [PATCH 008/106] Minor fix --- .../Helpers/Search/SearchFilterBuilder.cs | 88 ++++++++++++++++--- .../AzureSearch/AzureSearchService.cs | 62 +------------ 2 files changed, 81 insertions(+), 69 deletions(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs index 10868b817..b31344d26 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs @@ -1,4 +1,4 @@ -namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search +namespace LearningHub.Nhs.OpenApi.Services.Helpers.Search { using System; using System.Collections.Generic; @@ -15,7 +15,7 @@ public static Dictionary> CombineAndNormaliseFilters(string { var filters = new Dictionary> { - // { "resource_collection", new List { "Resource" } } + // { "resource_collection", new List { "Resource" } } }; // Parse and merge additional filters from query string @@ -26,7 +26,7 @@ public static Dictionary> CombineAndNormaliseFilters(string MergeFilterDictionary(filters, requestTypeFilters); // MergeFilterDictionary(filters, providerFilters); - NormaliseFilters(filters); + //NormaliseFilters(filters); return filters; } @@ -47,25 +47,93 @@ private static void MergeFilterDictionary(Dictionary> targe /// /// The filters to apply. /// The filter expression string. - public static string BuildFilterExpression(Dictionary>? filters) + /// + /// Build an OData filter that supports multi-select values. + /// Pass a dictionary where key = field name, value = list of selected values. + /// If `collectionFields` contains a field name, that field will be treated as a collection and use any(...). + /// + public static string BuildFilterExpression( + Dictionary>? filters, + ISet? collectionFields = null) { if (filters == null || !filters.Any()) return string.Empty; - var filterExpressions = new List(); + collectionFields ??= new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var filter in filters) + // Handle spacing, NBSP, escaping quotes + string Normalize(string v) { - if (filter.Value?.Any() == true) + if (v == null) return string.Empty; + + // Replace NBSP + v = v.Replace('\u00A0', ' ').Trim(); + + // Collapse multiple spaces + v = System.Text.RegularExpressions.Regex.Replace(v, @"\s+", " "); + + // Escape single quotes for OData + v = v.Replace("'", "''"); + + return v; + } + + var expressions = new List(); + + foreach (var kvp in filters) + { + var field = kvp.Key; + var values = kvp.Value?.Where(v => !string.IsNullOrWhiteSpace(v)).ToList(); + + if (values == null || values.Count == 0) + continue; + + // Normalize all values + var normalizedValues = values.Select(Normalize).Distinct().ToList(); + + // Single value → use eq + if (normalizedValues.Count == 1) + { + var v = normalizedValues[0]; + + if (collectionFields.Contains(field)) + { + expressions.Add($"{field}/any(t: t eq '{v}')"); + } + else + { + expressions.Add($"{field} eq '{v}'"); + } + + continue; + } + + // Multiple values → use OR conditions (ALWAYS works) + if (collectionFields.Contains(field)) { - var values = string.Join(",", filter.Value); - filterExpressions.Add($"search.in({filter.Key}, '{values}')"); + // collection field (array) → OR any(...) conditions + var ors = normalizedValues + .Select(v => $"{field}/any(t: t eq '{v}')"); + + expressions.Add("(" + string.Join(" or ", ors) + ")"); + } + else + { + // single string field → OR eq conditions + var ors = normalizedValues + .Select(v => $"{field} eq '{v}'"); + + expressions.Add("(" + string.Join(" or ", ors) + ")"); } } - return filterExpressions.Any() ? string.Join(" and ", filterExpressions) : string.Empty; + return expressions.Count > 0 + ? string.Join(" and ", expressions) + : string.Empty; } + + /// /// Parses filter parameters from a query string. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 6db310df7..b66fe9aa0 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -142,7 +142,7 @@ public async Task GetSearchResultAsync(SearchRequestModel sea ProviderIds = doc.ProviderIds?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(), CatalogueIds = doc.ResourceType == "catalogue" - ? new List { Convert.ToInt32(doc.Id) } // convert single id → List + ? new List { Convert.ToInt32(doc.Id) } : ( doc.CatalogueId? .Split(',', StringSplitOptions.RemoveEmptyEntries) @@ -150,10 +150,7 @@ public async Task GetSearchResultAsync(SearchRequestModel sea .ToList() ?? new List() ), - // CatalogueIds = doc.CatalogueId?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(id => int.TryParse(id, out var val) ? val : 0).ToList() ?? new List(), - //CatalogueIds = doc.CatalogueId?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(), - //CatalogueIds = new List(1), - Rating = Convert.ToDecimal(doc.Rating), + Rating = Convert.ToDecimal(doc.Rating), Author = doc.Author, Authors = doc.Author?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(a => a.Trim()).ToList(), AuthoredDate = doc.DateAuthored?.ToString(), @@ -213,60 +210,7 @@ private string MapToResourceType(string resourceType) return cleanedResourceType; } - - /// - /// Executes a search query with the specified parameters. - /// - /// The search query text. - /// The type of search query. - /// The number of results to skip. - /// The number of results to return. - /// The filters to apply. - /// The sort to apply. - /// Whether to include facets in the results. - /// Cancellation token. - /// The search results. - //private async Task> ExecuteSearchAsync( - // string query, - // SearchQueryType searchQueryType, - // int offset, - // int pageSize, - // Dictionary> filters, - // Dictionary searchBy, - // bool includeFacets, - // CancellationToken cancellationToken) - //{ - // var searchOptions = BuildSearchOptions(searchQueryType, offset, pageSize, filters, searchBy, includeFacets); - // return await this.searchClient.SearchAsync(query, searchOptions, cancellationToken); - //} - - /// - /// Builds search options for Azure Search queries. - /// - /// The type of search query. - /// The number of results to skip. - /// The number of results to return. - /// The filters to apply. - /// The sort to apply. - /// Whether to include facets. - /// The configured search options. - private SearchOptions BuildSearchOptions( - SearchQueryType searchQueryType, - int offset, - int pageSize, - Dictionary> filters, - Dictionary sortBy, - bool includeFacets) - { - return SearchOptionsBuilder.BuildSearchOptions( - searchQueryType, - offset, - pageSize, - filters, - sortBy, - includeFacets); - } - + /// /// Gets unfiltered facets for a search term, using caching. /// From 9f72b2b90c53849965ea64ed029c50d31af7a5ae Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 24 Nov 2025 23:36:52 +0000 Subject: [PATCH 009/106] Updated the admin UI to use azure search flag and refactored Azuresearchservice.cs --- .../Helpers/FeatureFlags.cs | 5 + .../Views/Shared/_NavPartial.cshtml | 20 +- .../LearningHub.Nhs.AdminUI/appsettings.json | 3 +- .../Configuration/AzureSearchConfig.cs | 5 + .../Helpers/Search/SearchOptionsBuilder.cs | 21 ++ .../AzureSearch/AzureSearchService.cs | 210 ++++++++++-------- .../LearningHub.Nhs.OpenApi/appsettings.json | 1 + 7 files changed, 165 insertions(+), 100 deletions(-) diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Helpers/FeatureFlags.cs b/AdminUI/LearningHub.Nhs.AdminUI/Helpers/FeatureFlags.cs index 24e9e3be9..a304e49d6 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Helpers/FeatureFlags.cs +++ b/AdminUI/LearningHub.Nhs.AdminUI/Helpers/FeatureFlags.cs @@ -14,5 +14,10 @@ public static class FeatureFlags /// The DisplayAudioVideo. ///
public const string DisplayAudioVideo = "DisplayAudioVideo"; + + /// + /// The AzureSearch. + /// + public const string AzureSearch = "AzureSearch"; } } diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_NavPartial.cshtml b/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_NavPartial.cshtml index 191e87560..169e30fd4 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_NavPartial.cshtml +++ b/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_NavPartial.cshtml @@ -1,6 +1,9 @@ -@using Microsoft.Extensions.Options; +@using Microsoft.Extensions.Options; @using LearningHub.Nhs.AdminUI.Configuration; +@using LearningHub.Nhs.AdminUI.Helpers; +@using Microsoft.FeatureManagement; @inject IOptions webSettings +@inject IFeatureManager featureManager @{ var mainMenu = "Home"; @@ -13,7 +16,7 @@ case "externalsystem": case "log": case "roadmap": - case "cms": + case "cms": case "release": case "cache": mainMenu = "Settings"; @@ -44,7 +47,6 @@
-
@@ -81,9 +83,12 @@ - + @if (!await featureManager.IsEnabledAsync(FeatureFlags.AzureSearch)) + { + + } }
- - + \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/appsettings.json b/AdminUI/LearningHub.Nhs.AdminUI/appsettings.json index 17b29013c..31e99fff0 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/appsettings.json +++ b/AdminUI/LearningHub.Nhs.AdminUI/appsettings.json @@ -73,6 +73,7 @@ "APPINSIGHTS_INSTRUMENTATIONKEY": "", "FeatureManagement": { "AddAudioVideo": true, - "DisplayAudioVideo": true + "DisplayAudioVideo": true, + "AzureSearch": false } } \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs index 109a970c0..cf050977c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs @@ -44,5 +44,10 @@ public class AzureSearchConfig /// Gets or sets the suggester name for auto-complete and suggestions. ///
public string SuggesterName { get; set; } = "test-search-suggester"; + + /// + /// Gets or sets the search query type (semantic, full, or simple). + /// + public string SearchQueryType { get; set; } = "Semantic"; } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs index 7f0f2fedc..0f46b9068 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs @@ -123,5 +123,26 @@ private static string GetSortOption(Dictionary? sortBy) return $"{sortColumn} {sortDirection}"; } + /// + /// Parses the search query type from configuration string. + /// Parsing is case-insensitive. If the value is null, empty, or invalid, defaults to Semantic. + /// + /// The search query type string (semantic, full, or simple). + /// The parsed SearchQueryType enum value. + public static SearchQueryType ParseSearchQueryType(string searchQueryTypeString) + { + if (string.IsNullOrWhiteSpace(searchQueryTypeString)) + { + return SearchQueryType.Semantic; + } + + if (Enum.TryParse(searchQueryTypeString, ignoreCase: true, out var queryType) && + (queryType == SearchQueryType.Semantic || queryType == SearchQueryType.Full || queryType == SearchQueryType.Simple)) + { + return queryType; + } + + return SearchQueryType.Semantic; + } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index b66fe9aa0..42b89b96c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -1,15 +1,11 @@ namespace LearningHub.Nhs.OpenApi.Services.Services.AzureSearch { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; using AutoMapper; using Azure.Search.Documents; using Azure.Search.Documents.Models; using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Resource; + using LearningHub.Nhs.Models.Entities.Resource.Blocks; using LearningHub.Nhs.Models.Enums; using LearningHub.Nhs.Models.Search; using LearningHub.Nhs.Models.Search.SearchClick; @@ -26,6 +22,12 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using static Microsoft.EntityFrameworkCore.DbLoggerCategory; using Event = LearningHub.Nhs.Models.Entities.Analytics.Event; @@ -85,7 +87,7 @@ public async Task GetSearchResultAsync(SearchRequestModel sea try { - var searchQueryType = SearchQueryType.Full; + var searchQueryType = SearchOptionsBuilder.ParseSearchQueryType(this.azureSearchConfig.SearchQueryType); ; var pageSize = searchRequestModel.PageSize; var offset = searchRequestModel.PageIndex * pageSize; @@ -104,28 +106,8 @@ public async Task GetSearchResultAsync(SearchRequestModel sea var searchOptions = SearchOptionsBuilder.BuildSearchOptions(searchQueryType, offset, pageSize, filters, sortBy, true); SearchResults filteredResponse = await this.searchClient.SearchAsync(query, searchOptions, cancellationToken); + var count = Convert.ToInt32(filteredResponse.TotalCount); - // Execute filtered search and unfiltered facet query in parallel - //var filteredSearchTask1 = ExecuteSearchAsync( - // query, - // searchQueryType, - // offset, - // pageSize, - // filters, - // sortBy, - // includeFacets: true, - // cancellationToken); - - //var unfilteredFacetsTask = GetUnfilteredFacetsAsync( - // searchRequestModel.SearchText, - // searchQueryType, - // cancellationToken); - - // await Task.WhenAll(filteredSearchTask); - - // var filteredResponse = await filteredSearchTask; - //var unfilteredFacets = await unfilteredFacetsTask; - // Map documents var documents = filteredResponse.GetResults() .Select(result => @@ -142,7 +124,7 @@ public async Task GetSearchResultAsync(SearchRequestModel sea ProviderIds = doc.ProviderIds?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(), CatalogueIds = doc.ResourceType == "catalogue" - ? new List { Convert.ToInt32(doc.Id) } + ? new List { Convert.ToInt32(doc.Id) } : ( doc.CatalogueId? .Split(',', StringSplitOptions.RemoveEmptyEntries) @@ -150,12 +132,12 @@ public async Task GetSearchResultAsync(SearchRequestModel sea .ToList() ?? new List() ), - Rating = Convert.ToDecimal(doc.Rating), + Rating = Convert.ToDecimal(doc.Rating), Author = doc.Author, Authors = doc.Author?.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(a => a.Trim()).ToList(), AuthoredDate = doc.DateAuthored?.ToString(), ResourceReferenceId = int.TryParse(doc.ResourceReferenceId, out var id) ? id : 0, - Click = new SearchClickModel { Payload = new SearchClickPayloadModel { HitNumber = 1 }, Url = "binon" } + Click = BuildSearchClickModel(doc.Id, doc.Title, searchRequestModel.PageIndex, searchRequestModel.SearchId, filters, query, count) }; }) .ToList(); @@ -173,7 +155,7 @@ public async Task GetSearchResultAsync(SearchRequestModel sea // Merge facets from filtered and unfiltered results viewmodel.Facets = AzureSearchFacetHelper.MergeFacets(filteredResponse.Facets, unfilteredFacets, filters); - var count = Convert.ToInt32(filteredResponse.TotalCount); + viewmodel.Stats = new Stats { TotalHits = count @@ -189,59 +171,6 @@ public async Task GetSearchResultAsync(SearchRequestModel sea } } - private string MapToResourceType(string resourceType) - { - if (string.IsNullOrWhiteSpace(resourceType)) - return ResourceTypeEnum.Undefined.ToString(); - - string cleanedResourceType = resourceType - .Trim() - .ToLower() - .Replace(" ", "") - .Replace("_", "") - .Replace("-", ""); - - if (cleanedResourceType.StartsWith("scorm")) - cleanedResourceType = ResourceTypeEnum.Scorm.ToString(); - else if (cleanedResourceType.StartsWith("web")) - cleanedResourceType = ResourceTypeEnum.WebLink.ToString(); - else if (cleanedResourceType.Contains("file")) - cleanedResourceType = ResourceTypeEnum.GenericFile.ToString(); - - return cleanedResourceType; - } - - /// - /// Gets unfiltered facets for a search term, using caching. - /// - /// The search text. - /// The facet results. - /// Cancellation token. - /// The unfiltered facet results. - private async Task>> GetUnfilteredFacetsAsync( - string searchText, - IDictionary> facets, - CancellationToken cancellationToken) - { - var cacheKey = $"AllFacets_{searchText?.ToLowerInvariant() ?? "*"}"; - var cacheResponse = await this.cachingService.GetAsync>>(cacheKey); - - if (cacheResponse.ResponseEnum == CacheReadResponseEnum.Found) - { - // Convert cached DTO back to FacetResult dictionary - return AzureSearchFacetHelper.ConvertFromCacheable(cacheResponse.Item); - } - - if (facets != null) - { - // Convert to cacheable DTO before caching - var cacheableFacets = AzureSearchFacetHelper.ConvertToCacheable(facets); - await this.cachingService.SetAsync(cacheKey, cacheableFacets, DefaultFacetCacheExpirationMinutes, slidingExpiration: true); - } - - return facets ?? new Dictionary>(); - } - /// public async Task GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId, CancellationToken cancellationToken = default) { @@ -269,6 +198,7 @@ public async Task GetCatalogueSearchResultAsync(Cata catalogSearchRequestModel.SearchText, searchOptions, cancellationToken); + var count = Convert.ToInt32(response.TotalCount); var documentList = new CatalogueDocumentList { @@ -283,7 +213,7 @@ public async Task GetCatalogueSearchResultAsync(Cata Id = doc.Id, Name = doc.Title, Description = doc.Description, - Click = new SearchClickModel { Payload = new SearchClickPayloadModel { HitNumber = 1 }, Url = "binon" } + Click = BuildSearchClickModel(doc.Id, doc.Title, catalogSearchRequestModel.PageIndex, catalogSearchRequestModel.SearchId, filters, catalogSearchRequestModel.SearchText, count) }; }) .ToArray() @@ -292,7 +222,7 @@ public async Task GetCatalogueSearchResultAsync(Cata viewmodel.DocumentList = documentList; viewmodel.Stats = new Stats { - TotalHits = Convert.ToInt32(response.TotalCount) + TotalHits = count }; catalogSearchRequestModel.TotalNumberOfHits = viewmodel.Stats.TotalHits; @@ -594,6 +524,7 @@ public async Task GetAllCatalogueSearchResultsAsy SearchResults response = await this.searchClient.SearchAsync( catalogSearchRequestModel.SearchText, searchOptions, cancellationToken); + var count = Convert.ToInt32(response.TotalCount); var documentList = new CatalogueDocumentList { @@ -608,7 +539,7 @@ public async Task GetAllCatalogueSearchResultsAsy Id = doc.Id, Name = doc.Title, Description = doc.Description, - Click = new SearchClickModel { Payload = new SearchClickPayloadModel { HitNumber = 1 }, Url = "binon" } + Click = BuildSearchClickModel(doc.Id, doc.Title, catalogSearchRequestModel.PageIndex, catalogSearchRequestModel.SearchId, filters, catalogSearchRequestModel.SearchText, count) }; }) .ToArray() @@ -617,8 +548,9 @@ public async Task GetAllCatalogueSearchResultsAsy viewmodel.DocumentList = documentList; viewmodel.Stats = new Stats { - TotalHits = Convert.ToInt32(response.TotalCount) + TotalHits = count }; + catalogSearchRequestModel.TotalNumberOfHits = viewmodel.Stats.TotalHits; return viewmodel; @@ -718,7 +650,7 @@ public async Task GetAutoSuggestionResultsAsync(string term { Id = item.Id?.Substring(1), Title = item.Text, - Click = new AutoSuggestionClickModel { Payload = new AutoSuggestionClickPayloadModel { HitNumber = 1 }, Url = "binon" } + Click = BuildAutoSuggestClickModel(item.Id?.Substring(1), item.Text, 0, 0 , term, suggestResults.Count()) }) .Take(3) .ToList() @@ -733,7 +665,7 @@ public async Task GetAutoSuggestionResultsAsync(string term { Id = item.Id?.Substring(1), Name = item.Text, - Click = new AutoSuggestionClickModel { Payload = new AutoSuggestionClickPayloadModel { HitNumber = 1 }, Url = "binon" } + Click = BuildAutoSuggestClickModel(item.Id?.Substring(1), item.Text, 0, 0, term, suggestResults.Count()) }) .Take(3) .ToList() @@ -748,7 +680,7 @@ public async Task GetAutoSuggestionResultsAsync(string term Id = item.Id?.Substring(1), Concept = item.Text, Title = item.Text, - Click = new AutoSuggestionClickModel { Payload = new AutoSuggestionClickPayloadModel { HitNumber = 1 }, Url = "binon" } + Click = BuildAutoSuggestClickModel(item.Id?.Substring(1), item.Text, 0, 0, term, autoResults.Count()) }) .ToList() }; @@ -812,6 +744,102 @@ private async Task> GetResourceMetadataViewModel return resourceMetadataViewModels; } + + private static SearchClickModel BuildSearchClickModel(string targetUrl, string title, int hitNumber, long searchId, Dictionary> filters, string query, int count) + { + return new SearchClickModel + { + Payload = new SearchClickPayloadModel + { + ClickTargetUrl = targetUrl, + DocumentFields = new SearchClickDocumentModel() { Name = title, Title = title }, + HitNumber = hitNumber, + SearchSignal = new SearchClickSignalModel() + { + SearchId = Convert.ToString(searchId), + Query = query + "&" + filters, + UserQuery = query, + TimeOfSearch = DateTimeOffset.Now.ToUnixTimeMilliseconds(), + Stats = new SearchClickStatsModel() { TotalHits = count } + }, + } + }; + } + + private static AutoSuggestionClickModel BuildAutoSuggestClickModel(string targetUrl, string title, int hitNumber, long searchId, string query, int count) + { + return new AutoSuggestionClickModel + { + Payload = new AutoSuggestionClickPayloadModel + { + ClickTargetUrl = targetUrl, + DocumentFields = new SearchClickDocumentModel() { Name = title, Title = title }, + HitNumber = hitNumber, + SearchSignal = new SearchClickSignalModel() + { + SearchId = Convert.ToString(searchId), + Query = query, + UserQuery = query, + TimeOfSearch = DateTimeOffset.Now.ToUnixTimeMilliseconds(), + Stats = new SearchClickStatsModel() { TotalHits = count } + }, + } + }; + } + + private string MapToResourceType(string resourceType) + { + if (string.IsNullOrWhiteSpace(resourceType)) + return ResourceTypeEnum.Undefined.ToString(); + + string cleanedResourceType = resourceType + .Trim() + .ToLower() + .Replace(" ", "") + .Replace("_", "") + .Replace("-", ""); + + if (cleanedResourceType.StartsWith("scorm")) + cleanedResourceType = ResourceTypeEnum.Scorm.ToString(); + else if (cleanedResourceType.StartsWith("web")) + cleanedResourceType = ResourceTypeEnum.WebLink.ToString(); + else if (cleanedResourceType.Contains("file")) + cleanedResourceType = ResourceTypeEnum.GenericFile.ToString(); + + return cleanedResourceType; + } + + /// + /// Gets unfiltered facets for a search term, using caching. + /// + /// The search text. + /// The facet results. + /// Cancellation token. + /// The unfiltered facet results. + private async Task>> GetUnfilteredFacetsAsync( + string searchText, + IDictionary> facets, + CancellationToken cancellationToken) + { + var cacheKey = $"AllFacets_{searchText?.ToLowerInvariant() ?? "*"}"; + var cacheResponse = await this.cachingService.GetAsync>>(cacheKey); + + if (cacheResponse.ResponseEnum == CacheReadResponseEnum.Found) + { + // Convert cached DTO back to FacetResult dictionary + return AzureSearchFacetHelper.ConvertFromCacheable(cacheResponse.Item); + } + + if (facets != null) + { + // Convert to cacheable DTO before caching + var cacheableFacets = AzureSearchFacetHelper.ConvertToCacheable(facets); + await this.cachingService.SetAsync(cacheKey, cacheableFacets, DefaultFacetCacheExpirationMinutes, slidingExpiration: true); + } + + return facets ?? new Dictionary>(); + } + private ResourceMetadataViewModel MapToViewModel(Resource resource, List resourceActivities) { var hasCurrentResourceVersion = resource.CurrentResourceVersion != null; diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index 4f04ca015..05b4095a1 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -58,6 +58,7 @@ "QueryApiKey": "", "IndexName": "", "SuggesterName": "", + "SearchQueryType": "semantic", //semantic, full, or simple "DefaultItemLimitForSearch": 10, "DescriptionLengthLimit": 3000, "MaximumDescriptionLength": 150 From a0f98fa53a553a023749396731e52d3dc2a1bff8 Mon Sep 17 00:00:00 2001 From: Binon Date: Tue, 25 Nov 2025 13:30:30 +0000 Subject: [PATCH 010/106] Upgraded to latest model project --- AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj | 2 +- .../LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj.user | 2 +- .../LearningHub.Nhs.WebUI.AutomatedUiTests.csproj | 2 +- LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Models.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Repositories.Interface.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Repositories.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Services.Interface.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Services.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Tests.csproj | 2 +- OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj | 2 +- .../LearningHub.Nhs.ReportApi.Services.Interface.csproj | 2 +- .../LearningHub.Nhs.ReportApi.Services.UnitTests.csproj | 2 +- .../LearningHub.Nhs.ReportApi.Services.csproj | 2 +- .../LearningHub.Nhs.ReportApi.Shared.csproj | 2 +- .../LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj | 2 +- WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj | 2 +- .../LearningHub.Nhs.Api.Shared.csproj | 2 +- .../LearningHub.Nhs.Api.UnitTests.csproj | 2 +- .../LearningHub.Nhs.Repository.Interface.csproj | 2 +- .../LearningHub.Nhs.Repository.csproj | 2 +- .../LearningHub.Nhs.Services.Interface.csproj | 2 +- .../LearningHub.Nhs.Services.UnitTests.csproj | 2 +- WebAPI/LearningHub.Nhs.Services/LearningHub.Nhs.Services.csproj | 2 +- .../LearningHub.Nhs.Migration.ConsoleApp.csproj | 2 +- .../LearningHub.Nhs.Migration.Interface.csproj | 2 +- .../LearningHub.Nhs.Migration.Models.csproj | 2 +- .../LearningHub.Nhs.Migration.Staging.Repository.csproj | 2 +- .../LearningHub.Nhs.Migration.UnitTests.csproj | 2 +- .../LearningHub.Nhs.Migration/LearningHub.Nhs.Migration.csproj | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj index b3a8d9b6b..f33ea1faf 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/LearningHub.Nhs.AdminUI.csproj.user b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj.user index b17387f00..953342020 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj.user +++ b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj.user @@ -4,6 +4,6 @@ ProjectDebugger - IIS Local + Local IIS \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj index d846dfde5..a452c06fe 100644 --- a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index 09cb454c3..38b9acece 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -113,7 +113,7 @@ - + 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 1735b0b62..ecb95e713 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj index eb117ebe3..cebc741cc 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj index c6d370145..1a52f83b3 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj @@ -24,7 +24,7 @@ - + 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 ceef6ce67..602eb5291 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 @@ -17,7 +17,7 @@ - + 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 d485036ba..ce155abe1 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -31,7 +31,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj index ac9eaae79..0c6e3ca1d 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj index 34579b31b..f59780c2a 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj @@ -19,7 +19,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj index 9925c85ea..6b82577e3 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj @@ -16,7 +16,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj index 0948fa06c..349cd0ea7 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj @@ -18,7 +18,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj index 1e3c6f360..f0a0c0607 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj @@ -19,7 +19,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj index 2be568801..8e61f6b21 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj @@ -17,7 +17,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj b/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj index 99ba74bda..92181b7c3 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj @@ -20,7 +20,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj b/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj index 8040a9f8d..6c56ace1e 100644 --- a/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj +++ b/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj @@ -29,7 +29,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 05dbc4e35..0cab3f87b 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 72f200f46..a17bac47c 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.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj b/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj index 337b1120d..533a72516 100644 --- a/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj +++ b/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj @@ -10,7 +10,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj b/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj index 2a2cf49da..edc9c5d7f 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.Services.Interface/LearningHub.Nhs.Services.Interface.csproj b/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj index 827bc8750..1e4f5ce99 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 658c2bc1b..95ac49c5f 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 979ee394d..10ed93419 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/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj index 1555adfe3..b0b5dc579 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 @@ -25,7 +25,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 b03e83409..483e47cae 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 1d0a0e3e9..a7ceeea55 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 15b2ec905..7e8022692 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 4d44dba23..0b5589cfd 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 094c39953..4485ef979 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 From fa65ee1bae7575c85771a00156a60897942f4461 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Wed, 26 Nov 2025 09:25:54 +0000 Subject: [PATCH 011/106] new Commit without token --- ...rningHub.Nhs.WebUI.AutomatedUiTests.csproj | 3 +- .../Configuration/Settings.cs | 5 + .../Controllers/ReportsController.cs | 429 ++++++++++++++++ .../Helpers/CommonValidationErrorMessages.cs | 5 + .../Interfaces/IFileService.cs | 7 + .../Interfaces/IReportService.cs | 57 +++ .../LearningHub.Nhs.WebUI.csproj | 2 +- .../DynamicCheckboxItemViewModel.cs | 28 + .../DynamicCheckboxesViewModel.cs | 45 ++ .../Models/NavigationModel.cs | 5 + .../Report/CourseCompletionViewModel.cs | 66 +++ .../Report/ReportCreationCourseSelection.cs | 46 ++ .../Report/ReportCreationDateSelection.cs | 225 ++++++++ .../Models/Report/ReportFormActionTypeEnum.cs | 23 + .../Models/Report/ReportHistoryViewModel.cs | 38 ++ .../Models/Report/ReportPagingModel.cs | 20 + LearningHub.Nhs.WebUI/Services/FileService.cs | 22 + .../Services/NavigationPermissionService.cs | 13 +- .../Services/ReportService.cs | 194 +++++++ .../Startup/ServiceMappings.cs | 1 + .../Styles/nhsuk/pages/reporting.scss | 56 ++ .../DynamicCheckboxesViewComponent.cs | 58 +++ .../Reports/CourseCompletionReport.cshtml | 135 +++++ .../CreateReportCourseSelection.cshtml | 62 +++ .../Reports/CreateReportDateSelection.cshtml | 132 +++++ .../Views/Reports/Index.cshtml | 184 +++++++ .../Views/Reports/_ReportPaging.cshtml | 76 +++ .../Views/Reports/_ReportTable.cshtml | 145 ++++++ .../DynamicCheckboxes/Default.cshtml | 34 ++ .../Components/NavigationItems/Default.cshtml | 9 + .../Configuration/DatabricksConfig.cs | 60 +++ .../Configuration/LearningHubConfig.cs | 14 + .../Configuration/NotificationSetting.cs | 10 + .../LearningHub.Nhs.OpenApi.Models.csproj | 2 +- .../ViewModels/DatabricksNotification.cs | 40 ++ .../ViewModels/NavigationModel.cs | 5 + ....Nhs.OpenApi.Repositories.Interface.csproj | 2 +- .../Repositories/IReportHistoryRepository.cs | 28 + .../EntityFramework/LearningHubDbContext.cs | 6 + .../EntityFramework/ServiceMappings.cs | 1 + ...earningHub.Nhs.OpenApi.Repositories.csproj | 2 +- .../Map/ReportHistoryMap.cs | 23 + .../Repositories/ReportHistoryRepository.cs | 57 +++ .../Startup.cs | 1 + .../HttpClients/IDatabricksApiHttpClient.cs | 26 + ...gHub.Nhs.OpenApi.Services.Interface.csproj | 2 +- .../Services/IDatabricksService.cs | 77 +++ .../Services/INotificationService.cs | 9 + .../HttpClients/DatabricksApiHttpClient.cs | 63 +++ .../LearningHub.Nhs.OpenApi.Services.csproj | 2 +- .../Services/DatabricksService.cs | 483 ++++++++++++++++++ .../Services/NavigationPermissionService.cs | 12 +- .../Services/NotificationService.cs | 28 + .../Startup.cs | 2 + .../LearningHub.Nhs.OpenApi.Tests.csproj | 2 +- .../Configuration/ConfigurationExtensions.cs | 7 + .../Controllers/ReportController.cs | 129 +++++ .../Controllers/UserController.cs | 7 +- .../LearningHub.NHS.OpenAPI.csproj | 2 +- .../LearningHub.NHS.OpenAPI.csproj.user | 4 +- .../LearningHub.Nhs.OpenApi/appsettings.json | 16 +- .../LearningHub.Nhs.Database.sqlproj | 2 + .../Tables/Databricks/ReportHistory.sql | 25 + replacements.txt | 1 + 64 files changed, 3259 insertions(+), 16 deletions(-) create mode 100644 LearningHub.Nhs.WebUI/Controllers/ReportsController.cs create mode 100644 LearningHub.Nhs.WebUI/Interfaces/IReportService.cs create mode 100644 LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs create mode 100644 LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxesViewModel.cs create mode 100644 LearningHub.Nhs.WebUI/Models/Report/CourseCompletionViewModel.cs create mode 100644 LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs create mode 100644 LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs create mode 100644 LearningHub.Nhs.WebUI/Models/Report/ReportFormActionTypeEnum.cs create mode 100644 LearningHub.Nhs.WebUI/Models/Report/ReportHistoryViewModel.cs create mode 100644 LearningHub.Nhs.WebUI/Models/Report/ReportPagingModel.cs create mode 100644 LearningHub.Nhs.WebUI/Services/ReportService.cs create mode 100644 LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss create mode 100644 LearningHub.Nhs.WebUI/ViewComponents/DynamicCheckboxesViewComponent.cs create mode 100644 LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml create mode 100644 LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml create mode 100644 LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml create mode 100644 LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml create mode 100644 LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml create mode 100644 LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml create mode 100644 LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/DatabricksNotification.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IReportHistoryRepository.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/ReportHistoryMap.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ReportHistoryRepository.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IDatabricksApiHttpClient.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IDatabricksService.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/DatabricksApiHttpClient.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ReportController.cs create mode 100644 WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql create mode 100644 replacements.txt diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj index 695e93e2d..2f201378f 100644 --- a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj @@ -9,10 +9,11 @@ True + - + diff --git a/LearningHub.Nhs.WebUI/Configuration/Settings.cs b/LearningHub.Nhs.WebUI/Configuration/Settings.cs index e300e80e3..42778aeee 100644 --- a/LearningHub.Nhs.WebUI/Configuration/Settings.cs +++ b/LearningHub.Nhs.WebUI/Configuration/Settings.cs @@ -265,5 +265,10 @@ public Settings() /// Gets or sets AllCataloguePageSize. ///
public int AllCataloguePageSize { get; set; } + + /// + /// Gets or sets the StatMandId. + /// + public int StatMandId { get; set; } = 12; } } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs new file mode 100644 index 000000000..4ea7d3a5b --- /dev/null +++ b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs @@ -0,0 +1,429 @@ +namespace LearningHub.Nhs.WebUI.Controllers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Azure; + using GDS.MultiPageFormData; + using GDS.MultiPageFormData.Enums; + using LearningHub.Nhs.Caching; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Moodle; + using LearningHub.Nhs.Models.MyLearning; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.WebUI.Configuration; + using LearningHub.Nhs.WebUI.Filters; + using LearningHub.Nhs.WebUI.Helpers; + using LearningHub.Nhs.WebUI.Interfaces; + using LearningHub.Nhs.WebUI.Models; + using LearningHub.Nhs.WebUI.Models.Account; + using LearningHub.Nhs.WebUI.Models.DynamicCheckbox; + using LearningHub.Nhs.WebUI.Models.Learning; + using LearningHub.Nhs.WebUI.Models.Report; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using NHSUKViewComponents.Web.ViewModels; + + /// + /// Defines the . + /// + // [ServiceFilter(typeof(LoginWizardFilter))] + [Authorize] + [Route("Reports")] + public class ReportsController : BaseController + { + private const int ReportPageSize = 10; + private readonly ICacheService cacheService; + private readonly ICategoryService categoryService; + private readonly IMultiPageFormService multiPageFormService; + private readonly IReportService reportService; + private readonly IFileService fileService; + + /// + /// Initializes a new instance of the class. + /// + /// httpClientFactory. + /// cacheService. + /// multiPageFormService. + /// reportService. + /// categoryService. + /// fileService. + /// The hostingEnvironment. + /// The logger. + /// settings. + public ReportsController(IHttpClientFactory httpClientFactory, IWebHostEnvironment hostingEnvironment, ILogger logger, IOptions settings, ICacheService cacheService, IMultiPageFormService multiPageFormService, IReportService reportService, ICategoryService categoryService, IFileService fileService) + : base(hostingEnvironment, httpClientFactory, logger, settings.Value) + { + this.cacheService = cacheService; + this.multiPageFormService = multiPageFormService; + this.reportService = reportService; + this.categoryService = categoryService; + this.fileService = fileService; + } + + /// + /// The Report landing page. + /// + /// reportHistoryViewModel. + /// The . + [ResponseCache(CacheProfileName = "Never")] + public async Task Index(ReportHistoryViewModel reportHistoryViewModel = null) + { + int page = 1; + this.TempData.Clear(); + var newReport = new DatabricksRequestModel { Take = ReportPageSize, Skip = 0 }; + + await this.multiPageFormService.SetMultiPageFormData( + newReport, + MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), + this.TempData); + + var historyRequest = new PagingRequestModel + { + PageSize = ReportPageSize, + }; + + switch (reportHistoryViewModel.ReportFormActionType) + { + case ReportFormActionTypeEnum.NextPageChange: + reportHistoryViewModel.CurrentPageIndex += 1; + break; + + case ReportFormActionTypeEnum.PreviousPageChange: + reportHistoryViewModel.CurrentPageIndex -= 1; + break; + default: + reportHistoryViewModel.CurrentPageIndex = 0; + break; + } + + page = page + reportHistoryViewModel.CurrentPageIndex; + historyRequest.Page = page; + + // get a list of report history and send to view + var result = await this.reportService.GetReportHistory(historyRequest); + if (result != null) + { + reportHistoryViewModel.TotalCount = result.TotalItemCount; + reportHistoryViewModel.ReportHistoryModels = result.Items; + } + + this.ViewData["AllCourses"] = await this.GetCoursesAsync(); + reportHistoryViewModel.ReportPaging = new ReportPagingModel() { CurrentPage = reportHistoryViewModel.CurrentPageIndex, PageSize = ReportPageSize, TotalItems = reportHistoryViewModel.TotalCount, HasItems = reportHistoryViewModel.TotalCount > 0 }; + return this.View(reportHistoryViewModel); + } + + /// + /// CreateReportCourseSelection. + /// + /// searchText. + /// A representing the result of the asynchronous operation. + [Route("CreateReportCourseSelection")] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task CreateReportCourseSelection(string searchText = "") + { + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + var coursevm = new ReportCreationCourseSelection { SearchText = searchText, Courses = reportCreation.Courses != null ? reportCreation.Courses : new List() }; + var getCourses = await this.GetCoursesAsync(); + if (!string.IsNullOrWhiteSpace(searchText)) + { + getCourses = getCourses.Where(x => x.Value.ToLower().Contains(searchText.ToLower())).ToList(); + } + + if (coursevm.Courses.Count == 0 && !string.IsNullOrWhiteSpace(reportCreation.TimePeriod)) + { + coursevm.Courses = new List { "all" }; + } + + coursevm.BuildCourses(getCourses); + return this.View(coursevm); + } + + /// + /// CreateReportCourseSelection. + /// + /// courseSelection. + /// A representing the result of the asynchronous operation. + [HttpPost] + [Route("CreateReportCourseSelection")] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task CreateReportCourseSelection(ReportCreationCourseSelection courseSelection) + { + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + + if (courseSelection != null) + { + if (courseSelection.Courses.Any()) + { + reportCreation.Courses = courseSelection.Courses.Contains("all") ? new List() : courseSelection.Courses; + await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + return this.RedirectToAction("CreateReportDateSelection"); + } + } + + this.ModelState.AddModelError("Courses", CommonValidationErrorMessages.CourseRequired); + courseSelection.BuildCourses(await this.GetCoursesAsync()); + courseSelection.Courses = reportCreation.Courses; + return this.View("CreateReportCourseSelection", courseSelection); + } + + /// + /// CreateReportDateSelection. + /// + /// A representing the result of the asynchronous operation. + [Route("CreateReportDateSelection")] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task CreateReportDateSelection() + { + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + var dateVM = new ReportCreationDateSelection(); + dateVM.TimePeriod = reportCreation.TimePeriod; + if (reportCreation.StartDate.HasValue) + { + dateVM.StartDay = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Day : 0; + dateVM.StartMonth = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Month : 0; + dateVM.StartYear = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Year : 0; + dateVM.EndDay = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Day : 0; + dateVM.EndMonth = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Month : 0; + dateVM.EndYear = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Year : 0; + } + else + { + var result = await this.reportService.GetCourseCompletionReport(new DatabricksRequestModel + { + StartDate = reportCreation.StartDate, + EndDate = reportCreation.EndDate, + TimePeriod = reportCreation.TimePeriod, + Courses = reportCreation.Courses, + ReportHistoryId = reportCreation.ReportHistoryId, + Take = 1, + Skip = 1, + }); + + DateTime startDate = DateTime.MinValue; + var validDate = DateTime.TryParse(result.MinValidDate, out startDate); + dateVM.StartDay = validDate ? startDate.Day : 0; + dateVM.StartMonth = validDate ? startDate.Month : 0; + dateVM.StartYear = validDate ? startDate.Year : 0; + } + + return this.View(dateVM); + } + + /// + /// CreateReportDateSelection. + /// + /// reportCreationDate. + /// A representing the result of the asynchronous operation. + [Route("CreateReportSummary")] + [HttpPost] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task CreateReportSummary(ReportCreationDateSelection reportCreationDate) + { + // validate date + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + reportCreation.TimePeriod = reportCreationDate.TimePeriod; + reportCreation.StartDate = reportCreationDate.GetStartDate(); + reportCreation.EndDate = reportCreationDate.GetEndDate(); + await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + return this.RedirectToAction("CourseCompletionReport"); + } + + /// + /// ViewReport. + /// + /// reportHistoryId. + /// A representing the result of the asynchronous operation. + [Route("ViewReport/{reportHistoryId}")] + [HttpGet] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task ViewReport(int reportHistoryId) + { + this.TempData.Clear(); + var report = await this.reportService.GetReportHistoryById(reportHistoryId); + if (report == null) + { + return this.RedirectToAction("Index"); + } + + var reportRequest = new DatabricksRequestModel { Take = ReportPageSize, Skip = 0 }; + var periodCheck = int.TryParse(report.PeriodDays.ToString(), out int numberOfDays); + if (report.PeriodDays > 0 && periodCheck) + { + reportRequest.TimePeriod = report.PeriodDays.ToString(); + reportRequest.StartDate = DateTime.Now.AddDays(-numberOfDays); + reportRequest.EndDate = DateTime.Now; + } + else + { + reportRequest.TimePeriod = "Custom"; + reportRequest.StartDate = report.StartDate; + reportRequest.EndDate = report.EndDate; + reportRequest.ReportHistoryId = reportHistoryId; + } + + if (report.CourseFilter == "all") + { + report.CourseFilter = string.Empty; + } + + reportRequest.Courses = report.CourseFilter.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(f => f.Trim()).ToList(); + + await this.multiPageFormService.SetMultiPageFormData(reportRequest, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + return this.RedirectToAction("CourseCompletionReport"); + } + + /// + /// DownloadReport. + /// + /// reportHistoryId. + /// A representing the result of the asynchronous operation. + [Route("DownloadReport/{reportHistoryId}")] + [HttpGet] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task DownloadReport(int reportHistoryId) + { + var report = await this.reportService.DownloadReport(reportHistoryId); + if (report == null) + { + return this.RedirectToAction("Index"); + } + + var result = await this.fileService.DownloadBlobFileAsync(report.FilePath); + return this.File(result.Stream, result.ContentType, result.FileName); + } + + /// + /// ViewReport. + /// + /// reportHistoryId. + /// A representing the result of the asynchronous operation. + [Route("QueueReportDownload")] + [HttpPost] + [ResponseCache(CacheProfileName = "Never")] + public async Task QueueReportDownload(int reportHistoryId) + { + await this.reportService.QueueReportDownload(reportHistoryId); + return this.RedirectToAction("CourseCompletionReport"); + } + + /// + /// CourseCompletionReport. + /// + /// courseCompletion. + /// A representing the result of the asynchronous operation. + [Route("CourseCompletionReport")] + [HttpGet] + [ResponseCache(CacheProfileName = "Never")] + [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })] + public async Task CourseCompletionReport(CourseCompletionViewModel courseCompletion = null) + { + int page = 1; + + // validate date + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + + switch (courseCompletion.ReportFormActionType) + { + case ReportFormActionTypeEnum.NextPageChange: + courseCompletion.CurrentPageIndex += 1; + page = page + courseCompletion.CurrentPageIndex; + reportCreation.Skip = courseCompletion.CurrentPageIndex * ReportPageSize; + break; + + case ReportFormActionTypeEnum.PreviousPageChange: + courseCompletion.CurrentPageIndex -= 1; + page = page + courseCompletion.CurrentPageIndex; + reportCreation.Skip = courseCompletion.CurrentPageIndex * ReportPageSize; + break; + default: + courseCompletion.CurrentPageIndex = 0; + reportCreation.Skip = 0; + break; + } + + DateTimeOffset today = DateTimeOffset.Now.Date; + DateTimeOffset? startDate = null; + DateTimeOffset? endDate = null; + + if (int.TryParse(reportCreation.TimePeriod, out int days)) + { + startDate = today.AddDays(-days); + endDate = today; + } + else if (reportCreation.TimePeriod == "Custom") + { + startDate = reportCreation.StartDate; + endDate = reportCreation.EndDate; + } + + var result = await this.reportService.GetCourseCompletionReport(new DatabricksRequestModel + { + StartDate = startDate, + EndDate = endDate, + TimePeriod = reportCreation.TimePeriod, + Courses = reportCreation.Courses, + ReportHistoryId = reportCreation.ReportHistoryId, + Take = reportCreation.Take, + Skip = page, + }); + + var response = new CourseCompletionViewModel(reportCreation); + + if (result != null) + { + response.TotalCount = result.TotalCount; + response.CourseCompletionRecords = result.CourseCompletionRecords; + response.ReportHistoryModel = await this.reportService.GetReportHistoryById(result.ReportHistoryId); + reportCreation.ReportHistoryId = result.ReportHistoryId; + } + + await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + + var allCourses = await this.GetCoursesAsync(); + + List matchedCourseNames; + + if (reportCreation.Courses.Count == 0) + { + matchedCourseNames = allCourses.Select(course => course.Value).ToList(); + } + else + { + matchedCourseNames = allCourses + .Where(course => reportCreation.Courses.Contains(course.Key)) + .Select(course => course.Value) + .ToList(); + } + + this.ViewData["matchedCourseNames"] = matchedCourseNames; + response.ReportPaging = new ReportPagingModel() { CurrentPage = courseCompletion.CurrentPageIndex, PageSize = ReportPageSize, TotalItems = response.TotalCount, HasItems = response.TotalCount > 0 }; + return this.View(response); + } + + private async Task>> GetCoursesAsync() + { + int categoryId = this.Settings.StatMandId; + var courses = new List>(); + var subCategories = await this.categoryService.GetCoursesByCategoryIdAsync(categoryId); + + foreach (var subCategory in subCategories.Courses) + { + courses.Add(new KeyValuePair(subCategory.Id.ToString(), subCategory.Displayname)); + } + + return courses; + } + } +} diff --git a/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs b/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs index e41b794f1..e763fefa1 100644 --- a/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs +++ b/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs @@ -259,5 +259,10 @@ public static class CommonValidationErrorMessages /// Security question Required. ///
public const string SecurityQuestionRequired = "Please select a security question"; + + /// + /// Course Required. + /// + public const string CourseRequired = "Select a course"; } } diff --git a/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs b/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs index eb94d0185..8ab50935a 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs @@ -53,5 +53,12 @@ public interface IFileService /// . /// The . Task PurgeResourceFile(ResourceVersionExtendedViewModel vm = null, List filePaths = null); + + /// + /// The DownloadBlobFile. + /// + /// uri. + /// The . + Task<(Stream Stream, string FileName, string ContentType)> DownloadBlobFileAsync(string uri); } } diff --git a/LearningHub.Nhs.WebUI/Interfaces/IReportService.cs b/LearningHub.Nhs.WebUI/Interfaces/IReportService.cs new file mode 100644 index 000000000..8afa2cbc7 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Interfaces/IReportService.cs @@ -0,0 +1,57 @@ +namespace LearningHub.Nhs.WebUI.Interfaces +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using elfhHub.Nhs.Models.Common; + using LearningHub.Nhs.Models.Common; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Paging; + + /// + /// Defines the . + /// + public interface IReportService + { + /// + /// The GetReporterPermission. + /// + /// A representing the result of the asynchronous operation. + Task GetReporterPermission(); + + /// + /// The GetCourseCompletionReport. + /// + /// The requestModel.. + /// The . + Task GetCourseCompletionReport(DatabricksRequestModel requestModel); + + /// + /// The GetReportHistory. + /// + /// The requestModel.. + /// The . + Task> GetReportHistory(PagingRequestModel requestModel); + + /// + /// The GetReportHistory. + /// + /// The reportHistoryId.. + /// The . + Task GetReportHistoryById(int reportHistoryId); + + /// + /// The QueueReportDownload. + /// + /// The reportHistoryId.. + /// The . + Task QueueReportDownload(int reportHistoryId); + + /// + /// The DownloadReport. + /// + /// reportHistoryId. + /// The . + Task DownloadReport(int reportHistoryId); + } +} diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index 39df64b2a..09cb454c3 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -113,7 +113,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs new file mode 100644 index 000000000..47cd5ea44 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs @@ -0,0 +1,28 @@ +namespace LearningHub.Nhs.WebUI.Models.DynamicCheckbox +{ + /// + /// DynamicCheckboxItemViewModel. + /// + public class DynamicCheckboxItemViewModel + { + /// + /// Gets or sets a value. + /// + public string Value { get; set; } + + /// + /// Gets or sets a Label. + /// + public string Label { get; set; } + + /// + /// Gets or sets a HintText. + /// + public string? HintText { get; set; } + + /// + /// Gets or sets a value indicating whether gets or sets a selected. + /// + public bool Selected { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxesViewModel.cs b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxesViewModel.cs new file mode 100644 index 000000000..85738837a --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxesViewModel.cs @@ -0,0 +1,45 @@ +namespace LearningHub.Nhs.WebUI.Models.DynamicCheckbox +{ + using System.Collections.Generic; + + /// + /// DynamicCheckboxesViewModel. + /// + public class DynamicCheckboxesViewModel + { + /// + /// Gets or sets a Label. + /// + public string Label { get; set; } + + /// + /// Gets or sets a HintText. + /// + public string HintText { get; set; } + + /// + /// Gets or sets a ErrorMessage. + /// + public string ErrorMessage { get; set; } + + /// + /// Gets or sets a value indicating whether gets or sets a Required. + /// + public bool Required { get; set; } + + /// + /// Gets or sets a CssClass. + /// + public string CssClass { get; set; } + + /// + /// Gets or sets SelectedValues. + /// + public List SelectedValues { get; set; } = []; + + /// + /// Gets or sets a Checkboxes. + /// + public List Checkboxes { get; set; } = []; + } +} diff --git a/LearningHub.Nhs.WebUI/Models/NavigationModel.cs b/LearningHub.Nhs.WebUI/Models/NavigationModel.cs index 25b51671c..934564960 100644 --- a/LearningHub.Nhs.WebUI/Models/NavigationModel.cs +++ b/LearningHub.Nhs.WebUI/Models/NavigationModel.cs @@ -75,6 +75,11 @@ public class NavigationModel ///
public bool ShowBrowseCatalogues { get; set; } + /// + /// Gets or sets a value indicating whether to show reports. + /// + public bool ShowReports { get; set; } + /// /// Gets or sets a value indicating whether ShowHome. /// diff --git a/LearningHub.Nhs.WebUI/Models/Report/CourseCompletionViewModel.cs b/LearningHub.Nhs.WebUI/Models/Report/CourseCompletionViewModel.cs new file mode 100644 index 000000000..3e183c63e --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/CourseCompletionViewModel.cs @@ -0,0 +1,66 @@ +namespace LearningHub.Nhs.WebUI.Models.Report +{ + using System.Collections.Generic; + using System.Reflection; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.MyLearning; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.WebUI.Models.Learning; + + /// + /// CourseCompletionViewModel. + /// + public class CourseCompletionViewModel : DatabricksRequestModel + { + /// + /// Initializes a new instance of the class. + /// + public CourseCompletionViewModel() + { + this.CourseCompletionRecords = new List(); + } + + /// + /// Initializes a new instance of the class. + /// + /// DatabricksRequestModel. + public CourseCompletionViewModel(DatabricksRequestModel requestModel) + { + this.CourseCompletionRecords = new List(); + foreach (PropertyInfo prop in requestModel.GetType().GetProperties()) + { + this.GetType().GetProperty(prop.Name).SetValue(this, prop.GetValue(requestModel, null), null); + } + } + + /// + /// Gets or sets the CurrentPageIndex. + /// + public int CurrentPageIndex { get; set; } = 0; + + /// + /// Gets or sets the TotalCount. + /// + public int TotalCount { get; set; } + + /// + /// Gets or sets the report result paging. + /// + public PagingViewModel ReportPaging { get; set; } + + /// + /// Gets or sets the ReportFormActionTypeEnum. + /// + public ReportFormActionTypeEnum ReportFormActionType { get; set; } + + /// + /// Gets or sets the CourseCompletionRecords. + /// + public List CourseCompletionRecords { get; set; } + + /// + /// Gets or sets the ReportHistoryModel. + /// + public ReportHistoryModel ReportHistoryModel { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs new file mode 100644 index 000000000..8d7600987 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs @@ -0,0 +1,46 @@ +namespace LearningHub.Nhs.WebUI.Models.Report +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Linq; + using LearningHub.Nhs.WebUI.Models.DynamicCheckbox; + using NHSUKViewComponents.Web.ViewModels; + + /// + /// CourseSelection. + /// + public class ReportCreationCourseSelection + { + /// + /// Gets or sets the list of courses. + /// + public List Courses { get; set; } + + /// + /// Gets or sets the list of all courses. + /// + public List AllCources { get; set; } + + /// + /// Gets or sets the list of SearchText. + /// + public string SearchText { get; set; } + + /// + /// BuildCourses. + /// + /// The all Courses. + /// The . + public List BuildCourses(List> allCourses) + { + this.AllCources = allCourses.Select(r => new DynamicCheckboxItemViewModel + { + Value = r.Key.ToString(), + Label = r.Value, + }).ToList(); + this.AllCources.Insert(0, new DynamicCheckboxItemViewModel { Value = "all", Label = "All Courses", }); + return this.AllCources; + } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs new file mode 100644 index 000000000..2263a0aa1 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs @@ -0,0 +1,225 @@ +namespace LearningHub.Nhs.WebUI.Models.Report +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.Linq; + using LearningHub.Nhs.WebUI.Helpers; + using NHSUKViewComponents.Web.ViewModels; + + /// + /// ReportCreationDateSelection. + /// + public class ReportCreationDateSelection : IValidatableObject + { + /// + /// Gets or sets the start date to define on the search. + /// + public string TimePeriod { get; set; } + + /// + /// Gets or sets the start date to define on the search. + /// + /// + /// Gets or sets the Day. + /// + public int? StartDay { get; set; } + + /// + /// Gets or sets the end Day. + /// + public int? EndDay { get; set; } + + /// + /// Gets or sets the Country. + /// + public int? StartMonth { get; set; } + + /// + /// Gets or sets the Country. + /// + public int? EndMonth { get; set; } + + /// + /// Gets or sets the Year. + /// + public int? StartYear { get; set; } + + /// + /// Gets or sets the Year. + /// + public int? EndYear { get; set; } + + /// + /// Gets or sets the Year. + /// + public DateTime? DataStart { get; set; } + + /// + /// Gets or sets a value indicating whether gets or sets the EndDate. + /// + public bool EndDate { get; set; } + + /// + /// Gets or sets the GetDate. + /// + /// DateTime. + public DateTime? GetStartDate() + { + return (this.StartDay.HasValue && this.StartMonth.HasValue && this.StartYear.HasValue) ? new DateTime(this.StartYear!.Value, this.StartMonth!.Value, this.StartDay!.Value) : (DateTime?)null; + } + + /// + /// Gets or sets the GetDate. + /// + /// DateTime. + public DateTime? GetEndDate() + { + return (this.EndDay.HasValue && this.EndMonth.HasValue && this.EndYear.HasValue) ? new DateTime(this.EndYear!.Value, this.EndMonth!.Value, this.EndDay!.Value) : (DateTime?)null; + } + + /// + public IEnumerable Validate(ValidationContext validationContext) + { + var validationResults = new List(); + if (this.TimePeriod == "Custom") + { + this.ValidateStartDate(validationResults); + + if (this.EndDate) + { + this.ValidateEndDate(validationResults); + } + } + + return validationResults; + } + + /// + /// Gets or sets the GetValidatedStartDate. + /// + /// DateTime. + public DateTime GetValidatedStartDate() + { + return new DateTime(this.StartYear!.Value, this.StartMonth!.Value, this.StartDay!.Value); + } + + /// + /// Gets or sets the GetValidatedEndDate. + /// + /// DateTime. + public DateTime? GetValidatedEndDate() + { + return this.EndDate + ? new DateTime(this.EndYear!.Value, this.EndMonth!.Value, this.EndDay!.Value) + : (DateTime?)null; + } + + /// + /// sets the list of radio region. + /// + /// The . + public List PopulateDateRange() + { + var radios = new List() + { + new RadiosItemViewModel("7", "7 days", false, null), + new RadiosItemViewModel("30", "30 days", false, null), + new RadiosItemViewModel("90", "90 days", false, null), + }; + + // if (string.IsNullOrWhiteSpace(this.TimePeriod)) + // { + // this.TimePeriod = "Custom"; + // } + return radios; + } + + /// + // public IEnumerable Validate(ValidationContext validationContext) + // { + // var results = new List(); + // if (this.TimePeriod == "dateRange") + // { + // var startDateValidation = DateValidator.ValidateDate(this.StartDay, this.StartMonth, this.StartYear, "valid start date") + // .ToValidationResultList(nameof(this.StartDay), nameof(this.StartMonth), nameof(this.StartYear)); + // if (startDateValidation.Any()) + // { + // results.AddRange(startDateValidation); + // } + // var endDateValidation = DateValidator.ValidateDate(this.EndDay, this.EndMonth, this.EndYear, "valid end date") + // .ToValidationResultList(nameof(this.EndDay), nameof(this.EndMonth), nameof(this.EndYear)); + // if (endDateValidation.Any()) + // { + // results.AddRange(endDateValidation); + // } + // } + // return results; + // } + private void ValidateStartDate(List validationResults) + { + var startDateValidationResults = DateValidator.ValidateDate( + this.StartDay, + this.StartMonth, + this.StartYear, + "Start date", + true, + false, + true) + .ToValidationResultList(nameof(this.StartDay), nameof(this.StartMonth), nameof(this.StartYear)); + + if (!startDateValidationResults.Any()) + { + this.ValidateStartDateIsAfterDataStart(startDateValidationResults); + } + + validationResults.AddRange(startDateValidationResults); + } + + private void ValidateStartDateIsAfterDataStart(List startDateValidationResults) + { + var startDate = this.GetValidatedStartDate(); + + if (startDate.AddDays(1) < this.DataStart) + { + startDateValidationResults.Add( + new ValidationResult( + "Enter a start date after the start of data in the platform", new[] { nameof(this.StartDay), })); + startDateValidationResults.Add( + new ValidationResult( + string.Empty, + new[] { nameof(this.StartMonth), nameof(this.StartYear), })); + } + } + + private void ValidateEndDate(List validationResults) + { + var endDateValidationResults = DateValidator.ValidateDate( + this.EndDay, + this.EndMonth, + this.EndYear, + "End date", + true, + false, + true) + .ToValidationResultList(nameof(this.EndDay), nameof(this.EndMonth), nameof(this.EndYear)); + + this.ValidateEndDateIsAfterStartDate(endDateValidationResults); + + validationResults.AddRange(endDateValidationResults); + } + + private void ValidateEndDateIsAfterStartDate(List endDateValidationResults) + { + if (this.StartYear > this.EndYear + || (this.StartYear == this.EndYear && this.StartMonth > this.EndMonth) + || (this.StartYear == this.EndYear && this.StartMonth == this.EndMonth && this.StartDay > this.EndDay)) + { + endDateValidationResults.Add( + new ValidationResult("Enter an end date after the start date", new[] { nameof(this.EndDay), })); + endDateValidationResults.Add( + new ValidationResult(string.Empty, new[] { nameof(this.EndMonth), nameof(this.EndYear), })); + } + } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportFormActionTypeEnum.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportFormActionTypeEnum.cs new file mode 100644 index 000000000..2e38defc1 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportFormActionTypeEnum.cs @@ -0,0 +1,23 @@ +namespace LearningHub.Nhs.WebUI.Models.Learning +{ + /// + /// Defines the ReportFormActionTypeEnum. + /// + public enum ReportFormActionTypeEnum + { + /// + /// Defines the basic search for mylearning + /// + BasicSearch = 0, + + /// + /// Previoous page change. + /// + PreviousPageChange = 1, + + /// + /// Next page change. + /// + NextPageChange = 2, + } +} diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportHistoryViewModel.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportHistoryViewModel.cs new file mode 100644 index 000000000..a958c39f7 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportHistoryViewModel.cs @@ -0,0 +1,38 @@ +namespace LearningHub.Nhs.WebUI.Models.Report +{ + using System.Collections.Generic; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.WebUI.Models.Learning; + + /// + /// ReportHistoryViewModel. + /// + public class ReportHistoryViewModel + { + /// + /// Gets or sets the CurrentPageIndex. + /// + public int CurrentPageIndex { get; set; } = 0; + + /// + /// Gets or sets the TotalCount. + /// + public int TotalCount { get; set; } + + /// + /// Gets or sets the report result paging. + /// + public PagingViewModel ReportPaging { get; set; } + + /// + /// Gets or sets the ReportFormActionTypeEnum. + /// + public ReportFormActionTypeEnum ReportFormActionType { get; set; } + + /// + /// Gets or sets the ReportHistoryModels. + /// + public List ReportHistoryModels { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportPagingModel.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportPagingModel.cs new file mode 100644 index 000000000..5468bab7b --- /dev/null +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportPagingModel.cs @@ -0,0 +1,20 @@ +namespace LearningHub.Nhs.WebUI.Models.Learning +{ + using LearningHub.Nhs.Models.Paging; + + /// + /// Defines the . + /// + public class ReportPagingModel : PagingViewModel + { + /// + /// Gets or sets the page previous action value. + /// + public int PreviousActionValue { get; set; } + + /// + /// Gets or sets the page next action value. + /// + public int NextActionValue { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Services/FileService.cs b/LearningHub.Nhs.WebUI/Services/FileService.cs index a09195d62..fda4a0444 100644 --- a/LearningHub.Nhs.WebUI/Services/FileService.cs +++ b/LearningHub.Nhs.WebUI/Services/FileService.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; + using Azure.Storage.Blobs; using Azure.Storage.Files.Shares; using Azure.Storage.Files.Shares.Models; using Azure.Storage.Sas; @@ -12,6 +13,7 @@ using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.Models; + using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Options; @@ -249,6 +251,26 @@ public async Task PurgeResourceFile(ResourceVersionExtendedViewModel vm = null, } } + /// + /// The DownloadBlobFile. + /// + /// url. + /// The . + public async Task<(Stream Stream, string FileName, string ContentType)> DownloadBlobFileAsync(string url) + { + var uri = new Uri(url); + string containerName = uri.Segments[1].TrimEnd('/'); + string blobName = string.Join(string.Empty, uri.Segments, 2, uri.Segments.Length - 2); + BlobClient blobClient = new BlobClient(this.settings.AzureBlobSettings.ConnectionString, containerName, blobName); + + var properties = await blobClient.GetPropertiesAsync(); + string contentType = properties.Value.ContentType ?? "application/octet-stream"; + string fileName = Path.GetFileName(blobClient.Name); + var stream = await blobClient.OpenReadAsync(); + + return (stream, fileName, contentType); + } + private static async Task WaitForCopyAsync(ShareFileClient fileClient) { // Wait for the copy operation to complete diff --git a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs index 7c357ada6..bae61e8eb 100644 --- a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs +++ b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs @@ -12,18 +12,22 @@ public class NavigationPermissionService : INavigationPermissionService { private readonly IResourceService resourceService; private readonly IUserGroupService userGroupService; + private readonly IReportService reportService; /// /// Initializes a new instance of the class. /// /// Resource service. /// UserGroup service. + /// Report Service. public NavigationPermissionService( IResourceService resourceService, - IUserGroupService userGroupService) + IUserGroupService userGroupService, + IReportService reportService) { this.resourceService = resourceService; this.userGroupService = userGroupService; + this.reportService = reportService; } /// @@ -87,6 +91,7 @@ public NavigationModel NotAuthenticated() ShowSignOut = false, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } @@ -113,6 +118,7 @@ private NavigationModel AuthenticatedAdministrator(string controllerName) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = true, }; } @@ -139,6 +145,7 @@ private async Task AuthenticatedBlueUser(string controllerName) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = await this.reportService.GetReporterPermission(), }; } @@ -164,6 +171,7 @@ private NavigationModel AuthenticatedGuest() ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } @@ -190,6 +198,7 @@ private async Task AuthenticatedReadOnly(string controllerName) ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = true, + ShowReports = await this.reportService.GetReporterPermission(), }; } @@ -215,6 +224,7 @@ private async Task AuthenticatedBasicUserOnly() ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = false, }; } @@ -240,6 +250,7 @@ private NavigationModel InLoginWizard() ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } } diff --git a/LearningHub.Nhs.WebUI/Services/ReportService.cs b/LearningHub.Nhs.WebUI/Services/ReportService.cs new file mode 100644 index 000000000..de383e4df --- /dev/null +++ b/LearningHub.Nhs.WebUI/Services/ReportService.cs @@ -0,0 +1,194 @@ +namespace LearningHub.Nhs.WebUI.Services +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + using elfhHub.Nhs.Models.Common; + using LearningHub.Nhs.Models.Common; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.Models.Validation; + using LearningHub.Nhs.WebUI.Interfaces; + using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + + /// + /// Defines the . + /// + public class ReportService : BaseService, IReportService + { + /// + /// Initializes a new instance of the class. + /// + /// The Web Api Http Client. + /// The Open Api Http Client. + /// logger. + public ReportService(ILearningHubHttpClient learningHubHttpClient, IOpenApiHttpClient openApiHttpClient, ILogger logger) + : base(learningHubHttpClient, openApiHttpClient, logger) + { + } + + /// + /// The GetAllAsync. + /// + /// The . + public async Task GetReporterPermission() + { + bool viewmodel = false; + + var client = await this.OpenApiHttpClient.GetClientAsync(); + + var request = $"Report/GetReporterPermission"; + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + viewmodel = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized + || + response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return viewmodel; + } + + /// + /// The GetCourseCompletionReport. + /// + /// The requestModel.. + /// The . + public async Task GetCourseCompletionReport(DatabricksRequestModel requestModel) + { + DatabricksDetailedViewModel apiResponse = null; + var json = JsonConvert.SerializeObject(requestModel); + var stringContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json"); + + var client = await this.OpenApiHttpClient.GetClientAsync(); + + var request = $"Report/GetCourseCompletionReport"; + var response = await client.PostAsync(request, stringContent).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + apiResponse = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return apiResponse; + } + + /// + /// The GetReportHistory. + /// + /// The requestModel.. + /// The . + public async Task> GetReportHistory(PagingRequestModel requestModel) + { + PagedResultSet apiResponse = null; + var json = JsonConvert.SerializeObject(requestModel); + var stringContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json"); + + var client = await this.OpenApiHttpClient.GetClientAsync(); + + var request = $"Report/GetReportHistory"; + var response = await client.PostAsync(request, stringContent).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + apiResponse = JsonConvert.DeserializeObject>(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return apiResponse; + } + + /// + /// The GetReportHistory. + /// + /// The reportHistoryId.. + /// The . + public async Task GetReportHistoryById(int reportHistoryId) + { + ReportHistoryModel apiResponse = null; + var client = await this.OpenApiHttpClient.GetClientAsync(); + var request = $"Report/GetReportHistoryById/{reportHistoryId}"; + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + apiResponse = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return apiResponse; + } + + /// + /// The QueueReportDownload. + /// + /// The reportHistoryId.. + /// The . + public async Task QueueReportDownload(int reportHistoryId) + { + bool apiResponse = false; + var client = await this.OpenApiHttpClient.GetClientAsync(); + var request = $"Report/QueueReportDownload/{reportHistoryId}"; + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + apiResponse = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return apiResponse; + } + + /// + /// The DownloadReport. + /// + /// reportHistoryId. + /// The . + public async Task DownloadReport(int reportHistoryId) + { + ReportHistoryModel apiResponse = null; + var client = await this.OpenApiHttpClient.GetClientAsync(); + var request = $"Report/DownloadReport/{reportHistoryId}"; + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + apiResponse = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return apiResponse; + } + } +} diff --git a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs index 6ed258354..26f75aeb1 100644 --- a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs +++ b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs @@ -81,6 +81,7 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss new file mode 100644 index 000000000..0f6d99905 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss @@ -0,0 +1,56 @@ +@use "../../abstracts/all" as *; + + +.user-report { + .nhsuk-details__summary { + display: flex; + justify-content: space-between; + align-items: center; + } + + .override-summary-color { + color: black !important; + font-weight: normal; + text-decoration: none; + } + + .nhsuk-summary-list__key--tight { + flex: 0 0 20%; + width: auto; + } + + .nhsuk-summary-list__row { + display: flex !important; + align-items: flex-start; /* optional: aligns top of key/value */ + } + + .nhsuk-summary-list__row { + border-bottom: none; + } + + .nhsuk-summary-list__key, .nhsuk-summary-list__value, .nhsuk-summary-list__actions { + border: none; + } + + .nhsuk-date-inline { + display: flex; + align-items: center; + gap: 0.5rem; /* space between label and input */ + } + + .nhsuk-date-inline .nhsuk-label { + margin-bottom: 0; + white-space: nowrap; + } + + .nhsuk-button--with-border { + border: 2px solid #005eb8; + background-color: #ffffff; + color: #005eb8; + } + + .nhsuk-button--with-border:hover { + background-color: #f0f8ff; + border-color: #003087; + } +} diff --git a/LearningHub.Nhs.WebUI/ViewComponents/DynamicCheckboxesViewComponent.cs b/LearningHub.Nhs.WebUI/ViewComponents/DynamicCheckboxesViewComponent.cs new file mode 100644 index 000000000..01ef484ac --- /dev/null +++ b/LearningHub.Nhs.WebUI/ViewComponents/DynamicCheckboxesViewComponent.cs @@ -0,0 +1,58 @@ +namespace LearningHub.Nhs.WebUI.ViewComponents +{ + using System.Collections.Generic; + using System.Linq; + using LearningHub.Nhs.WebUI.Models.DynamicCheckbox; + using Microsoft.AspNetCore.Mvc; + + /// + /// Defines the . + /// + public class DynamicCheckboxesViewComponent : ViewComponent + { + /// + /// The Invoke. + /// + /// label. + /// checkboxes. + /// required. + /// errorMessage. + /// hintText. + /// cssClass. + /// selectedValues. + /// propertyName. + /// A representing the result of the synchronous operation. + public IViewComponentResult Invoke( + string label, + IEnumerable checkboxes, + bool required = false, + string? errorMessage = null, + string? hintText = null, + string? cssClass = null, + IEnumerable? selectedValues = null, + string propertyName = "SelectedValues") + { + var selectedList = selectedValues?.ToList() ?? new List(); + + var viewModel = new DynamicCheckboxesViewModel + { + Label = label, + HintText = string.IsNullOrWhiteSpace(hintText) ? null : hintText, + ErrorMessage = errorMessage, + Required = required, + CssClass = string.IsNullOrWhiteSpace(cssClass) ? null : cssClass, + SelectedValues = selectedList, + Checkboxes = checkboxes.Select(cb => new DynamicCheckboxItemViewModel + { + Value = cb.Value, + Label = cb.Label, + HintText = cb.HintText, + Selected = selectedList.Contains(cb.Value), + }).ToList(), + }; + + this.ViewData["PropertyName"] = propertyName; + return this.View(viewModel); + } + } +} diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml new file mode 100644 index 000000000..9677805f1 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml @@ -0,0 +1,135 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.Models.Enums.Report +@using LearningHub.Nhs.WebUI.Helpers +@using LearningHub.Nhs.WebUI.Models +@using LearningHub.Nhs.WebUI.Models.Learning +@model LearningHub.Nhs.WebUI.Models.Report.CourseCompletionViewModel + +@{ + ViewData["Title"] = "Course completion report"; + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + + + var pagingModel = Model.ReportPaging; + int currentPage = pagingModel.CurrentPage; + int pageSize = pagingModel.PageSize; + int totalRows = pagingModel.TotalItems; + + int startRow = (currentPage * pageSize) + 1; + int endRow = Math.Min(startRow + pageSize - 1, totalRows); + var distinctCourses = this.ViewData["matchedCourseNames"] as List; + +} + +@section styles { + +} + +
+
+
+ + +
+ +
+ Home + + Reports +
+ +

Course completion report

+
+ +
+
+ Course@(distinctCourses.Count() > 1 ? "s" : "") +
+
+
    + @foreach (var entry in distinctCourses) + { +
  • @entry
  • + } +
+ +
+ +
+ + Change Course + + +
+ +
+
+
+ Reporting period +
+
+ @{ + if (Model.TimePeriod == "Custom" && Model.StartDate.HasValue && Model.EndDate.HasValue) + { + @Model.StartDate.Value.ToString("dd MMMM yyyy") to @Model.EndDate.Value.ToString("dd MMMM yyyy") + } + else + { + @Model.TimePeriod days + } + } + +
+ +
+ + Change Reporting period + + +
+ +
+
+ +
+

Displaying @startRow–@endRow of @Model.TotalCount filtered row@(Model.TotalCount > 1 ? "s" : "")

+ + @if (Model.TotalCount != 0) + { +

+ Request to download this report in a spreadsheet (.xls) format.You will be notified + when the report is ready. +

+ @if (Model.ReportHistoryModel != null && Model.ReportHistoryModel.DownloadRequest == null) + { +
+ + +
+ } + else if (Model.ReportHistoryModel != null && Model.ReportHistoryModel.DownloadRequest == true && Model.ReportHistoryModel.ReportStatusId == ((int)Status.Pending)) + { +
+ Information: +

We’re getting your report ready

+

+ You will be notified once it’s ready to download and you will be able to access it in the + Reports section. +

+
+ } + } + + @if (Model.CourseCompletionRecords.Any()) + { + @await Html.PartialAsync("_ReportTable", Model) + } + +
+
+
+ @await Html.PartialAsync("_ReportPaging", Model) +
+
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml new file mode 100644 index 000000000..f6a7e947c --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml @@ -0,0 +1,62 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.WebUI.Helpers +@using LearningHub.Nhs.WebUI.Models +@using LearningHub.Nhs.WebUI.Models.Learning +@using NHSUKViewComponents.Web.ViewModels +@model LearningHub.Nhs.WebUI.Models.Report.ReportCreationCourseSelection; +@{ + ViewData["Title"] = "Select Course"; + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + var errorHasOccurred = !ViewData.ModelState.IsValid; + var routeData = new Dictionary { { "ReturnToConfirmation", Context.Request.Query["returnToConfirmation"] } }; +} + +@section styles { + +} + +
+
+
+ + +
+ +
+ Create a course completion report +
+

+ Select course(s) +

+
+ + @if (errorHasOccurred) + { + + } + +
+
+ @await Component.InvokeAsync("DynamicCheckboxes", new + { + label = "", + checkboxes = Model.AllCources, + required = false, + errorMessage = "Please select at least one course", + selectedValues = Model.Courses, + propertyName = nameof(Model.Courses) + }) +
+
+ + +
+
+ +
+ + +
+
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml new file mode 100644 index 000000000..745e1e0d4 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml @@ -0,0 +1,132 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.WebUI.Helpers +@using LearningHub.Nhs.WebUI.Models +@using LearningHub.Nhs.WebUI.Models.Learning +@using NHSUKViewComponents.Web.ViewModels +@model LearningHub.Nhs.WebUI.Models.Report.ReportCreationDateSelection; +@{ + ViewData["Title"] = "Select Date"; + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + var errorHasOccurred = !ViewData.ModelState.IsValid; + var customId = $"TimePeriod-{Model.PopulateDateRange().Count()}"; + var hintTextLines = new List { $"For example, {Model.StartDay} {Model.StartMonth} {Model.StartYear}" }; + var endHintTextLines = new List { $" " }; +} + +@section styles { + +} + +
+
+
+ + +
+ +
+ Create a course completion report +
+

+ Reporting Period +

+
+ + @if (errorHasOccurred) + { + + } + +
+ +
+
+ +

+ +

+
+
+ For the last: +
+
+ @foreach (var (radio, index) in Model.PopulateDateRange().Select((r, i) => (r, i))) + { + var radioId = $"TimePeriod-{index}"; +
+ + + @if (radio.HintText != null) + { +
+ @radio.HintText +
+ } +
+ + } + +
or
+ +
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+ + +
+ + +
+ +
+ + +
+
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml new file mode 100644 index 000000000..9145e6bed --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml @@ -0,0 +1,184 @@ +@using LearningHub.Nhs.Models.Enums +@using LearningHub.Nhs.Models.Enums.Report +@using LearningHub.Nhs.WebUI.Helpers +@using LearningHub.Nhs.WebUI.Models +@using LearningHub.Nhs.WebUI.Models.Learning +@model LearningHub.Nhs.WebUI.Models.Report.ReportHistoryViewModel +; +@{ + ViewData["Title"] = "Reports"; + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; + var allCourses = this.ViewData["AllCourses"] as List>; +} + +@section styles { + +} + +@section NavBreadcrumbs { + +
+
+
+
+ +
+ Home + +
+ +
+

Reports

+

View and manage your reports

+
+ +
+
+
+
+} +
+
+
+ + +
+
+

+ This page lists all reports you can access or have created. Use the Create a course completion report button to generate a new report. +

+ Create a course completion report +
+ +
+

Previously run reports

+

+ Reports are stored for 30 days from the date they’re generated. If you need to keep a copy, make sure you download it before it expires. +

+
+ @if (Model.ReportHistoryModels.Any()) + { + @foreach (var entry in Model.ReportHistoryModels) + { + var matchedCourseNames = new List(); + if (string.IsNullOrWhiteSpace(entry.CourseFilter)) + { + matchedCourseNames = allCourses.Select(course => course.Value).ToList(); + } + else + { + matchedCourseNames = allCourses + .Where(course => entry.CourseFilter.Contains(course.Key)) + .Select(course => course.Value) + .ToList(); + } + string datePeriod = entry.PeriodDays > 0 ? $"{entry.PeriodDays} days" : $"{entry.StartDate.GetValueOrDefault().ToString("dd MMM yyyy")} to {entry.EndDate.GetValueOrDefault().ToString("dd MMM yyyy")}"; + bool downloadCheck = entry.DownloadRequest != null && (bool)entry.DownloadRequest; + string expiryDate = entry.LastRun.AddDays(30).ToString("dd MMM yyyy"); + +
+ + + Course completion for @matchedCourseNames.FirstOrDefault()?.Normalize() + + @if (downloadCheck) + { + + @if (entry.ReportStatusId == ((int)Status.Ready) && DateTime.Now.Date < entry.LastRun.AddDays(30).Date) + { + + + Expires on @expiryDate + + + Ready to download + } + else if (entry.ReportStatusId == ((int)Status.Pending)) + { + Getting it ready + } + else + { + Expired + } + + } + +
+
+
+
Date requested:
+
@entry.FirstRun.Date.ToString("dd MMM yyyy")
+
+
+
Date period:
+
@datePeriod
+
+
+
Type:
+
Course completions
+
+
+
Reporting on:
+
+
    + @foreach(var item in matchedCourseNames) + { +
  • @item
  • + } +
+
+
+
+ +
    +
  • + + View report + +
  • + + @if (downloadCheck) + { + @if (entry.ReportStatusId == ((int)Status.Ready) && DateTime.Now.Date < entry.LastRun.AddDays(30).Date) + { +
  • + Download report +
  • + } + } +
+
+
+
+ } + + } + else + { +
    +
  • +
    +
    +

    + No reports available yet +

    +

    + You haven’t run any reports yet, so this section is currently empty. + Once you generate a report, it will appear here for you to view or download. +

    +
    +
    +
  • +
+ + } + +
+
+
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml new file mode 100644 index 000000000..5fe25a280 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml @@ -0,0 +1,76 @@ +@using System.Web; +@using LearningHub.Nhs.WebUI.Models.Learning +@using LearningHub.Nhs.WebUI.Models.Search; +@model LearningHub.Nhs.WebUI.Models.Report.CourseCompletionViewModel + + +@{ + var pagingModel = Model.ReportPaging; + var showPaging = pagingModel.CurrentPage >= 0 && pagingModel.CurrentPage <= pagingModel.TotalPages - 1; + var previousMessage = $"{pagingModel.CurrentPage} of {pagingModel.TotalPages}"; + int CurrentPageNumber = pagingModel.CurrentPage + 1; + var nextMessage = string.Empty; + if (CurrentPageNumber <= pagingModel.TotalPages) + { + nextMessage = $"{CurrentPageNumber + 1} of {pagingModel.TotalPages}"; + } + else + { + previousMessage = $"{CurrentPageNumber - 1} of {pagingModel.TotalPages}"; + nextMessage = $"{CurrentPageNumber} of {pagingModel.TotalPages}"; + } + + var routeData = new Dictionary(); + routeData["CurrentPageIndex"] = pagingModel.CurrentPage.ToString(); + var nextRouteData = new Dictionary(routeData); + var previousRouteData = new Dictionary(routeData); + nextRouteData["ReportFormActionType"] = ReportFormActionTypeEnum.NextPageChange.ToString(); + previousRouteData["ReportFormActionType"] = ReportFormActionTypeEnum.PreviousPageChange.ToString(); +} + +@if (pagingModel.TotalPages > 1) +{ + +} \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml new file mode 100644 index 000000000..492abdc07 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml @@ -0,0 +1,145 @@ +@model LearningHub.Nhs.WebUI.Models.Report.CourseCompletionViewModel + +@{ + var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}"; +} + + +@if (Model.TotalCount > 0) +{ +
+ + + + + + + + + + + + + + + + + + @foreach (var entry in Model.CourseCompletionRecords) + { + + + + + + + + + + + + + + + + } + + +
+ Username + + First Name + + Last Name + + Email Address + + Medical Council No + + Medical Council Name + + Role + + Grade + + Location + + Programme Name + + Course Learning Path Name +
+ Username + + @entry.UserName + + + First Name + @entry.FirstName + + + Last Name + @entry.LastName + + + Email Address + @entry.Email + + + Medical Council No + @entry.MedicalCouncilNo + + + Medical Council Name + @entry.MedicalCouncilName + + + Role + @entry.Role + + + Grade + @entry.Grade + + + Location + @entry.Location + + + Programme Name + @entry.Programme + + + Course Learning Path Name + @entry.Course + +
+
+} diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml new file mode 100644 index 000000000..8099618a4 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml @@ -0,0 +1,34 @@ +@using LearningHub.Nhs.WebUI.Models.DynamicCheckbox +@model DynamicCheckboxesViewModel +@{ + var propertyName = ViewData["PropertyName"]?.ToString() ?? "SelectedValues"; +} + +
+
+ + @Model.Label + + +
+ @for (int i = 0; i < Model.Checkboxes.Count; i++) + { + var checkbox = Model.Checkboxes[i]; + var inputId = $"{propertyName}_{i}"; + +
+ + + +
+ } +
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml index 43881e477..d98f78679 100644 --- a/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Default.cshtml @@ -50,7 +50,15 @@ } + @if (Model.ShowReports) + { +
  • + + Reports + +
  • + } @if (Model.ShowMyBookmarks) {
  • @@ -61,6 +69,7 @@
  • } + @if (Context.Request.Path.Value != "/Home/Error" && !SystemOffline()) { @if (Model.ShowHelp) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs new file mode 100644 index 000000000..d33e0d710 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs @@ -0,0 +1,60 @@ +namespace LearningHub.Nhs.OpenApi.Models.Configuration +{ + /// + /// DatabricksConfig + /// + public class DatabricksConfig + { + /// + /// Gets or sets the ResourceId for the databricks instance. + /// + public string ResourceId { get; set; } = null!; + + /// + /// Gets or sets the base url for the databricks instance. + /// + public string InstanceUrl { get; set; } = null!; + + /// + /// Gets or sets the warehouse id for databricks. + /// + public string WarehouseId { get; set; } = null!; + + /// + /// Gets or sets the job id for databricks. + /// + public string JobId { get; set; } = null!; + + /// + /// Gets or sets the tenant Id of the service pricncipl. + /// + public string TenantId { get; set; } = null!; + + /// + /// Gets or sets the client Id of the service pricncipl. + /// + public string ClientId { get; set; } = null!; + + /// + /// Gets or sets the client scret of the service pricncipl. + /// + public string clientSecret { get; set; } = null!; + + /// + /// Gets or sets the endpoint to check user permission. + /// + public string UserPermissionEndpoint { get; set; } = null!; + + + /// + /// Gets or sets the endpoint for course completion record. + /// + public string CourseCompletionEndpoint { get; set; } = null!; + + /// + /// Gets or sets the token. + /// + public string Token { get; set; } = null!; + + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/LearningHubConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/LearningHubConfig.cs index 81b91aa65..9815f4828 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/LearningHubConfig.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/LearningHubConfig.cs @@ -67,6 +67,11 @@ public class LearningHubConfig ///
    public string ContentManagementQueueName { get; set; } = null!; + /// + /// Gets or sets . + /// + public string DatabricksProcessingQueueName { get; set; } = null!; + /// /// Gets or sets . /// @@ -142,6 +147,15 @@ public class LearningHubConfig /// public string BrowseCataloguesUrl { get; set; } = null!; + /// + /// Gets or sets . + /// + public string ReportUrl { get; set; } = null!; + + /// + /// Gets or sets the StatMandId. + /// + public int StatMandId { get; set; } /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/NotificationSetting.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/NotificationSetting.cs index 7b9b7b5ba..a3ea9a1bf 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/NotificationSetting.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/NotificationSetting.cs @@ -55,5 +55,15 @@ public class NotificationSetting /// Gets or sets the ResourceContributeAccess. /// public string ResourceContributeAccess { get; set; } = null!; + + /// + /// Gets or sets the report title notification content. + /// + public string ReportTitle { get; set; } = null!; + + /// + /// Gets or sets the report notification content. + /// + public string Report { get; set; } = 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 cecf1b22a..ffcc3db8b 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/DatabricksNotification.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/DatabricksNotification.cs new file mode 100644 index 000000000..5ce62c6eb --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/DatabricksNotification.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace LearningHub.Nhs.OpenApi.Models.ViewModels +{ + /// + /// DatabricksNotification + /// + public class DatabricksNotification + { + /// + /// Gets or sets . + /// + [JsonProperty("event_type")] + public string EventType { get; set; } + + /// + /// Gets or sets . + /// + [JsonProperty("run")] + public RunInfo Run { get; set; } + + /// + /// RunInfo + /// + public class RunInfo + { + /// + /// Gets or sets . + /// + [JsonProperty("run_id")] + public long RunId { get; set; } + + /// + /// Gets or sets . + /// + [JsonProperty("parent_run_id")] + public long ParentRunId { get; set; } + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/NavigationModel.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/NavigationModel.cs index 912d99cae..c7ccc7862 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/NavigationModel.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/NavigationModel.cs @@ -75,6 +75,11 @@ public class NavigationModel /// public bool ShowBrowseCatalogues { get; set; } + /// + /// Gets or sets a value indicating whether to show reports. + /// + public bool ShowReports { get; set; } + /// /// Gets or sets a value indicating whether to show home. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj index a52fd6bf1..eb117ebe3 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IReportHistoryRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IReportHistoryRepository.cs new file mode 100644 index 000000000..b2aabcb95 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IReportHistoryRepository.cs @@ -0,0 +1,28 @@ +namespace LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using LearningHub.Nhs.Models.Entities; + using LearningHub.Nhs.Models.Entities.DatabricksReport; + + /// + /// The ProviderRepository interface. + /// + public interface IReportHistoryRepository : IGenericRepository + { + /// + /// The get by id async. + /// + /// The id. + /// The . + Task GetByIdAsync(int id); + + /// + /// The get by user id async. + /// + /// The userId. + /// The . + IQueryable GetByUserIdAsync(int userId); + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs index 1b8fe8783..80ec18a5c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs @@ -6,6 +6,7 @@ namespace LearningHub.Nhs.OpenApi.Repositories.EntityFramework using LearningHub.Nhs.Models.Entities; using LearningHub.Nhs.Models.Entities.Activity; using LearningHub.Nhs.Models.Entities.Content; + using LearningHub.Nhs.Models.Entities.DatabricksReport; using LearningHub.Nhs.Models.Entities.External; using LearningHub.Nhs.Models.Entities.Hierarchy; using LearningHub.Nhs.Models.Entities.Messaging; @@ -750,6 +751,11 @@ public LearningHubDbContextOptions Options /// public virtual DbSet UserProvider { get; set; } + /// + /// Gets or sets Report History. + /// + public virtual DbSet ReportHistory { get; set; } + /// /// Gets or sets Resource Version Provider. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs index a571d177a..197f95dcb 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs @@ -184,6 +184,7 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // External services.AddSingleton(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj index 46722121a..c6d370145 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj @@ -24,7 +24,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/ReportHistoryMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/ReportHistoryMap.cs new file mode 100644 index 000000000..3e398744c --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/ReportHistoryMap.cs @@ -0,0 +1,23 @@ +using LearningHub.Nhs.Models.Entities.DatabricksReport; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace LearningHub.Nhs.OpenApi.Repositories.Map +{ + /// + /// The ReportHistory Map. + /// + public class ReportHistoryMap : BaseEntityMap + { + /// + /// The internal map. + /// + /// + /// The model builder. + /// + protected override void InternalMap(EntityTypeBuilder modelBuilder) + { + modelBuilder.ToTable("ReportHistory", "Reports"); + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ReportHistoryRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ReportHistoryRepository.cs new file mode 100644 index 000000000..32bfd0458 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ReportHistoryRepository.cs @@ -0,0 +1,57 @@ +namespace LearningHub.Nhs.OpenApi.Repositories.Repositories +{ + using System.Collections.Generic; + using System.Data; + using System.Linq; + using System.Threading.Tasks; + using LearningHub.Nhs.Models.Entities; + using LearningHub.Nhs.Models.Entities.DatabricksReport; + using LearningHub.Nhs.Models.Entities.Hierarchy; + using LearningHub.Nhs.Models.Entities.Resource; + using LearningHub.Nhs.OpenApi.Repositories.EntityFramework; + using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; + using Microsoft.EntityFrameworkCore; + + /// + /// The provider repository. + /// + public class ReportHistoryRepository : GenericRepository, IReportHistoryRepository + { + /// + /// Initializes a new instance of the class. + /// + /// The db context. + /// The Timezone offset manager. + public ReportHistoryRepository(LearningHubDbContext dbContext, ITimezoneOffsetManager tzOffsetManager) + : base(dbContext, tzOffsetManager) + { + } + + /// + public async Task GetByIdAsync(int id) + { + return await DbContext.ReportHistory.AsNoTracking().FirstOrDefaultAsync(n => n.Id == id && !n.Deleted); + } + + /// + public IQueryable GetByUserIdAsync(int userId) + { + return DbContext.ReportHistory.AsNoTracking().Where(n => n.CreateUserId == userId && !n.Deleted); + } + + /// + /// The get by user id async. + /// + /// The user id. + /// The . + public IQueryable GetProvidersByUserIdAsync(int userId) + { + return DbContext.Set() + .Include(up => up.Provider) + .Where(up => up.UserId == userId && !up.Deleted).AsNoTracking() + .Select(up => up.Provider); + } + + + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Startup.cs index 767d35d19..37d56292d 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Startup.cs @@ -56,6 +56,7 @@ private static void AddRepositoryImplementations(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IDatabricksApiHttpClient.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IDatabricksApiHttpClient.cs new file mode 100644 index 000000000..f4226cd8f --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/HttpClients/IDatabricksApiHttpClient.cs @@ -0,0 +1,26 @@ +namespace LearningHub.Nhs.OpenApi.Services.Interface.HttpClients +{ + using System; + using System.Net.Http; + using System.Threading.Tasks; + + /// + /// The Bookmark Http Client interface. + /// + public interface IDatabricksApiHttpClient : IDisposable + { + /// + /// GETs data from Databricks API. + /// + /// The URL to make a get call to. + /// Optional authorization header. + /// A representing the result of the asynchronous operation. + Task GetData(string requestUrl, string? authHeader); + + /// + /// The Get Client method. + /// + /// The . + HttpClient GetClient(); + } +} 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 f6e92a8ce..ceef6ce67 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 @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IDatabricksService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IDatabricksService.cs new file mode 100644 index 000000000..10e0f358c --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IDatabricksService.cs @@ -0,0 +1,77 @@ +using LearningHub.Nhs.Models.Common; +using LearningHub.Nhs.Models.Databricks; +using LearningHub.Nhs.OpenApi.Models.ViewModels; +using System.Threading.Tasks; + +namespace LearningHub.Nhs.OpenApi.Services.Interface.Services +{ + /// + /// IDatabricks service + /// + public interface IDatabricksService + { + /// + /// IsUserReporter. + /// + /// The userId. + /// A representing the result of the asynchronous operation. + Task IsUserReporter(int userId); + + /// + /// CourseCompletionReport. + /// + /// The userId. + /// The model. + /// A representing the result of the asynchronous operation. + Task CourseCompletionReport(int userId, DatabricksRequestModel model); + + /// + /// CourseCompletionReport. + /// + /// The userId. + /// The page. + /// The pageSize. + /// A representing the result of the asynchronous operation. + Task> GetPagedReportHistory(int userId, int page, int pageSize); + + /// + /// GetPagedReportHistoryById. + /// + /// The userId. + /// The reportHistoryId. + /// A representing the result of the asynchronous operation. + Task GetPagedReportHistoryById(int userId, int reportHistoryId); + + /// + /// QueueReportDownload + /// + /// + /// + /// + Task QueueReportDownload(int userId, int reportHistoryId); + + /// + /// DownloadReport + /// + /// + /// + /// + Task DownloadReport(int userId, int reportHistoryId); + + /// + /// DatabricksJobUpdate. + /// + /// + /// + /// + Task DatabricksJobUpdate(int userId, DatabricksNotification databricksNotification); + + /// + /// DatabricksJobUpdate. + /// + /// userId. + /// databricksUpdateRequest. + /// + Task UpdateDatabricksReport(int userId, DatabricksUpdateRequest databricksUpdateRequest); + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/INotificationService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/INotificationService.cs index 0a0af21eb..217a77228 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/INotificationService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/INotificationService.cs @@ -69,5 +69,14 @@ public interface INotificationService /// Error message. /// The . Task CreatePublishFailedNotificationAsync(int userId, string resourceTitle, string errorMessage = ""); + + /// + /// Creates report processed notification. + /// + /// The current user id. + /// Report Name. + /// Report Content. + /// The . + Task CreateReportNotificationAsync(int userId, string reportName, string reportContent); } } \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/DatabricksApiHttpClient.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/DatabricksApiHttpClient.cs new file mode 100644 index 000000000..b5452240a --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/HttpClients/DatabricksApiHttpClient.cs @@ -0,0 +1,63 @@ +namespace LearningHub.Nhs.OpenApi.Services.HttpClients +{ + using System; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Threading.Tasks; + using IdentityModel.Client; + using LearningHub.Nhs.OpenApi.Models.Configuration; + using LearningHub.Nhs.OpenApi.Services.Interface.HttpClients; + using Microsoft.Extensions.Options; + using Microsoft.IdentityModel.Protocols.OpenIdConnect; + + /// + /// Http client for Databricks. + /// + public class DatabricksApiHttpClient : IDatabricksApiHttpClient + { + private readonly HttpClient httpClient; + private readonly IOptions databricksConfig; + + /// + /// Initializes a new instance of the class. + /// + /// Configuration details for the databricks. + public DatabricksApiHttpClient(IOptions databricksConfig) + { + this.databricksConfig = databricksConfig; + this.httpClient = new HttpClient { BaseAddress = new Uri(databricksConfig.Value.InstanceUrl) }; + this.httpClient.DefaultRequestHeaders.Accept.Clear(); + this.httpClient.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + } + + /// + public void Dispose() + { + this.httpClient.Dispose(); + } + + /// + /// The Get Client method. + /// + /// The . + public HttpClient GetClient() + { + string accessToken = this.databricksConfig.Value.Token; + this.httpClient.SetBearerToken(accessToken); + return this.httpClient; + } + + /// + public async Task GetData(string requestUrl, string? authHeader) + { + if (!string.IsNullOrEmpty(authHeader)) + { + this.httpClient.SetBearerToken(authHeader); + } + + var message = await this.httpClient.GetAsync(requestUrl).ConfigureAwait(false); + return message; + } + } +} 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 e2e064738..ccd7f9de7 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -30,7 +30,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs new file mode 100644 index 000000000..88705817e --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs @@ -0,0 +1,483 @@ +using LearningHub.Nhs.Models.Bookmark; +using LearningHub.Nhs.Models.Entities.Reporting; +using LearningHub.Nhs.OpenApi.Models.Configuration; +using LearningHub.Nhs.OpenApi.Services.HttpClients; +using LearningHub.Nhs.OpenApi.Services.Interface.HttpClients; +using LearningHub.Nhs.OpenApi.Services.Interface.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Text; +using System.Threading.Tasks; +using LearningHub.Nhs.Models.Databricks; +using System.Linq; +using System.Net.Http.Headers; +using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; +using LearningHub.Nhs.Models.Entities.DatabricksReport; +using AutoMapper; +using LearningHub.Nhs.Models.Entities.Activity; +using LearningHub.Nhs.Models.Resource.Activity; +using LearningHub.Nhs.Models.Common; +using LearningHub.Nhs.Models.Notification; +using Microsoft.EntityFrameworkCore; +using LearningHub.Nhs.Models.Enums.Report; +using Newtonsoft.Json.Linq; +using LearningHub.Nhs.OpenApi.Models.ViewModels; +using LearningHub.Nhs.Models.Resource; +using LearningHub.Nhs.OpenApi.Repositories.Repositories; +using LearningHub.Nhs.Models.Constants; +using LearningHub.Nhs.Models.Hierarchy; +using LearningHub.Nhs.Models.Enums; +using System.Text.Json; +using LearningHub.Nhs.Models.Entities.Resource; +using LearningHub.Nhs.Models.Entities; + +namespace LearningHub.Nhs.OpenApi.Services.Services +{ + /// + /// DatabricksService + /// + public class DatabricksService : IDatabricksService + { + private const string CacheKey = "DatabricksReporter"; + private readonly IOptions databricksConfig; + private readonly IOptions learningHubConfig; + private readonly IReportHistoryRepository reportHistoryRepository; + private readonly IQueueCommunicatorService queueCommunicatorService; + private readonly ICachingService cachingService; + private readonly INotificationService notificationService; + private readonly IUserNotificationService userNotificationService; + private readonly IMoodleApiService moodleApiService; + private readonly IMapper mapper; + + /// + /// Initializes a new instance of the class. + /// + /// databricksConfig. + /// learningHubConfig. + /// reportHistoryRepository. + /// mapper. + /// queueCommunicatorService. + /// cachingService. + /// notificationService. + /// userNotificationService. + /// moodleApiService. + public DatabricksService(IOptions databricksConfig,IOptions learningHubConfig, IReportHistoryRepository reportHistoryRepository, IMapper mapper, IQueueCommunicatorService queueCommunicatorService, ICachingService cachingService, INotificationService notificationService, IUserNotificationService userNotificationService, IMoodleApiService moodleApiService) + { + this.databricksConfig = databricksConfig; + this.learningHubConfig = learningHubConfig; + this.reportHistoryRepository = reportHistoryRepository; + this.mapper = mapper; + this.queueCommunicatorService = queueCommunicatorService; + this.cachingService = cachingService; + this.notificationService = notificationService; + this.userNotificationService = userNotificationService; + this.moodleApiService = moodleApiService; + } + + /// + public async Task IsUserReporter(int userId) + { + string cacheKey = $"{CacheKey}_{userId}"; + var userReportPermission = await this.cachingService.GetAsync(cacheKey); + if (userReportPermission.ResponseEnum == CacheReadResponseEnum.Found) + { + return userReportPermission.Item; + } + + + DatabricksApiHttpClient databricksInstance = new DatabricksApiHttpClient(this.databricksConfig); + + var sqlText = $"CALL {this.databricksConfig.Value.UserPermissionEndpoint}({userId});"; + const string requestUrl = "/api/2.0/sql/statements"; + + var requestPayload = new + { + warehouse_id = this.databricksConfig.Value.WarehouseId, + statement = sqlText, + wait_timeout = "30s", + on_wait_timeout = "CANCEL" + }; + + var jsonBody = JsonConvert.SerializeObject(requestPayload); + using var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + + var response = await databricksInstance.GetClient().PostAsync(requestUrl, content); + + var databricksResponse = await databricksInstance.GetClient().PostAsync(requestUrl, content); + if (databricksResponse.StatusCode is not HttpStatusCode.OK) + { + //log failure + return false; + } + var responseResult = await databricksResponse.Content.ReadAsStringAsync(); + + responseResult = responseResult.Trim(); + var root = JsonDocument.Parse(responseResult).RootElement; + string data = root.GetProperty("result").GetProperty("data_array")[0][0].GetString(); + bool isReporter = data == "1"; + + await this.cachingService.SetAsync(cacheKey, isReporter); + return isReporter; + } + + /// + public async Task CourseCompletionReport(int userId, DatabricksRequestModel model) + { + userId = 22527; + newEntry: + if (model.ReportHistoryId == 0 && model.Take > 1) + { + + bool timePeriodCheck = int.TryParse(model.TimePeriod, out int timePeriod); + var reportHistory = new ReportHistory { CourseFilter = string.Join(",", model.Courses) ,StartDate = model.StartDate,EndDate =model.EndDate, PeriodDays = timePeriodCheck ? timePeriod : 0 , + FirstRun = DateTimeOffset.Now, LastRun = DateTimeOffset.Now, ReportStatusId = 2}; + model.ReportHistoryId = await AddReportHistory(userId, reportHistory); + } + else if(model.ReportHistoryId > 0 && model.Take > 1) + { + //get the existing values and compare + var reportChecker = await GetPagedReportHistoryById(userId, model.ReportHistoryId); + if (reportChecker != null) + { + if(reportChecker.CourseFilter == "all") { reportChecker.CourseFilter = string.Empty; } + if(reportChecker.CourseFilter != string.Join(",", model.Courses) || reportChecker.StartDate.GetValueOrDefault().Date != model.StartDate.GetValueOrDefault().Date || reportChecker.EndDate.GetValueOrDefault().Date != model.EndDate.GetValueOrDefault().Date) + { + model.ReportHistoryId = 0; + goto newEntry; + } + } + await UpdateReportLastRunTime(userId, model.ReportHistoryId); + } + + DatabricksApiHttpClient databricksInstance = new DatabricksApiHttpClient(this.databricksConfig); + + const string requestUrl = "/api/2.0/sql/statements"; + + var sql = $@"CALL {this.databricksConfig.Value.CourseCompletionEndpoint}(:par_adminId, :par_completionFlag, :par_locationId, :par_catalogueId, :par_learnerId, :par_courseId, :par_PageSize, :par_PageNumber, :par_Date_from, :par_Date_to);"; + + + var parameters = new List> + { + new("par_adminId", userId), + new("par_completionFlag", -1), + new("par_locationId", -1), + new("par_catalogueId", -1), + new("par_learnerId", -1), + new("par_courseId", model.Courses.Count < 1 ? string.Empty : string.Join(",", model.Courses)), + new("par_PageSize", model.Take), + new("par_PageNumber", model.Skip), + new("par_Date_from", model.StartDate.HasValue ? model.StartDate.Value.ToString("yyyy-MM-dd"): string.Empty), + new("par_Date_to", model.EndDate.HasValue ? model.EndDate.Value.ToString("yyyy-MM-dd"): string.Empty), + }; + + var formattedParams = parameters.Select(p => new { name = p.Key, value = p.Value }); + + var body = new + { + warehouse_id = this.databricksConfig.Value.WarehouseId, + statement = sql, + parameters = formattedParams, + wait_timeout = "30s", + on_wait_timeout = "CANCEL" + }; + + var json = JsonConvert.SerializeObject(body); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await databricksInstance.GetClient().PostAsync(requestUrl, content); + + var databricksResponse = await databricksInstance.GetClient().PostAsync(requestUrl, content); + if (databricksResponse.StatusCode is not HttpStatusCode.OK) + { + //log failure + return new DatabricksDetailedViewModel { ReportHistoryId = model.ReportHistoryId }; + } + var responseResult = await databricksResponse.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(responseResult); + if (result != null && result.Result.DataArray != null) + { + var records = MapDataArrayToCourseCompletionRecords(result.Result.DataArray); + return new DatabricksDetailedViewModel { CourseCompletionRecords = records, ReportHistoryId = model.ReportHistoryId }; + + } + + return new DatabricksDetailedViewModel { CourseCompletionRecords= new List(), ReportHistoryId = model.ReportHistoryId }; + } + + /// + public async Task> GetPagedReportHistory(int userId,int page, int pageSize) + { + userId = 22527; + var result = new PagedResultSet(); + var query = this.reportHistoryRepository.GetByUserIdAsync(userId); + + // Execute async count + result.TotalItemCount = await query.CountAsync(); + try + { + // Execute async paging + var pagedItems = await query + .OrderByDescending(x => x.LastRun) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + result.Items = mapper.Map>(pagedItems); + } + catch(Exception e) + { + + } + + return result; + } + + /// + public async Task GetPagedReportHistoryById(int userId, int reportHistoryId) + { + userId = 22527; + var result = new ReportHistoryModel(); + + var reportHistory = await this.reportHistoryRepository.GetByIdAsync(reportHistoryId); + if(reportHistory != null) + { + if(reportHistory.CreateUserId != userId) + { + throw new Exception("Invalid Id"); + } + } + result = mapper.Map(reportHistory); + if(result != null && string.IsNullOrWhiteSpace(result.CourseFilter)) + { + result.CourseFilter = "all"; + } + + return result; + + } + + /// + public async Task QueueReportDownload(int userId, int reportHistoryId) + { + userId = 22527; + var result = new ReportHistoryModel(); + + var reportHistory = await this.reportHistoryRepository.GetByIdAsync(reportHistoryId); + if (reportHistory != null && reportHistory.DownloadRequest == null) + { + if (reportHistory.CreateUserId != userId) + { + throw new Exception("Invalid Id"); + } + reportHistory.DownloadRequest = true; + reportHistory.DownloadRequested = DateTimeOffset.Now; + } + else + { + throw new Exception("Invalid Id"); + } + //call the job + DatabricksApiHttpClient databricksInstance = new DatabricksApiHttpClient(this.databricksConfig); + + const string requestUrl = "/api/2.1/jobs/run-now"; + + var body = new + { + job_id = this.databricksConfig.Value.JobId, + notebook_params = new + { + par_adminId = userId, + par_completionFlag = -1, + par_locationId = -1, + par_catalogueId = -1, + par_learnerId = -1, + par_courseId = reportHistory.CourseFilter, + par_PageSize = 0, + par_PageNumber = 0, + par_Date_from = reportHistory.StartDate.GetValueOrDefault().ToString("yyyy-MM-dd"), + par_Date_to = reportHistory.EndDate.GetValueOrDefault().ToString("yyyy-MM-dd") + } + }; + + var json = JsonConvert.SerializeObject(body); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var databricksResponse = await databricksInstance.GetClient().PostAsync(requestUrl, content); + if (databricksResponse.StatusCode is not HttpStatusCode.OK) + { + reportHistory.ProcessingMessage = databricksResponse.ReasonPhrase; + reportHistory.ReportStatusId = (int)Nhs.Models.Enums.Report.Status.Failed; + await reportHistoryRepository.UpdateAsync(userId, reportHistory); + return false; + } + var responseResult = await databricksResponse.Content.ReadAsStringAsync(); + var responseData = JsonConvert.DeserializeObject(responseResult); + if (responseData != null) + { + reportHistory.ReportStatusId = (int)Nhs.Models.Enums.Report.Status.Pending; + reportHistory.ParentJobRunId = (long)responseData.run_id; + await reportHistoryRepository.UpdateAsync(userId, reportHistory); + return true; + } + return false; + } + + /// + public async Task DownloadReport(int userId, int reportHistoryId) + { + userId = 22527; + var response = new ReportHistoryModel(); + + var reportHistory = await this.reportHistoryRepository.GetByIdAsync(reportHistoryId); + if (reportHistory != null) + { + if (reportHistory.CreateUserId != userId) + { + throw new Exception("Invalid Id"); + } + reportHistory.DownloadedDate = DateTimeOffset.Now; + await reportHistoryRepository.UpdateAsync(userId, reportHistory); + response = mapper.Map(reportHistory); + } + else + { + throw new Exception("Invalid Id"); + } + + return response; + } + + /// + /// DatabricksJobUpdate. + /// + /// + /// + /// + public async Task DatabricksJobUpdate(int userId, DatabricksNotification databricksNotification) + { + var reportHistory = await this.reportHistoryRepository.GetAll().FirstOrDefaultAsync(x=>x.ParentJobRunId == databricksNotification.Run.ParentRunId); + if (reportHistory == null) { return; } + reportHistory.JobRunId = databricksNotification.Run.RunId; + if (!databricksNotification.EventType.Contains("success")) + { + reportHistory.ReportStatusId = (int)Nhs.Models.Enums.Report.Status.Failed; + reportHistoryRepository.Update(userId, reportHistory); + return; + } + reportHistoryRepository.Update(userId, reportHistory); + + await this.queueCommunicatorService.SendAsync(this.learningHubConfig.Value.DatabricksProcessingQueueName, databricksNotification.Run.RunId); + return; + } + + + /// + /// DatabricksJobUpdate. + /// + /// userId. + /// databricksUpdateRequest. + /// + public async Task UpdateDatabricksReport(int userId, DatabricksUpdateRequest databricksUpdateRequest) + { + var reportHistory = await this.reportHistoryRepository.GetAll().FirstOrDefaultAsync(x => x.JobRunId == databricksUpdateRequest.RunId); + if (reportHistory == null) { return; } + if(string.IsNullOrWhiteSpace(databricksUpdateRequest.ProcessingMessage)) + { + reportHistory.DownloadReady = DateTimeOffset.Now; + reportHistory.FilePath = databricksUpdateRequest.FilePath; + reportHistory.ReportStatusId = (int)Nhs.Models.Enums.Report.Status.Ready; + //send notification + string firstCourse = string.Empty; + + var courses = await moodleApiService.GetCoursesByCategoryIdAsync(learningHubConfig.Value.StatMandId); + + firstCourse = string.IsNullOrWhiteSpace(reportHistory.CourseFilter) + ? courses.Courses.Select(c => c.Displayname).FirstOrDefault() + : courses.Courses + .Where(c => reportHistory.CourseFilter.Contains(c.Id.ToString())) + .Select(c => c.Displayname) + .FirstOrDefault(); + + + var notificationId = await this.notificationService.CreateReportNotificationAsync(userId, "Course Completion", firstCourse); + + if (notificationId > 0) + { + await this.userNotificationService.CreateAsync(userId, new UserNotification { UserId = reportHistory.CreateUserId, NotificationId = notificationId }); + } + } + else + { + reportHistory.ProcessingMessage = databricksUpdateRequest.ProcessingMessage; + reportHistory.ReportStatusId = (int)Nhs.Models.Enums.Report.Status.Failed; + } + + reportHistoryRepository.Update(userId, reportHistory); + return; + } + + + private async Task AddReportHistory(int userId,ReportHistory model) + { + return await reportHistoryRepository.CreateAsync(userId, model); + } + + private async Task UpdateReportLastRunTime(int userId, int reportHistoryId) + { + var entry = await reportHistoryRepository.GetByIdAsync(reportHistoryId); + entry.LastRun = DateTime.Now; + await reportHistoryRepository.UpdateAsync(userId, entry); + } + + /// + /// MapDataArrayToCourseCompletionRecords. + /// + /// + /// + public static List MapDataArrayToCourseCompletionRecords(List> dataArray) + { + var records = new List(); + + foreach (var row in dataArray) + { + if (row == null || row.Count < 19) continue; + + var record = new DatabricksDetailedItemViewModel + { + UserName = row[0]?.ToString(), + FirstName = row[1]?.ToString(), + LastName = row[2]?.ToString(), + Email = row[3]?.ToString(), + Programme = row[4]?.ToString(), + Course = row[5]?.ToString(), + CourseStatus = row[6]?.ToString(), + Location = row[7]?.ToString(), + Role = row[8]?.ToString(), + Grade = row[9]?.ToString(), + MedicalCouncilNo = row[10]?.ToString(), + MedicalCouncilName = row[11]?.ToString(), + LastAccess = row[12]?.ToString(), + CourseCompletionDate = row[13]?.ToString(), + ReferenceType = row[14]?.ToString(), + ReferenceValue = row[15]?.ToString(), + PermissionType = row[16]?.ToString(), + MinValidDate = row[17]?.ToString(), + TotalRows = row[18] != null && int.TryParse(row[18].ToString(), out int totalRows) ? totalRows : 0 + }; + + records.Add(record); + } + + return records; + } + + } + +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs index a8c6ceb78..9e28c5041 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs @@ -13,16 +13,19 @@ public class NavigationPermissionService : INavigationPermissionService { private readonly IResourceService resourceService; private readonly IUserGroupService userGroupService; + private readonly IDatabricksService databricksService; /// /// Initializes a new instance of the class. /// /// Resource service. /// userGroup service. - public NavigationPermissionService(IResourceService resourceService, IUserGroupService userGroupService) + /// databricksService. + public NavigationPermissionService(IResourceService resourceService, IUserGroupService userGroupService, IDatabricksService databricksService) { this.resourceService = resourceService; this.userGroupService = userGroupService; + this.databricksService = databricksService; } /// @@ -86,6 +89,7 @@ public NavigationModel NotAuthenticated() ShowSignOut = false, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } @@ -111,6 +115,7 @@ private NavigationModel AuthenticatedAdministrator(string controllerName) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = true, }; } @@ -137,6 +142,7 @@ private async Task AuthenticatedBlueUser(string controllerName, ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = await this.databricksService.IsUserReporter(userId), }; } @@ -161,6 +167,7 @@ private NavigationModel AuthenticatedGuest() ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } @@ -186,6 +193,7 @@ private async Task AuthenticatedReadOnly(string controllerName, ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = true, + ShowReports = await this.databricksService.IsUserReporter(userId), }; } @@ -210,6 +218,7 @@ private async Task AuthenticatedBasicUserOnly(int userId) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, + ShowReports = false, }; } @@ -234,6 +243,7 @@ private NavigationModel InLoginWizard() ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = false, + ShowReports = false, }; } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationService.cs index fe002695a..e001da091 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NotificationService.cs @@ -195,6 +195,34 @@ public async Task CreateResourcePublishedNotificationAsync(int userId, stri } } + /// + /// Creates report processed notification. + /// + /// The current user id. + /// Report Name. + /// Report Content. + /// The . + public async Task CreateReportNotificationAsync(int userId, string reportName, string reportContent) + { + var title = learningHubConfig.Notifications.ReportTitle.Replace("[ReportName]", reportName).Replace("[ReportContent]", reportContent); + + var message = learningHubConfig.Notifications.Report.Replace("[ReportName]", reportName).Replace("[ReportContent]", reportContent); + + + var notification = await this.CreateAsync(userId, this.UserSpecificNotification( + title, message, NotificationTypeEnum.ReportProcessed, NotificationPriorityEnum.General)); + + if (notification.CreatedId.HasValue) + { + return notification.CreatedId.Value; + } + else + { + return 0; + } + } + + /// /// Creates resource publish failed notification. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs index 87c94f64e..870ccba05 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs @@ -24,6 +24,7 @@ public static void AddServices(this IServiceCollection services) { services.AddScoped(); services.AddHttpClient(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -36,6 +37,7 @@ public static void AddServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj index 35c780917..ac9eaae79 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs index 7d5a61795..1c5a692e3 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs @@ -45,6 +45,11 @@ public static class ConfigurationExtensions /// public const string MoodleSectionName = "Moodle"; + /// + /// The DatabricksSectionName. + /// + public const string DatabricksSectionName = "Databricks"; + /// /// Adds config. /// @@ -65,6 +70,8 @@ public static void AddConfig(this IServiceCollection services, IConfiguration co services.AddOptions().Bind(config.GetSection(AzureSectionName)); services.AddOptions().Bind(config.GetSection(MoodleSectionName)); + + services.AddOptions().Bind(config.GetSection(DatabricksSectionName)); } private static OptionsBuilder RegisterPostConfigure(this OptionsBuilder builder) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ReportController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ReportController.cs new file mode 100644 index 000000000..faee4af05 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ReportController.cs @@ -0,0 +1,129 @@ +namespace LearningHub.NHS.OpenAPI.Controllers +{ + using System.Threading.Tasks; + using LearningHub.Nhs.Models.Common; + using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Paging; + using LearningHub.Nhs.OpenApi.Models.ViewModels; + using LearningHub.Nhs.OpenApi.Services.Interface.Services; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + + /// + /// Report Controller. + /// + [ApiController] + [Authorize(Policy = "AuthorizeOrCallFromLH")] + [Route("Report")] + public class ReportController : OpenApiControllerBase + { + private readonly IDatabricksService databricksService; + + /// + /// Initializes a new instance of the class. + /// + /// The catalogue service. + public ReportController(IDatabricksService databricksService) + { + this.databricksService = databricksService; + } + + /// + /// Get all catalogues. + /// + /// Task. + [HttpGet] + [Route("GetReporterPermission")] + public async Task GetReporterPermission() + { + return await this.databricksService.IsUserReporter(this.CurrentUserId.GetValueOrDefault()); + } + + /// + /// Get CourseCompletionReport from Databricks. + /// + /// requestModel. + /// Task. + [HttpPost] + [Route("GetCourseCompletionReport")] + public async Task CourseCompletionReport(DatabricksRequestModel requestModel) + { + return await this.databricksService.CourseCompletionReport(this.CurrentUserId.GetValueOrDefault(),requestModel); + } + + /// + /// Get CourseCompletionReport from Databricks. + /// + /// request. + /// Task. + [HttpPost] + [Route("GetReportHistory")] + public async Task> GetReportHistory(PagingRequestModel request) + { + return await this.databricksService.GetPagedReportHistory(this.CurrentUserId.GetValueOrDefault(), request.Page, request.PageSize); + } + + /// + /// Get GetReportHistoryById. + /// + /// reportHistoryId. + /// Task. + [HttpGet] + [Route("GetReportHistoryById/{reportHistoryId}")] + public async Task GetReportHistoryById(int reportHistoryId) + { + return await this.databricksService.GetPagedReportHistoryById(this.CurrentUserId.GetValueOrDefault(), reportHistoryId); + } + + /// + /// Get QueueReportDownload. + /// + /// reportHistoryId. + /// Task. + [HttpGet] + [Route("QueueReportDownload/{reportHistoryId}")] + public async Task QueueReportDownload(int reportHistoryId) + { + return await this.databricksService.QueueReportDownload(this.CurrentUserId.GetValueOrDefault(), reportHistoryId); + } + + /// + /// Get DownloadReport. + /// + /// reportHistoryId. + /// Task. + [HttpGet] + [Route("DownloadReport/{reportHistoryId}")] + public async Task DownloadReport(int reportHistoryId) + { + return await this.databricksService.DownloadReport(this.CurrentUserId.GetValueOrDefault(), reportHistoryId); + } + + /// + /// DatabricksJobNotify. + /// + /// databricksNotification. + /// Task. + [HttpPost] + [AllowAnonymous] + [Route("DatabricksJobNotify")] + public async Task DatabricksJobNotify(DatabricksNotification databricksNotification) + { + await this.databricksService.DatabricksJobUpdate(this.CurrentUserId.GetValueOrDefault(), databricksNotification); + return this.Ok(new ApiResponse(true)); + } + + /// + /// UpdateDatabricksReport. + /// + /// databricksUpdateRequest. + /// Task. + [HttpPost] + [Route("UpdateDatabricksReport")] + public async Task UpdateDatabricksReport(DatabricksUpdateRequest databricksUpdateRequest) + { + await this.databricksService.UpdateDatabricksReport(this.CurrentUserId.GetValueOrDefault(), databricksUpdateRequest); + return this.Ok(new ApiResponse(true)); + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/UserController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/UserController.cs index 98590761f..54d043086 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/UserController.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/UserController.cs @@ -353,7 +353,6 @@ public async Task>> GetLHUserNavigation() return this.MenuItems(model); } - private List> MenuItems(NavigationModel model) { var menu = new List> @@ -382,6 +381,12 @@ private List> MenuItems(NavigationModel model) { "url", this.learningHubConfig.BrowseCataloguesUrl }, { "visible", model.ShowBrowseCatalogues }, }, + new Dictionary + { + { "title", "Reports" }, + { "url", this.learningHubConfig.ReportUrl }, + { "visible", model.ShowReports }, + }, new Dictionary { { "title", "Admin" }, diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj index f23f74ba3..34579b31b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj @@ -19,7 +19,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user index 5bdd12166..1d8db97aa 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user @@ -5,7 +5,7 @@ IIS Local - ApiControllerEmptyScaffolder - root/Common/Api + MvcControllerEmptyScaffolder + root/Common/MVC/Controller \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index bec2f2309..4d62b872c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -80,6 +80,7 @@ "ResourcePublishQueueRouteName": "", "HierarchyEditPublishQueueName": "", "ContentManagementQueueName": "", + "DatabricksProcessingQueueName": "", "AuthClientIdentityKey": "", "LHClientIdentityKey": "", "ReportApiClientIdentityKey": "", @@ -93,7 +94,9 @@ "ResourcePublishFailedWithReason": "

    The resource you contributed failed to publish, which means that users cannot access it.

    The error message generated was:
    [ErrorMessage]

    Please contact the support team for more information.

    ", "ResourceAccessTitle": "What you can do in the Learning Hub has changed", "ResourceReadonlyAccess": "

    You can continue to search for and access resources in the Learning Hub, however you cannot contribute to it.

    If you have any questions about this, please contact the support team.

    ", - "ResourceContributeAccess": "

    You can now contribute to the Learning Hub and continue to search for and access resources.

    If you have any questions about this, please contact the support team.

    " + "ResourceContributeAccess": "

    You can now contribute to the Learning Hub and continue to search for and access resources.

    If you have any questions about this, please contact the support team.

    ", + "ReportTitle": "

    [ReportName] report for [ReportContent] is ready

    ", + "Report": "

    Content: Your report [ReportName] report for [ReportContent] is ready. You can view and download the report in the Reports Section section

    " }, "MyContributionsUrl": "/my-contributions", "MyLearningUrl": "/MyLearning", @@ -107,7 +110,8 @@ "RegisterUrl": "/register", "SignOutUrl": "/home/logout", "MyAccountUrl": "/myaccount", - "BrowseCataloguesUrl": "/allcatalogue" + "BrowseCataloguesUrl": "/allcatalogue", + "ReportUrl": "" }, "LearningHubAPIConfig": { "BaseUrl": "https://learninghub.nhs.uk/api" @@ -126,5 +130,13 @@ "ApiBaseUrl": "", "ApiWsRestFormat": "json", "ApiWsToken": "" + }, + "Databricks": { + "InstanceUrl": "", + "Token": "", + "WarehouseId": "", + "JobId": "", + "UserPermissionEndpoint": "", + "CourseCompletionEndpoint": "" } } diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index a88a4bcff..1c0d8ebee 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -103,6 +103,7 @@ +
    @@ -691,6 +692,7 @@ + diff --git a/WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql b/WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql new file mode 100644 index 000000000..61c2cec72 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql @@ -0,0 +1,25 @@ +CREATE TABLE [reports].[ReportHistory] +( + [Id] INT NOT NULL IDENTITY (1, 1), + [CourseFilter] NVARCHAR(512) NULL, + [FirstRun] DATETIMEOFFSET(7) NOT NULL, + [LastRun] DATETIMEOFFSET(7) NOT NULL, + [PeriodDays] INT NOT NULL, + [StartDate][datetimeoffset](7) NULL, + [EndDate][datetimeoffset](7) NULL, + [DownloadRequest] BIT NULL, + [DownloadRequested][datetimeoffset](7) NULL, + [DownloadReady][datetimeoffset](7) NULL, + [ReportStatusId] INT NULL, + [FilePath] NVARCHAR(1024) NULL, + [DownloadedDate] [datetimeoffset](7) NULL, + [ParentJobRunId] INT NULL, + [JobRunId] INT NULL, + [ProcessingMessage] NVARCHAR(1024) NULL, + [Deleted] [bit] NOT NULL, + [CreateUserId] INT NOT NULL, + [CreateDate] [datetimeoffset](7) NOT NULL, + [AmendUserId] INT NOT NULL, + [AmendDate] [datetimeoffset](7) NOT NULL, + CONSTRAINT [PK_Reports_ReportHistory] PRIMARY KEY CLUSTERED ([Id] ASC) ON [PRIMARY] +); \ No newline at end of file diff --git a/replacements.txt b/replacements.txt new file mode 100644 index 000000000..4819f42b1 --- /dev/null +++ b/replacements.txt @@ -0,0 +1 @@ +Databricks_API_Token==>REMOVED \ No newline at end of file From 7af543deeb12c8afb6f43fa2ab3891c188784d14 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Wed, 26 Nov 2025 11:00:06 +0000 Subject: [PATCH 012/106] LH model Update --- .../LearningHub.Nhs.WebUI.AutomatedUiTests.csproj | 2 +- LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Models.csproj | 4 ++-- .../LearningHub.Nhs.OpenApi.Repositories.Interface.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Repositories.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Services.Interface.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Services.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Tests.csproj | 2 +- .../LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj index 2f201378f..f474d537d 100644 --- a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index 09cb454c3..adada3f14 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -113,7 +113,7 @@ - + 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 ffcc3db8b..2991f677e 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -16,7 +16,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj index eb117ebe3..e7e923978 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj index c6d370145..0ee6fa49a 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj @@ -24,7 +24,7 @@ - + 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 ceef6ce67..99e4fe969 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 @@ -17,7 +17,7 @@ - + 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 ccd7f9de7..7c534adfc 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -30,7 +30,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj index ac9eaae79..3ecb4d1a5 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj index 34579b31b..cfc4572c1 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj @@ -19,7 +19,7 @@ - + From 84b2bf6ab60ea907a113ad49daec328d6d4ac156 Mon Sep 17 00:00:00 2001 From: Binon Date: Wed, 26 Nov 2025 15:43:07 +0000 Subject: [PATCH 013/106] Implementing search admin, handling index and indexer --- .../Configuration/AzureSearchConfig.cs | 33 ++ .../Controllers/AzureSearchAdmin.cs | 119 +++++++ .../Interfaces/IAzureSearchAdminService.cs | 38 +++ .../LearningHub.Nhs.AdminUI.csproj | 2 +- .../LearningHub.Nhs.AdminUI.csproj.user | 4 + .../Models/AzureSearchAdminViewModel.cs | 30 ++ .../Models/IndexStatusViewModel.cs | 23 ++ .../Models/IndexerStatusViewModel.cs | 45 +++ .../ServiceCollectionExtension.cs | 37 ++- .../Services/AzureSearchAdminService.cs | 299 ++++++++++++++++++ .../Views/AzureSearchAdmin/Index.cshtml | 222 +++++++++++++ .../Views/Shared/_NavPartial.cshtml | 13 +- .../LearningHub.Nhs.AdminUI/appsettings.json | 7 + ...rningHub.Nhs.WebUI.AutomatedUiTests.csproj | 2 +- .../LearningHub.Nhs.WebUI.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Models.csproj | 2 +- ....Nhs.OpenApi.Repositories.Interface.csproj | 2 +- ...earningHub.Nhs.OpenApi.Repositories.csproj | 2 +- ...gHub.Nhs.OpenApi.Services.Interface.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Services.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Tests.csproj | 2 +- .../LearningHub.NHS.OpenAPI.csproj | 2 +- ...ub.Nhs.ReportApi.Services.Interface.csproj | 2 +- ...ub.Nhs.ReportApi.Services.UnitTests.csproj | 2 +- .../LearningHub.Nhs.ReportApi.Services.csproj | 2 +- .../LearningHub.Nhs.ReportApi.Shared.csproj | 2 +- .../LearningHub.Nhs.ReportApi.csproj | 2 +- .../LearningHub.Nhs.Api.csproj | 2 +- .../LearningHub.Nhs.Api.Shared.csproj | 2 +- .../LearningHub.Nhs.Api.UnitTests.csproj | 2 +- ...earningHub.Nhs.Repository.Interface.csproj | 2 +- .../LearningHub.Nhs.Repository.csproj | 2 +- .../LearningHub.Nhs.Services.Interface.csproj | 2 +- .../LearningHub.Nhs.Services.UnitTests.csproj | 2 +- .../LearningHub.Nhs.Services.csproj | 2 +- ...earningHub.Nhs.Migration.ConsoleApp.csproj | 2 +- ...LearningHub.Nhs.Migration.Interface.csproj | 2 +- .../LearningHub.Nhs.Migration.Models.csproj | 2 +- ...ub.Nhs.Migration.Staging.Repository.csproj | 2 +- ...LearningHub.Nhs.Migration.UnitTests.csproj | 2 +- .../LearningHub.Nhs.Migration.csproj | 2 +- 41 files changed, 882 insertions(+), 46 deletions(-) create mode 100644 AdminUI/LearningHub.Nhs.AdminUI/Configuration/AzureSearchConfig.cs create mode 100644 AdminUI/LearningHub.Nhs.AdminUI/Controllers/AzureSearchAdmin.cs create mode 100644 AdminUI/LearningHub.Nhs.AdminUI/Interfaces/IAzureSearchAdminService.cs create mode 100644 AdminUI/LearningHub.Nhs.AdminUI/Models/AzureSearchAdminViewModel.cs create mode 100644 AdminUI/LearningHub.Nhs.AdminUI/Models/IndexStatusViewModel.cs create mode 100644 AdminUI/LearningHub.Nhs.AdminUI/Models/IndexerStatusViewModel.cs create mode 100644 AdminUI/LearningHub.Nhs.AdminUI/Services/AzureSearchAdminService.cs create mode 100644 AdminUI/LearningHub.Nhs.AdminUI/Views/AzureSearchAdmin/Index.cshtml diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Configuration/AzureSearchConfig.cs b/AdminUI/LearningHub.Nhs.AdminUI/Configuration/AzureSearchConfig.cs new file mode 100644 index 000000000..d40d2135d --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/Configuration/AzureSearchConfig.cs @@ -0,0 +1,33 @@ +namespace LearningHub.Nhs.AdminUI.Configuration +{ + /// + /// Configuration settings for Azure AI Search. + /// + public class AzureSearchConfig + { + /// + /// Gets or sets the Azure Search service endpoint URL. + /// + public string ServiceEndpoint { get; set; } + + /// + /// Gets or sets the admin API key for managing indexes and indexers. + /// + public string AdminApiKey { get; set; } + + /// + /// Gets or sets the query API key for search operations. + /// + public string QueryApiKey { get; set; } + + /// + /// Gets or sets the name of the search index. + /// + public string IndexName { get; set; } + + /// + /// Gets or sets the name of the indexer. + /// + public string IndexerName { get; set; } + } +} \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Controllers/AzureSearchAdmin.cs b/AdminUI/LearningHub.Nhs.AdminUI/Controllers/AzureSearchAdmin.cs new file mode 100644 index 000000000..5e75160ea --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/Controllers/AzureSearchAdmin.cs @@ -0,0 +1,119 @@ +namespace LearningHub.Nhs.AdminUI.Controllers +{ + using System.Threading.Tasks; + using LearningHub.Nhs.AdminUI.Helpers; + using LearningHub.Nhs.AdminUI.Interfaces; + using LearningHub.Nhs.AdminUI.Models; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Logging; + using Microsoft.FeatureManagement; + + /// + /// Controller for Azure Search administration. + /// + public class AzureSearchAdminController : BaseController + { + private readonly IAzureSearchAdminService azureSearchAdminService; + private readonly IFeatureManager featureManager; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The hosting environment. + /// The Azure Search admin service. + /// The feature manager. + /// The logger. + public AzureSearchAdminController( + IWebHostEnvironment hostingEnvironment, + IAzureSearchAdminService azureSearchAdminService, + IFeatureManager featureManager, + ILogger logger) + : base(hostingEnvironment) + { + this.azureSearchAdminService = azureSearchAdminService; + this.featureManager = featureManager; + this.logger = logger; + } + + /// + /// Displays the Azure Search Admin dashboard. + /// + /// The view. + [HttpGet] + public async Task Index() + { + if (!await this.featureManager.IsEnabledAsync(FeatureFlags.AzureSearch)) + { + return this.NotFound(); + } + + var viewModel = new AzureSearchAdminViewModel + { + Indexers = await this.azureSearchAdminService.GetIndexersStatusAsync(), + Indexes = await this.azureSearchAdminService.GetIndexesStatusAsync(), + }; + + return this.View(viewModel); + } + + /// + /// Triggers an indexer run. + /// + /// The name of the indexer to run. + /// Redirects to Index with status message. + [HttpPost] + [ValidateAntiForgeryToken] + public async Task RunIndexer(string indexerName) + { + if (!await this.featureManager.IsEnabledAsync(FeatureFlags.AzureSearch)) + { + return this.NotFound(); + } + + if (string.IsNullOrEmpty(indexerName)) + { + return this.BadRequest("Indexer name is required."); + } + + var success = await this.azureSearchAdminService.RunIndexerAsync(indexerName); + + this.TempData["Message"] = success + ? $"Indexer '{indexerName}' has been triggered successfully." + : $"Failed to trigger indexer '{indexerName}'."; + this.TempData["IsError"] = !success; + + return this.RedirectToAction("Index"); + } + + /// + /// Resets an indexer. + /// + /// The name of the indexer to reset. + /// Redirects to Index with status message. + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ResetIndexer(string indexerName) + { + if (!await this.featureManager.IsEnabledAsync(FeatureFlags.AzureSearch)) + { + return this.NotFound(); + } + + if (string.IsNullOrEmpty(indexerName)) + { + return this.BadRequest("Indexer name is required."); + } + + var success = await this.azureSearchAdminService.ResetIndexerAsync(indexerName); + + this.TempData["Message"] = success + ? $"Indexer '{indexerName}' has been reset successfully. You may now run it to perform a full re-index." + : $"Failed to reset indexer '{indexerName}'."; + this.TempData["IsError"] = !success; + + return this.RedirectToAction("Index"); + } + } +} \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Interfaces/IAzureSearchAdminService.cs b/AdminUI/LearningHub.Nhs.AdminUI/Interfaces/IAzureSearchAdminService.cs new file mode 100644 index 000000000..f8f17d9c9 --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/Interfaces/IAzureSearchAdminService.cs @@ -0,0 +1,38 @@ +namespace LearningHub.Nhs.AdminUI.Interfaces +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using LearningHub.Nhs.AdminUI.Models; + + /// + /// Interface for Azure Search administration operations. + /// + public interface IAzureSearchAdminService + { + /// + /// Gets the status of all indexers. + /// + /// A list of indexer statuses. + Task> GetIndexersStatusAsync(); + + /// + /// Gets the status of all indexes. + /// + /// A list of index statuses. + Task> GetIndexesStatusAsync(); + + /// + /// Runs an indexer manually. + /// + /// The name of the indexer to run. + /// True if successful, false otherwise. + Task RunIndexerAsync(string indexerName); + + /// + /// Resets an indexer (clears state and allows full reindex). + /// + /// The name of the indexer to reset. + /// True if successful, false otherwise. + Task ResetIndexerAsync(string indexerName); + } +} \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj index f33ea1faf..b3a8d9b6b 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/LearningHub.Nhs.AdminUI.csproj.user b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj.user index 953342020..75a932fca 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj.user +++ b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj.user @@ -5,5 +5,9 @@ Local IIS + MvcControllerEmptyScaffolder + root/Common/MVC/Controller + RazorViewEmptyScaffolder + root/Common/MVC/View \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Models/AzureSearchAdminViewModel.cs b/AdminUI/LearningHub.Nhs.AdminUI/Models/AzureSearchAdminViewModel.cs new file mode 100644 index 000000000..767a769d1 --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/Models/AzureSearchAdminViewModel.cs @@ -0,0 +1,30 @@ +namespace LearningHub.Nhs.AdminUI.Models +{ + using System.Collections.Generic; + + /// + /// View model for Azure Search Admin page. + /// + public class AzureSearchAdminViewModel + { + /// + /// Gets or sets the list of indexer statuses. + /// + public List Indexers { get; set; } = new List(); + + /// + /// Gets or sets the list of index statuses. + /// + public List Indexes { get; set; } = new List(); + + /// + /// Gets or sets a message to display to the user. + /// + public string Message { get; set; } + + /// + /// Gets or sets a value indicating whether the message is an error. + /// + public bool IsError { get; set; } + } +} \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Models/IndexStatusViewModel.cs b/AdminUI/LearningHub.Nhs.AdminUI/Models/IndexStatusViewModel.cs new file mode 100644 index 000000000..41e27953d --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/Models/IndexStatusViewModel.cs @@ -0,0 +1,23 @@ +namespace LearningHub.Nhs.AdminUI.Models +{ + /// + /// View model for Azure Search index status. + /// + public class IndexStatusViewModel + { + /// + /// Gets or sets the name of the index. + /// + public string Name { get; set; } + + /// + /// Gets or sets the document count in the index. + /// + public long? DocumentCount { get; set; } + + /// + /// Gets or sets the storage size in bytes. + /// + public long? StorageSize { get; set; } + } +} \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Models/IndexerStatusViewModel.cs b/AdminUI/LearningHub.Nhs.AdminUI/Models/IndexerStatusViewModel.cs new file mode 100644 index 000000000..8cc027eb9 --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/Models/IndexerStatusViewModel.cs @@ -0,0 +1,45 @@ +namespace LearningHub.Nhs.AdminUI.Models +{ + using System; + + /// + /// View model for Azure Search indexer status. + /// + public class IndexerStatusViewModel + { + /// + /// Gets or sets the name of the indexer. + /// + public string Name { get; set; } + + /// + /// Gets or sets the current status of the indexer. + /// + public string Status { get; set; } + + /// + /// Gets or sets the last run time of the indexer. + /// + public DateTimeOffset? LastRunTime { get; set; } + + /// + /// Gets or sets the last run status (success/failed/inProgress). + /// + public string LastRunStatus { get; set; } + + /// + /// Gets or sets the error message if the last run failed. + /// + public string LastRunErrorMessage { get; set; } + + /// + /// Gets or sets the number of documents indexed in the last run. + /// + public int? ItemsProcessed { get; set; } + + /// + /// Gets or sets the number of documents that failed in the last run. + /// + public int? ItemsFailed { get; set; } + } +} \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/ServiceCollectionExtension.cs b/AdminUI/LearningHub.Nhs.AdminUI/ServiceCollectionExtension.cs index 4f95fe9fb..9137a55e7 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/ServiceCollectionExtension.cs +++ b/AdminUI/LearningHub.Nhs.AdminUI/ServiceCollectionExtension.cs @@ -108,6 +108,11 @@ public static void ConfigureServices(this IServiceCollection services, IConfigur services.AddScoped(); services.AddScoped(); + // Configure Azure Search + services.Configure(configuration.GetSection("AzureSearch")); + services.AddHttpClient("AzureSearch"); + services.AddScoped(); + // web settings binding var webSettings = new WebSettings(); configuration.Bind("WebSettings", webSettings); @@ -160,12 +165,12 @@ public static void ConfigureServices(this IServiceCollection services, IConfigur }).AddCookie( "Cookies", options => - { - options.AccessDeniedPath = "/Authorisation/AccessDenied"; - options.ExpireTimeSpan = TimeSpan.FromMinutes(webSettings.AuthTimeout); - options.SlidingExpiration = true; - options.EventsType = typeof(CookieEventHandler); - }).AddOpenIdConnect( + { + options.AccessDeniedPath = "/Authorisation/AccessDenied"; + options.ExpireTimeSpan = TimeSpan.FromMinutes(webSettings.AuthTimeout); + options.SlidingExpiration = true; + options.EventsType = typeof(CookieEventHandler); + }).AddOpenIdConnect( "oidc", options => { @@ -185,12 +190,12 @@ public static void ConfigureServices(this IServiceCollection services, IConfigur options.GetClaimsFromUserInfoEndpoint = true; options.Events.OnRemoteFailure = async context => - { - context.Response.Redirect("/"); // If login cancelled return to home page - context.HandleResponse(); + { + context.Response.Redirect("/"); // If login cancelled return to home page + context.HandleResponse(); - await Task.CompletedTask; - }; + await Task.CompletedTask; + }; options.ClaimActions.MapUniqueJsonKey("role", "role"); options.ClaimActions.MapUniqueJsonKey("name", "elfh_userName"); @@ -223,11 +228,11 @@ public static void ConfigureServices(this IServiceCollection services, IConfigur services.Configure( options => - { - options.ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor; - options.KnownNetworks.Clear(); - options.KnownProxies.Clear(); - }); + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + }); services.AddControllersWithViews(options => { diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Services/AzureSearchAdminService.cs b/AdminUI/LearningHub.Nhs.AdminUI/Services/AzureSearchAdminService.cs new file mode 100644 index 000000000..3d4406d7e --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/Services/AzureSearchAdminService.cs @@ -0,0 +1,299 @@ +namespace LearningHub.Nhs.AdminUI.Services +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Text.Json; + using System.Threading.Tasks; + using LearningHub.Nhs.AdminUI.Configuration; + using LearningHub.Nhs.AdminUI.Interfaces; + using LearningHub.Nhs.AdminUI.Models; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + + /// + /// Service for Azure Search administration operations. + /// + public class AzureSearchAdminService : IAzureSearchAdminService + { + /// + /// The Azure Search REST API version. + /// + private const string ApiVersion = "2024-07-01"; + + private readonly AzureSearchConfig config; + private readonly ILogger logger; + private readonly HttpClient httpClient; + + /// + /// Initializes a new instance of the class. + /// + /// The Azure Search configuration. + /// The logger. + /// The HTTP client factory. + public AzureSearchAdminService( + IOptions config, + ILogger logger, + IHttpClientFactory httpClientFactory) + { + this.config = config.Value; + this.logger = logger; + this.httpClient = httpClientFactory.CreateClient("AzureSearch"); + this.ConfigureHttpClient(); + } + + /// + public async Task> GetIndexersStatusAsync() + { + var result = new List(); + + try + { + if (string.IsNullOrEmpty(this.config.ServiceEndpoint) || string.IsNullOrEmpty(this.config.AdminApiKey)) + { + this.logger.LogWarning("Azure Search configuration is not set"); + return result; + } + + // Get list of indexers + var indexersResponse = await this.httpClient.GetAsync($"indexers?api-version={ApiVersion}"); + if (!indexersResponse.IsSuccessStatusCode) + { + this.logger.LogError("Failed to get indexers list: {StatusCode}", indexersResponse.StatusCode); + return result; + } + + var indexersJson = await indexersResponse.Content.ReadAsStringAsync(); + using var indexersDoc = JsonDocument.Parse(indexersJson); + + if (indexersDoc.RootElement.TryGetProperty("value", out var indexersArray)) + { + foreach (var indexer in indexersArray.EnumerateArray()) + { + var indexerName = indexer.GetProperty("name").GetString(); + var encodedIndexerName = Uri.EscapeDataString(indexerName); + + // Get status for each indexer + var statusResponse = await this.httpClient.GetAsync($"indexers/{encodedIndexerName}/status?api-version={ApiVersion}"); + if (statusResponse.IsSuccessStatusCode) + { + var statusJson = await statusResponse.Content.ReadAsStringAsync(); + var statusViewModel = this.ParseIndexerStatus(indexerName, statusJson); + result.Add(statusViewModel); + } + else + { + result.Add(new IndexerStatusViewModel + { + Name = indexerName, + Status = "Unknown", + LastRunStatus = "Error retrieving status", + }); + } + } + } + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error getting indexers status"); + } + + return result; + } + + /// + public async Task> GetIndexesStatusAsync() + { + var result = new List(); + + try + { + if (string.IsNullOrEmpty(this.config.ServiceEndpoint) || string.IsNullOrEmpty(this.config.AdminApiKey)) + { + this.logger.LogWarning("Azure Search configuration is not set"); + return result; + } + + // Get list of indexes + var indexesResponse = await this.httpClient.GetAsync($"indexes?api-version={ApiVersion}"); + if (!indexesResponse.IsSuccessStatusCode) + { + this.logger.LogError("Failed to get indexes list: {StatusCode}", indexesResponse.StatusCode); + return result; + } + + var indexesJson = await indexesResponse.Content.ReadAsStringAsync(); + using var indexesDoc = JsonDocument.Parse(indexesJson); + + if (indexesDoc.RootElement.TryGetProperty("value", out var indexesArray)) + { + foreach (var index in indexesArray.EnumerateArray()) + { + var indexName = index.GetProperty("name").GetString(); + var encodedIndexName = Uri.EscapeDataString(indexName); + + // Get statistics for each index + var statsResponse = await this.httpClient.GetAsync($"indexes/{encodedIndexName}/stats?api-version={ApiVersion}"); + if (statsResponse.IsSuccessStatusCode) + { + var statsJson = await statsResponse.Content.ReadAsStringAsync(); + using var statsDoc = JsonDocument.Parse(statsJson); + + result.Add(new IndexStatusViewModel + { + Name = indexName, + DocumentCount = statsDoc.RootElement.TryGetProperty("documentCount", out var docCount) ? docCount.GetInt64() : null, + StorageSize = statsDoc.RootElement.TryGetProperty("storageSize", out var storageSize) ? storageSize.GetInt64() : null, + }); + } + else + { + result.Add(new IndexStatusViewModel + { + Name = indexName, + DocumentCount = null, + StorageSize = null, + }); + } + } + } + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error getting indexes status"); + } + + return result; + } + + /// + public async Task RunIndexerAsync(string indexerName) + { + try + { + if (string.IsNullOrEmpty(this.config.ServiceEndpoint) || string.IsNullOrEmpty(this.config.AdminApiKey)) + { + this.logger.LogWarning("Azure Search configuration is not set"); + return false; + } + + var encodedIndexerName = Uri.EscapeDataString(indexerName); + var response = await this.httpClient.PostAsync($"indexers/{encodedIndexerName}/run?api-version={ApiVersion}", null); + + if (response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.Accepted) + { + this.logger.LogInformation("Successfully triggered indexer: {IndexerName}", indexerName); + return true; + } + + this.logger.LogError("Failed to run indexer {IndexerName}: {StatusCode}", indexerName, response.StatusCode); + return false; + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error running indexer {IndexerName}", indexerName); + return false; + } + } + + /// + public async Task ResetIndexerAsync(string indexerName) + { + try + { + if (string.IsNullOrEmpty(this.config.ServiceEndpoint) || string.IsNullOrEmpty(this.config.AdminApiKey)) + { + this.logger.LogWarning("Azure Search configuration is not set"); + return false; + } + + var encodedIndexerName = Uri.EscapeDataString(indexerName); + var response = await this.httpClient.PostAsync($"indexers/{encodedIndexerName}/reset?api-version={ApiVersion}", null); + + if (response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.NoContent) + { + this.logger.LogInformation("Successfully reset indexer: {IndexerName}", indexerName); + return true; + } + + this.logger.LogError("Failed to reset indexer {IndexerName}: {StatusCode}", indexerName, response.StatusCode); + return false; + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error resetting indexer {IndexerName}", indexerName); + return false; + } + } + + private void ConfigureHttpClient() + { + if (!string.IsNullOrEmpty(this.config.ServiceEndpoint)) + { + this.httpClient.BaseAddress = new Uri(this.config.ServiceEndpoint.TrimEnd('/') + "/"); + } + + if (!string.IsNullOrEmpty(this.config.AdminApiKey)) + { + this.httpClient.DefaultRequestHeaders.Add("api-key", this.config.AdminApiKey); + } + + this.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + + private IndexerStatusViewModel ParseIndexerStatus(string indexerName, string statusJson) + { + var viewModel = new IndexerStatusViewModel + { + Name = indexerName, + }; + + try + { + using var doc = JsonDocument.Parse(statusJson); + var root = doc.RootElement; + + if (root.TryGetProperty("status", out var status)) + { + viewModel.Status = status.GetString(); + } + + if (root.TryGetProperty("lastResult", out var lastResult)) + { + if (lastResult.TryGetProperty("status", out var lastStatus)) + { + viewModel.LastRunStatus = lastStatus.GetString(); + } + + if (lastResult.TryGetProperty("endTime", out var endTime)) + { + viewModel.LastRunTime = endTime.GetDateTimeOffset(); + } + + if (lastResult.TryGetProperty("errorMessage", out var errorMessage)) + { + viewModel.LastRunErrorMessage = errorMessage.GetString(); + } + + if (lastResult.TryGetProperty("itemsProcessed", out var itemsProcessed)) + { + viewModel.ItemsProcessed = itemsProcessed.GetInt32(); + } + + if (lastResult.TryGetProperty("itemsFailed", out var itemsFailed)) + { + viewModel.ItemsFailed = itemsFailed.GetInt32(); + } + } + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error parsing indexer status for {IndexerName}", indexerName); + viewModel.Status = "Error parsing status"; + } + + return viewModel; + } + } +} \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Views/AzureSearchAdmin/Index.cshtml b/AdminUI/LearningHub.Nhs.AdminUI/Views/AzureSearchAdmin/Index.cshtml new file mode 100644 index 000000000..98065c271 --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/Views/AzureSearchAdmin/Index.cshtml @@ -0,0 +1,222 @@ +@model LearningHub.Nhs.AdminUI.Models.AzureSearchAdminViewModel +@{ + ViewData["Title"] = "Azure AI Search Management"; + var message = TempData["Message"] as string; + var isError = TempData["IsError"] as bool? ?? false; +} + +@section SideMenu { + @{ + await Html.RenderPartialAsync("_NavSection"); + } +} + +
    +
    + Azure AI Search Management +
    + +
    + @if (!string.IsNullOrEmpty(message)) + { +
    +

    @message

    +
    + } + + +
    +

    Search Indexes

    + @if (Model.Indexes.Any()) + { +
    + + + + + + + + + + @foreach (var index in Model.Indexes) + { + + + + + + } + +
    Index NameDocument CountStorage Size
    @index.Name@(index.DocumentCount?.ToString("N0") ?? "N/A")@(index.StorageSize.HasValue? FormatBytes(index.StorageSize.Value) : "N/A")
    +
    + } + else + { +

    No indexes found or Azure Search is not configured.

    + } +
    + + +
    +

    Indexers

    + @if (Model.Indexers.Any()) + { +
    + + + + + + + + + + + + + + @foreach (var indexer in Model.Indexers) + { + + + + + + + + + + } + +
    Indexer NameStatusLast RunLast Run StatusItems ProcessedItems FailedActions
    @indexer.Name + @indexer.Status + @(indexer.LastRunTime?.ToString("dd/MM/yyyy HH:mm:ss") ?? "N/A") + @(indexer.LastRunStatus ?? "N/A") + @if (!string.IsNullOrEmpty(indexer.LastRunErrorMessage)) + { + + } + @(indexer.ItemsProcessed?.ToString("N0") ?? "N/A") + @if (indexer.ItemsFailed > 0) + { + @indexer.ItemsFailed?.ToString("N0") + } + else + { + @(indexer.ItemsFailed?.ToString("N0") ?? "N/A") + } + +
    + @Html.AntiForgeryToken() + + +
    + +
    +
    + } + else + { +

    No indexers found or Azure Search is not configured.

    + } +
    + + +
    +
    + + + + +@functions { + string FormatBytes(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB", "TB" }; + double len = bytes; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len /= 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + + string GetStatusBadgeClass(string status) + { + return status?.ToLower() switch + { + "running" => "badge-primary", + "error" => "badge-danger", + _ => "badge-secondary" + }; + } + + string GetLastRunStatusBadgeClass(string status) + { + return status?.ToLower() switch + { + "success" => "badge-success", + "transientfailure" or "transientFailure" => "badge-warning", + "persistentfailure" or "persistentFailure" => "badge-danger", + "reset" => "badge-info", + "inprogress" or "inProgress" => "badge-primary", + _ => "badge-secondary" + }; + } +} + +@section Scripts { + +} \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_NavPartial.cshtml b/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_NavPartial.cshtml index 169e30fd4..6a6e66a26 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_NavPartial.cshtml +++ b/AdminUI/LearningHub.Nhs.AdminUI/Views/Shared/_NavPartial.cshtml @@ -33,12 +33,17 @@ case "resourcesync": mainMenu = "ResourceSync"; break; + case "azuresearchadmin": + mainMenu = "AzureSearch"; + break; } string IsActive(string itemCheck) { return mainMenu == itemCheck ? "active" : "inactive"; } + + var azureSearchEnabled = await featureManager.IsEnabledAsync(FeatureFlags.AzureSearch); }
    @@ -83,12 +88,18 @@ - @if (!await featureManager.IsEnabledAsync(FeatureFlags.AzureSearch)) + @if (!azureSearchEnabled) { } + else + { + + } }
    From 04b9bb8470c739bfa8675085b2b01d1f11fd221e Mon Sep 17 00:00:00 2001 From: AnjuJose011 <154979799+AnjuJose011@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:26:59 +0000 Subject: [PATCH 024/106] policy update --- .../Views/Policies/AcceptableUsePolicy.cshtml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml index 0e1ed212f..57e6227d8 100644 --- a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml @@ -128,9 +128,9 @@

    11.1 Course Manager means a person authorised by NHS England who is responsible for creating and managing courses and learning resources on the Learning Hub, including the moderation of learner's Contributions within social learning environments of an online course, such as discussion forums.

    11.2 Course Managers shall be responsible for monitoring local forum activity and ensuring compliance of all Contributions with the Learning Hub’s Acceptable Use Policy. Inappropriate Contributions shall be addressed promptly and escalated where necessary to the Learning Hub, who will review the user’s access to the Platform and the relevant forum.

    11.3 Course Managers shall also take responsibility for the following:

    -

    11.3.1

    Clear Expectations:

    Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.

    -

    11.3.2

    Inclusive Practice:

    Encourage participation from all learners/users and foster a respectful, inclusive environment that values diverse perspectives. This should involve reminding local forum users of the appropriate measures of this Acceptable Use Policy and potentially removing Contributions that infringe the rules of this Acceptable Use Policy including under paragraph 4.5 above.

    -

    11.3.3

    Data Protection and Safety:

    Ensure that no sensitive or personal data is shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.

    +

    11.3.1 Clear Expectations:Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.

    +

    11.3.2 Inclusive Practice: Encourage participation from all learners/users and foster a respectful, inclusive environment that values diverse perspectives. This should involve reminding local forum users of the appropriate measures of this Acceptable Use Policy and potentially removing Contributions that infringe the rules of this Acceptable Use Policy including under paragraph 4.5 above.

    +

    11.3.3 Data Protection and Safety:Ensure that no sensitive or personal data is shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.

    11.4 All users must comply with this Acceptable Use Policy. Breaches may result in removal of Contributions and/or withdrawal or suspension of user access without notice.

    From d832da0b2e566c9ec79cf16c71c1c5808a396cbf Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 28 Nov 2025 16:06:25 +0000 Subject: [PATCH 025/106] TD-6628 -Update autosuggest panel to improve result limits and update type labels --- .../LearningHub.Nhs.AdminUI.csproj | 2 +- ...rningHub.Nhs.WebUI.AutomatedUiTests.csproj | 2 +- .../Controllers/SearchController.cs | 9 ++- .../LearningHub.Nhs.WebUI.csproj | 2 +- .../Views/Search/_AutoSuggest.cshtml | 70 +++++++++++++++++++ .../LearningHub.Nhs.OpenApi.Models.csproj | 2 +- .../AzureSearch/SearchDocument.cs | 6 ++ ....Nhs.OpenApi.Repositories.Interface.csproj | 2 +- ...earningHub.Nhs.OpenApi.Repositories.csproj | 2 +- ...gHub.Nhs.OpenApi.Services.Interface.csproj | 2 +- .../LearningHub.Nhs.OpenApi.Services.csproj | 2 +- .../AzureSearch/AzureSearchService.cs | 43 +++++++----- .../LearningHub.Nhs.OpenApi.Tests.csproj | 2 +- .../LearningHub.NHS.OpenAPI.csproj | 2 +- ...ub.Nhs.ReportApi.Services.Interface.csproj | 2 +- ...ub.Nhs.ReportApi.Services.UnitTests.csproj | 2 +- .../LearningHub.Nhs.ReportApi.Services.csproj | 2 +- .../LearningHub.Nhs.ReportApi.Shared.csproj | 2 +- .../LearningHub.Nhs.ReportApi.csproj | 2 +- .../LearningHub.Nhs.Api.csproj | 2 +- .../LearningHub.Nhs.Api.Shared.csproj | 2 +- .../LearningHub.Nhs.Api.UnitTests.csproj | 2 +- ...earningHub.Nhs.Repository.Interface.csproj | 2 +- .../LearningHub.Nhs.Repository.csproj | 2 +- .../LearningHub.Nhs.Services.Interface.csproj | 2 +- .../LearningHub.Nhs.Services.UnitTests.csproj | 2 +- .../LearningHub.Nhs.Services.csproj | 2 +- ...earningHub.Nhs.Migration.ConsoleApp.csproj | 2 +- ...LearningHub.Nhs.Migration.Interface.csproj | 2 +- .../LearningHub.Nhs.Migration.Models.csproj | 2 +- ...ub.Nhs.Migration.Staging.Repository.csproj | 2 +- ...LearningHub.Nhs.Migration.UnitTests.csproj | 2 +- .../LearningHub.Nhs.Migration.csproj | 2 +- 33 files changed, 138 insertions(+), 48 deletions(-) create mode 100644 LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml diff --git a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj index b3a8d9b6b..700c1a419 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/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj index d846dfde5..1378c0fa5 100644 --- a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj +++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs index ee1cae636..30c89a1e9 100644 --- a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs @@ -338,7 +338,14 @@ public async Task GetAutoSuggestion(string term) var autoSuggestions = await this.searchService.GetAutoSuggestionList(term); - return this.PartialView("_AutoComplete", autoSuggestions); + var azureSearchEnabled = Task.Run(() => this.featureManager.IsEnabledAsync(FeatureFlags.AzureSearch)).Result; + + if (!azureSearchEnabled) + { + return this.PartialView("_AutoComplete", autoSuggestions); + } + + return this.PartialView("_AutoSuggest", autoSuggestions); } /// diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index 09cb454c3..3a13b614f 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -113,7 +113,7 @@ - + diff --git a/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml new file mode 100644 index 000000000..ccb1ff5fd --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml @@ -0,0 +1,70 @@ +@model LearningHub.Nhs.Models.Search.AutoSuggestionModel; +@inject LearningHub.Nhs.WebUI.Interfaces.IMoodleApiService moodleApiService; + +@using LearningHub.Nhs.Models.Search.SearchClick; +@using Microsoft.AspNetCore.WebUtilities +@using System.Web +@{ + var counter = 0; + var counter_res = 0; + string GetUrl(string term, string searchType, AutoSuggestionClickPayloadModel payload, string reference, int? resourceReferenceId) + { + + var url = string.Empty; + if (!string.IsNullOrEmpty(reference) && searchType == "catalogue") + { + url = "/Catalogue/" + reference; + } + else if ((resourceReferenceId > 0) && searchType == "resource") + { + url = "/Resource/" + resourceReferenceId; + } + else if ((resourceReferenceId > 0) && searchType == "course") + { + if (resourceReferenceId != null) + return moodleApiService.GetCourseUrl(resourceReferenceId.Value); + } + else if (!string.IsNullOrEmpty(term) && searchType == "Concepts") + { + url = "/Search/results?term=" + term; + } + else + { + url = "/Search/results?term=" + term; + } + + return $@"/search/record-autosuggestion-click?term={term}&url={url}&clickTargetUrl={payload.ClickTargetUrl}&itemIndex={payload?.HitNumber}&&totalNumberOfHits={payload?.SearchSignal?.Stats?.TotalHits} +&containerId={payload?.ContainerId}&name={payload?.DocumentFields.Name}&query={payload?.SearchSignal?.Query}&userQuery={HttpUtility.UrlEncode(payload?.SearchSignal?.UserQuery)} +&searchId={payload?.SearchSignal?.SearchId}&timeOfSearch={payload?.SearchSignal?.TimeOfSearch}&title={payload?.DocumentFields?.Title}"; + } +} +@if (Model != null) +{ + @foreach (var item in Model.ConceptDocument.ConceptDocumentList) + { + counter++; +
  • + + + + + + +
  • + } + @foreach (var item in Model.ResourceCollectionDocument.DocumentList) + { + counter_res++; +
  • + + +

    + Type: + @(item.ResourceType == "resource" ? "Learning resource" : item.ResourceType) + +

    + +
    +
  • + } +} \ No newline at end of file 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 1735b0b62..b742b0cdc 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs index 30b4bbe7c..8f1c69261 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs @@ -117,6 +117,12 @@ public string Description [JsonPropertyName("author")] public string Author { get; set; } = string.Empty; + /// + /// Gets or sets the url. + /// + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; + /// /// Strips paragraph tags from input string. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj index eb117ebe3..d8e3b8642 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/LearningHub.Nhs.OpenApi.Repositories.Interface.csproj @@ -17,7 +17,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj index c6d370145..c3cd84190 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/LearningHub.Nhs.OpenApi.Repositories.csproj @@ -24,7 +24,7 @@ - + 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 ceef6ce67..2c4e458f0 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 @@ -17,7 +17,7 @@ - + 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 d485036ba..42331600e 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj @@ -31,7 +31,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 42b89b96c..0309915fd 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -593,11 +593,13 @@ public async Task GetAutoSuggestionResultsAsync(string term suggestOptions.Select.Add("manual_tag"); suggestOptions.Select.Add("resource_type"); suggestOptions.Select.Add("resource_collection"); + suggestOptions.Select.Add("url"); + suggestOptions.Select.Add("resource_reference_id"); var autoOptions = new AutocompleteOptions { Mode = AutocompleteMode.OneTermWithContext, - Size = 50 + Size = 5 }; var searchText = LuceneQueryBuilder.EscapeLuceneSpecialCharacters(term); @@ -618,15 +620,19 @@ public async Task GetAutoSuggestionResultsAsync(string term { Id = r.Document.Id, Text = r.Document.Title.Trim(), + URL = r.Document.Url, + ResourceReferenceId = (r.Document.ResourceCollection == "resource") ? r.Document.ResourceReferenceId : r.Document.Id, Type = r.Document.ResourceCollection ?? "Suggestion" }); - var autoResults = autoResponse.Value.Results + var autoResults = autoResponse.Value.Results .Where(r => !string.IsNullOrWhiteSpace(r.Text)) .Select((r, index) => new { Id = "A" + (index + 1), Text = r.Text.Trim(), + URL = string.Empty, + ResourceReferenceId = (string?)null, Type = "AutoComplete" }); @@ -641,18 +647,19 @@ public async Task GetAutoSuggestionResultsAsync(string term TotalHits = combined.Count }; - var autoSuggestionResource = new AutoSuggestionResource + var autoSuggestion = new AutoSuggestionResourceCollection { TotalHits = suggestResults.Count(), - ResourceDocumentList = suggestResults - .Where(a => a.Type == "resource") - .Select(item => new AutoSuggestionResourceDocument - { - Id = item.Id?.Substring(1), - Title = item.Text, - Click = BuildAutoSuggestClickModel(item.Id?.Substring(1), item.Text, 0, 0 , term, suggestResults.Count()) - }) - .Take(3) + DocumentList = suggestResults.Select(item => new AutoSuggestionResourceCollectionDocument + { + Id = item.Id, + ResourceType = item.Type, + URL = item.URL, + ResourceReferenceId = item.ResourceReferenceId != null && int.TryParse(item.ResourceReferenceId, out var refId) ? refId : 0, + Title = item.Text, + Click = BuildAutoSuggestClickModel(item.Id, item.Text, 0, 0, term, suggestResults.Count()) + }) + .Take(5) .ToList() }; @@ -663,11 +670,11 @@ public async Task GetAutoSuggestionResultsAsync(string term .Where(a => a.Type == "catalogue") .Select(item => new AutoSuggestionCatalogueDocument { - Id = item.Id?.Substring(1), + Id = item.Id, Name = item.Text, - Click = BuildAutoSuggestClickModel(item.Id?.Substring(1), item.Text, 0, 0, term, suggestResults.Count()) + Click = BuildAutoSuggestClickModel(item.Id, item.Text, 0, 0, term, suggestResults.Count()) }) - .Take(3) + .Take(0) .ToList() }; @@ -677,15 +684,15 @@ public async Task GetAutoSuggestionResultsAsync(string term ConceptDocumentList = autoResults .Select(item => new AutoSuggestionConceptDocument { - Id = item.Id?.Substring(1), + Id = item.Id, Concept = item.Text, Title = item.Text, - Click = BuildAutoSuggestClickModel(item.Id?.Substring(1), item.Text, 0, 0, term, autoResults.Count()) + Click = BuildAutoSuggestClickModel(item.Id, item.Text, 0, 0, term, autoResults.Count()) }) .ToList() }; - viewmodel.ResourceDocument = autoSuggestionResource; + viewmodel.ResourceCollectionDocument = autoSuggestion; viewmodel.CatalogueDocument = autoSuggestionCatalogue; viewmodel.ConceptDocument = autoSuggestionConcept; diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj index ac9eaae79..0163d76cb 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/LearningHub.Nhs.OpenApi.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj index 34579b31b..c26581814 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj +++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj @@ -19,7 +19,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj index 4f878aebb..32e2901a7 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Services.Interface/LearningHub.Nhs.ReportApi.Services.Interface.csproj @@ -16,7 +16,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj index db1666c0c..3619018f8 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Services.UnitTests/LearningHub.Nhs.ReportApi.Services.UnitTests.csproj @@ -18,7 +18,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj index 244ed6476..ac891c7e7 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Services/LearningHub.Nhs.ReportApi.Services.csproj @@ -19,7 +19,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj b/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj index d78156620..439f32b3d 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi.Shared/LearningHub.Nhs.ReportApi.Shared.csproj @@ -17,7 +17,7 @@ - + diff --git a/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj b/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj index e4906102c..5f9ab38ed 100644 --- a/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj +++ b/ReportAPI/LearningHub.Nhs.ReportApi/LearningHub.Nhs.ReportApi.csproj @@ -20,7 +20,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj b/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj index ecb7b4ea9..b82362ae0 100644 --- a/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj +++ b/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj @@ -29,7 +29,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 cff61a43a..3ab2a7c6c 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 790f0509b..c4b2a3c77 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.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj b/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj index ed8e66e7d..d41e04f1c 100644 --- a/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj +++ b/WebAPI/LearningHub.Nhs.Repository.Interface/LearningHub.Nhs.Repository.Interface.csproj @@ -10,7 +10,7 @@ - + diff --git a/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj b/WebAPI/LearningHub.Nhs.Repository/LearningHub.Nhs.Repository.csproj index 88c66fbd2..e523573ae 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.Services.Interface/LearningHub.Nhs.Services.Interface.csproj b/WebAPI/LearningHub.Nhs.Services.Interface/LearningHub.Nhs.Services.Interface.csproj index c2e2480b3..0ac26f322 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 f6f5bb385..c4d258f06 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 1ff2594cd..c7318ac07 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/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj b/WebAPI/MigrationTool/LearningHub.Nhs.Migration.ConsoleApp/LearningHub.Nhs.Migration.ConsoleApp.csproj index a179c4588..9a26446c3 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 @@ -25,7 +25,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 af8938c8a..a779c23cb 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 abf359917..34811d53e 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 223050743..cf8ea6ba5 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 14a647cf8..b6db51ab9 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 476f5e145..9aad67192 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 From 9c300a733402db40067399ef12f8ffd9b3976cc5 Mon Sep 17 00:00:00 2001 From: AnjuJose011 <154979799+AnjuJose011@users.noreply.github.com> Date: Fri, 28 Nov 2025 19:07:03 +0000 Subject: [PATCH 026/106] Policy update to Bold --- .../Views/Policies/AcceptableUsePolicy.cshtml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml index 57e6227d8..af85c380b 100644 --- a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml @@ -128,9 +128,9 @@

    11.1 Course Manager means a person authorised by NHS England who is responsible for creating and managing courses and learning resources on the Learning Hub, including the moderation of learner's Contributions within social learning environments of an online course, such as discussion forums.

    11.2 Course Managers shall be responsible for monitoring local forum activity and ensuring compliance of all Contributions with the Learning Hub’s Acceptable Use Policy. Inappropriate Contributions shall be addressed promptly and escalated where necessary to the Learning Hub, who will review the user’s access to the Platform and the relevant forum.

    11.3 Course Managers shall also take responsibility for the following:

    -

    11.3.1 Clear Expectations:Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.

    -

    11.3.2 Inclusive Practice: Encourage participation from all learners/users and foster a respectful, inclusive environment that values diverse perspectives. This should involve reminding local forum users of the appropriate measures of this Acceptable Use Policy and potentially removing Contributions that infringe the rules of this Acceptable Use Policy including under paragraph 4.5 above.

    -

    11.3.3 Data Protection and Safety:Ensure that no sensitive or personal data is shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.

    +

    11.3.1Clear Expectations:Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.

    +

    11.3.2 Inclusive Practice: Encourage participation from all learners/users and foster a respectful, inclusive environment that values diverse perspectives. This should involve reminding local forum users of the appropriate measures of this Acceptable Use Policy and potentially removing Contributions that infringe the rules of this Acceptable Use Policy including under paragraph 4.5 above.

    +

    11.3.3 Data Protection and Safety:Ensure that no sensitive or personal data is shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.

    11.4 All users must comply with this Acceptable Use Policy. Breaches may result in removal of Contributions and/or withdrawal or suspension of user access without notice.

    From 8f75a86373c4cd9860d6cf708482f0b79b391224 Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Mon, 1 Dec 2025 12:51:37 +0000 Subject: [PATCH 027/106] TD-6384: CORS Policy Changes for Moodle Heart beat --- LearningHub.Nhs.WebUI/Program.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/LearningHub.Nhs.WebUI/Program.cs b/LearningHub.Nhs.WebUI/Program.cs index c24d9057c..8f4852c2c 100644 --- a/LearningHub.Nhs.WebUI/Program.cs +++ b/LearningHub.Nhs.WebUI/Program.cs @@ -35,6 +35,19 @@ builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); builder.Host.UseNLog(); + string corsMoodleUrl = builder.Configuration.GetValue("MoodleAPIConfig:BaseUrl"); + + builder.Services.AddCors(options => + { + options.AddPolicy("MoodleCORS", builder => + { + builder.WithOrigins(corsMoodleUrl) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); + }); + builder.Services.AddHostedService(); builder.Services.ConfigureServices(builder.Configuration, builder.Environment); @@ -78,6 +91,8 @@ app.UseHttpsRedirection(); } + app.UseCors("MoodleCORS"); + app.UseRouting(); app.UseAuthentication(); From b493ad8fad3d78806adfd852a9ee3f0e063e8a38 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Tue, 2 Dec 2025 11:30:26 +0000 Subject: [PATCH 028/106] . --- .../Configuration/Settings.cs | 2 +- .../Views/Reports/Index.cshtml | 3 + .../Views/Reports/_ReportHistoryPaging.cshtml | 76 +++++++++++++++++++ .../Configuration/DatabricksConfig.cs | 2 +- 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 LearningHub.Nhs.WebUI/Views/Reports/_ReportHistoryPaging.cshtml diff --git a/LearningHub.Nhs.WebUI/Configuration/Settings.cs b/LearningHub.Nhs.WebUI/Configuration/Settings.cs index 42778aeee..b6eb94279 100644 --- a/LearningHub.Nhs.WebUI/Configuration/Settings.cs +++ b/LearningHub.Nhs.WebUI/Configuration/Settings.cs @@ -269,6 +269,6 @@ public Settings() /// /// Gets or sets the StatMandId. /// - public int StatMandId { get; set; } = 12; + public int StatMandId { get; set; } } } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml index 9145e6bed..7e8d5e6b7 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml @@ -179,6 +179,9 @@
    +
    + @await Html.PartialAsync("_ReportHistoryPaging", Model) +
    diff --git a/LearningHub.Nhs.WebUI/Views/Reports/_ReportHistoryPaging.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/_ReportHistoryPaging.cshtml new file mode 100644 index 000000000..2e4d43245 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Reports/_ReportHistoryPaging.cshtml @@ -0,0 +1,76 @@ +@using System.Web; +@using LearningHub.Nhs.WebUI.Models.Learning +@using LearningHub.Nhs.WebUI.Models.Search; +@model LearningHub.Nhs.WebUI.Models.Report.ReportHistoryViewModel + + +@{ + var pagingModel = Model.ReportPaging; + var showPaging = pagingModel.CurrentPage >= 0 && pagingModel.CurrentPage <= pagingModel.TotalPages - 1; + var previousMessage = $"{pagingModel.CurrentPage} of {pagingModel.TotalPages}"; + int CurrentPageNumber = pagingModel.CurrentPage + 1; + var nextMessage = string.Empty; + if (CurrentPageNumber <= pagingModel.TotalPages) + { + nextMessage = $"{CurrentPageNumber + 1} of {pagingModel.TotalPages}"; + } + else + { + previousMessage = $"{CurrentPageNumber - 1} of {pagingModel.TotalPages}"; + nextMessage = $"{CurrentPageNumber} of {pagingModel.TotalPages}"; + } + + var routeData = new Dictionary(); + routeData["CurrentPageIndex"] = pagingModel.CurrentPage.ToString(); + var nextRouteData = new Dictionary(routeData); + var previousRouteData = new Dictionary(routeData); + nextRouteData["ReportFormActionType"] = ReportFormActionTypeEnum.NextPageChange.ToString(); + previousRouteData["ReportFormActionType"] = ReportFormActionTypeEnum.PreviousPageChange.ToString(); +} + +@if (pagingModel.TotalPages > 1) +{ + +} \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs index d33e0d710..3483a2d60 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/DatabricksConfig.cs @@ -38,7 +38,7 @@ public class DatabricksConfig /// /// Gets or sets the client scret of the service pricncipl. /// - public string clientSecret { get; set; } = null!; + public string ClientSecret { get; set; } = null!; /// /// Gets or sets the endpoint to check user permission. From 507f772ac91ab8f7674963f728602daaac78415c Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Tue, 2 Dec 2025 11:31:49 +0000 Subject: [PATCH 029/106] Moved UseCors after UseRouting --- LearningHub.Nhs.WebUI/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Program.cs b/LearningHub.Nhs.WebUI/Program.cs index 8f4852c2c..7fd582d1b 100644 --- a/LearningHub.Nhs.WebUI/Program.cs +++ b/LearningHub.Nhs.WebUI/Program.cs @@ -91,10 +91,10 @@ app.UseHttpsRedirection(); } - app.UseCors("MoodleCORS"); - app.UseRouting(); + app.UseCors("MoodleCORS"); + app.UseAuthentication(); app.UseAuthorization(); From aa8fda218aa56d97093d8fecb81ce9f044bb8eaa Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Tue, 2 Dec 2025 14:01:22 +0000 Subject: [PATCH 030/106] Modified code to ensure that trailing slash is removed from Moodle URL --- LearningHub.Nhs.WebUI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LearningHub.Nhs.WebUI/Program.cs b/LearningHub.Nhs.WebUI/Program.cs index 7fd582d1b..dda754ec6 100644 --- a/LearningHub.Nhs.WebUI/Program.cs +++ b/LearningHub.Nhs.WebUI/Program.cs @@ -41,7 +41,7 @@ { options.AddPolicy("MoodleCORS", builder => { - builder.WithOrigins(corsMoodleUrl) + builder.WithOrigins(corsMoodleUrl.TrimEnd('/')) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); From 5733df6889136d12906eeb1a2874c47164e0d141 Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Thu, 4 Dec 2025 11:07:16 +0000 Subject: [PATCH 031/106] Acceptence Use policy - SIT fixes --- .../Views/Policies/AcceptableUsePolicy.cshtml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml index af85c380b..b141b0129 100644 --- a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml @@ -68,7 +68,7 @@

    4.5.2.2 which requires a subscription or payment to gain access to such content;

    4.5.2.3 in which the user has a commercial interest;

    4.5.2.4 which promotes a business name and/or logo;

    -

    4.5.2.5 which contains a link to an app via iOS or Google Play; or

    +

    4.5.2.5 which contains a link to an app via iOS or Google Play or similar; or

    4.5.2.6 which has as its purpose or effect the collection and sharing of personal data;

    4.5.3 be irrelevant to the purpose or aims of the Platform or while addressing relevant subject matter, contain an irrelevant, unsuitable or inappropriate slant (for example relating to potentially controversial opinions or beliefs of any kind intended to influence others);

    4.5.4 be defamatory of any person;

    @@ -92,13 +92,12 @@

    4.5.22 contain harmful material;

    4.5.23 give the impression that the Contribution emanates from us, if this is not the case; or

    4.5.24 disclose any third party’s confidential information, identity, personally identifiable information or personal data (including data concerning health).

    -

    4.5.25 contain or request any material, the provision of which is not compliant NHS England Information Governance guidance[https://www.england.nhs.uk/ig/ig-resources/].

    +

    4.5.25 contain or request any material, the provision of which is not compliant with NHS England Information Governance guidance[https://www.england.nhs.uk/ig/ig-resources/].

    4.6 Posts must be made with consideration of the NHS People Promise[https://www.england.nhs.uk/our-nhs-people/online-version/lfaop/our-nhs-people-promise/].

    4.7 You acknowledge and accept that, when using the Platform and accessing the Content, some Content that has been uploaded by third parties may be factually inaccurate, or the topics addressed by the Content may be offensive,indecent, or objectionable in nature. We are not responsible (legally or otherwise) for any claim you may have in relation to the Content.

    4.8 When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources of evidence are valid (for example, by peer review).

    5 Metadata

    When making any Contribution, you must where prompted include a sufficient description of the Content so that other users can understand the description, source, and age of the Content. For example, if Content has been quality assured, then the relevant information should be posted in the appropriate field. All metadata fields on the Platform must be completed appropriately before initiating upload. Including the correct information is important in order to help other users locate the Content (otherwise the Content may not appear in search results for others to select).

    -

    When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources evidence are valid (for example, by peer review).

    6 Updates

    You must update each Contribution at least once every 3 (three) years, or update or remove it should it cease to be relevant or become outdated or revealed or generally perceived to be unsafe or otherwise unsuitable for inclusion on the Platform.

    7 Accessibility

    @@ -128,9 +127,9 @@

    11.1 Course Manager means a person authorised by NHS England who is responsible for creating and managing courses and learning resources on the Learning Hub, including the moderation of learner's Contributions within social learning environments of an online course, such as discussion forums.

    11.2 Course Managers shall be responsible for monitoring local forum activity and ensuring compliance of all Contributions with the Learning Hub’s Acceptable Use Policy. Inappropriate Contributions shall be addressed promptly and escalated where necessary to the Learning Hub, who will review the user’s access to the Platform and the relevant forum.

    11.3 Course Managers shall also take responsibility for the following:

    -

    11.3.1Clear Expectations:Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.

    +

    11.3.1 Clear Expectations: Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.

    11.3.2 Inclusive Practice: Encourage participation from all learners/users and foster a respectful, inclusive environment that values diverse perspectives. This should involve reminding local forum users of the appropriate measures of this Acceptable Use Policy and potentially removing Contributions that infringe the rules of this Acceptable Use Policy including under paragraph 4.5 above.

    -

    11.3.3 Data Protection and Safety:Ensure that no sensitive or personal data is shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.

    +

    11.3.3 Data Protection and Safety: Ensure that no sensitive or personal data is shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.

    11.4 All users must comply with this Acceptable Use Policy. Breaches may result in removal of Contributions and/or withdrawal or suspension of user access without notice.

    From 4a0b74a6f5fff793b46cc179e930ded728c6329f Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Thu, 4 Dec 2025 11:27:11 +0000 Subject: [PATCH 032/106] dabricks update --- LearningHub.Nhs.WebUI/appsettings.json | 3 ++- .../Services/DatabricksService.cs | 25 ++----------------- .../LearningHub.Nhs.OpenApi/appsettings.json | 6 ++++- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/LearningHub.Nhs.WebUI/appsettings.json b/LearningHub.Nhs.WebUI/appsettings.json index c1875cc2b..264ddf3f9 100644 --- a/LearningHub.Nhs.WebUI/appsettings.json +++ b/LearningHub.Nhs.WebUI/appsettings.json @@ -115,10 +115,11 @@ "MKPlayerLicence": "", "MediaKindStorageConnectionString": "" }, + "StatMandId": 0, "EnableTempDebugging": "false", "LimitScormToAdmin": "false" - }, + }, "LearningHubAuthServiceConfig": { "Authority": "", "ClientId": "", diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs index 88705817e..459f97d0a 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs @@ -1,40 +1,24 @@ -using LearningHub.Nhs.Models.Bookmark; -using LearningHub.Nhs.Models.Entities.Reporting; -using LearningHub.Nhs.OpenApi.Models.Configuration; +using LearningHub.Nhs.OpenApi.Models.Configuration; using LearningHub.Nhs.OpenApi.Services.HttpClients; -using LearningHub.Nhs.OpenApi.Services.Interface.HttpClients; using LearningHub.Nhs.OpenApi.Services.Interface.Services; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Text.Json.Nodes; using System.Text; using System.Threading.Tasks; using LearningHub.Nhs.Models.Databricks; using System.Linq; -using System.Net.Http.Headers; using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories; using LearningHub.Nhs.Models.Entities.DatabricksReport; using AutoMapper; -using LearningHub.Nhs.Models.Entities.Activity; -using LearningHub.Nhs.Models.Resource.Activity; using LearningHub.Nhs.Models.Common; -using LearningHub.Nhs.Models.Notification; using Microsoft.EntityFrameworkCore; -using LearningHub.Nhs.Models.Enums.Report; -using Newtonsoft.Json.Linq; using LearningHub.Nhs.OpenApi.Models.ViewModels; -using LearningHub.Nhs.Models.Resource; -using LearningHub.Nhs.OpenApi.Repositories.Repositories; -using LearningHub.Nhs.Models.Constants; -using LearningHub.Nhs.Models.Hierarchy; using LearningHub.Nhs.Models.Enums; using System.Text.Json; -using LearningHub.Nhs.Models.Entities.Resource; using LearningHub.Nhs.Models.Entities; namespace LearningHub.Nhs.OpenApi.Services.Services @@ -83,7 +67,7 @@ public DatabricksService(IOptions databricksConfig,IOptions public async Task IsUserReporter(int userId) { - string cacheKey = $"{CacheKey}_{userId}"; + string cacheKey = $"{userId}:{CacheKey}"; var userReportPermission = await this.cachingService.GetAsync(cacheKey); if (userReportPermission.ResponseEnum == CacheReadResponseEnum.Found) { @@ -129,7 +113,6 @@ public async Task IsUserReporter(int userId) /// public async Task CourseCompletionReport(int userId, DatabricksRequestModel model) { - userId = 22527; newEntry: if (model.ReportHistoryId == 0 && model.Take > 1) { @@ -214,7 +197,6 @@ public async Task CourseCompletionReport(int userId /// public async Task> GetPagedReportHistory(int userId,int page, int pageSize) { - userId = 22527; var result = new PagedResultSet(); var query = this.reportHistoryRepository.GetByUserIdAsync(userId); @@ -242,7 +224,6 @@ public async Task> GetPagedReportHistory(int /// public async Task GetPagedReportHistoryById(int userId, int reportHistoryId) { - userId = 22527; var result = new ReportHistoryModel(); var reportHistory = await this.reportHistoryRepository.GetByIdAsync(reportHistoryId); @@ -266,7 +247,6 @@ public async Task GetPagedReportHistoryById(int userId, int /// public async Task QueueReportDownload(int userId, int reportHistoryId) { - userId = 22527; var result = new ReportHistoryModel(); var reportHistory = await this.reportHistoryRepository.GetByIdAsync(reportHistoryId); @@ -332,7 +312,6 @@ public async Task QueueReportDownload(int userId, int reportHistoryId) /// public async Task DownloadReport(int userId, int reportHistoryId) { - userId = 22527; var response = new ReportHistoryModel(); var reportHistory = await this.reportHistoryRepository.GetByIdAsync(reportHistoryId); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index 4d62b872c..b3b92e8dd 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -137,6 +137,10 @@ "WarehouseId": "", "JobId": "", "UserPermissionEndpoint": "", - "CourseCompletionEndpoint": "" + "CourseCompletionEndpoint": "", + "ResourceId": "", + "TenantId": "", + "ClientId": "", + "ClientSecret": "" } } From 042fb60ec54e3db9ef01abfc0610e1ccd774e95d Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Thu, 4 Dec 2025 11:52:58 +0000 Subject: [PATCH 033/106] update --- .../Services/DatabricksService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs index 459f97d0a..472f46261 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs @@ -282,7 +282,8 @@ public async Task QueueReportDownload(int userId, int reportHistoryId) par_PageSize = 0, par_PageNumber = 0, par_Date_from = reportHistory.StartDate.GetValueOrDefault().ToString("yyyy-MM-dd"), - par_Date_to = reportHistory.EndDate.GetValueOrDefault().ToString("yyyy-MM-dd") + par_Date_to = reportHistory.EndDate.GetValueOrDefault().ToString("yyyy-MM-dd"), + par_reportId = reportHistoryId } }; From e8e51e536a8da90443da792d7cc37734407b11fa Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Thu, 4 Dec 2025 13:40:13 +0000 Subject: [PATCH 034/106] Test file --- LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj | 1 - LearningHub.Nhs.WebUI/package-lock.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index adada3f14..b2b68ba54 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -28,7 +28,6 @@ - diff --git a/LearningHub.Nhs.WebUI/package-lock.json b/LearningHub.Nhs.WebUI/package-lock.json index 17125bf85..1b09c4094 100644 --- a/LearningHub.Nhs.WebUI/package-lock.json +++ b/LearningHub.Nhs.WebUI/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "learninghubnhswebui", "version": "1.0.0", "license": "MIT", "dependencies": { From 7026c2d3c93c3ba1181a73b0b3e9776558708520 Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 4 Dec 2025 13:56:54 +0000 Subject: [PATCH 035/106] Track course click --- .../Controllers/SearchController.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs index 30c89a1e9..074bc39ab 100644 --- a/LearningHub.Nhs.WebUI/Controllers/SearchController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/SearchController.cs @@ -264,6 +264,45 @@ public void RecordResourceClick(string url, int nodePathId, int itemIndex, int p this.Response.Redirect(url); } + /// + /// The RecordClickedSearchResult. + /// + /// The url. + /// The nodePathId. + /// The itemIndex. + /// The page index. + /// The totalNumberOfHits. + /// The searchText. + /// The resourceReferenceId. + /// The groupdId. + /// The search id. + /// time of search. + /// user query. + /// search query. + /// the title. + [HttpGet("record-course-click")] + public void RecordCourseClick(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 + { + NodePathId = nodePathId, + ItemIndex = itemIndex, + NumberOfHits = pageIndex * this.Settings.FindwiseSettings.ResourceSearchPageSize, + TotalNumberOfHits = totalNumberOfHits, + SearchText = searchText, + ResourceReferenceId = resourceReferenceId, + GroupId = groupId, + SearchId = searchId, + TimeOfSearch = timeOfSearch, + UserQuery = userQuery, + Query = query, + Title = title, + }; + + this.searchService.CreateResourceSearchActionAsync(searchActionResourceModel); + this.Response.Redirect(url); + } + /// /// The RecordClickedCatalogueSearchResult. /// From ce26c41fe921e208053a60725070585813b1454c Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 4 Dec 2025 14:16:20 +0000 Subject: [PATCH 036/106] Pin the EXACT version vue-ctk-date-time-picker to see whether it fixes the build error --- AdminUI/LearningHub.Nhs.AdminUI/package.json | 2 +- LearningHub.Nhs.WebUI/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AdminUI/LearningHub.Nhs.AdminUI/package.json b/AdminUI/LearningHub.Nhs.AdminUI/package.json index 2117d7501..0c7f82eca 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/package.json +++ b/AdminUI/LearningHub.Nhs.AdminUI/package.json @@ -43,7 +43,7 @@ "vue-carousel": "^0.18.0", "vue-clamp": "0.4.1", "vue-click-outside": "1.1.0", - "vue-ctk-date-time-picker": "^2.5.0", + "vue-ctk-date-time-picker": "2.5.0", "vue-router": "^3.6.5", "vue-simple-progress": "^1.1.1", "vue-typeahead": "^2.3.2", diff --git a/LearningHub.Nhs.WebUI/package.json b/LearningHub.Nhs.WebUI/package.json index 1759001f8..19f91556a 100644 --- a/LearningHub.Nhs.WebUI/package.json +++ b/LearningHub.Nhs.WebUI/package.json @@ -47,7 +47,7 @@ "vue-carousel-3d": "^1.0.1", "vue-clamp": "0.4.1", "vue-click-outside": "1.1.0", - "vue-ctk-date-time-picker": "^2.5.0", + "vue-ctk-date-time-picker": "2.5.0", "vue-router": "^3.6.5", "vue-simple-progress": "^1.1.1", "vue-typeahead": "^2.3.2", From cee881ab2dae36794f41fd306c57d9096a224f88 Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 4 Dec 2025 14:37:27 +0000 Subject: [PATCH 037/106] Pin the EXACT version vue-ctk-date-time-picker --- AdminUI/LearningHub.Nhs.AdminUI/package.json | 2 +- LearningHub.Nhs.WebUI/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AdminUI/LearningHub.Nhs.AdminUI/package.json b/AdminUI/LearningHub.Nhs.AdminUI/package.json index 2117d7501..0c7f82eca 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/package.json +++ b/AdminUI/LearningHub.Nhs.AdminUI/package.json @@ -43,7 +43,7 @@ "vue-carousel": "^0.18.0", "vue-clamp": "0.4.1", "vue-click-outside": "1.1.0", - "vue-ctk-date-time-picker": "^2.5.0", + "vue-ctk-date-time-picker": "2.5.0", "vue-router": "^3.6.5", "vue-simple-progress": "^1.1.1", "vue-typeahead": "^2.3.2", diff --git a/LearningHub.Nhs.WebUI/package.json b/LearningHub.Nhs.WebUI/package.json index 1759001f8..19f91556a 100644 --- a/LearningHub.Nhs.WebUI/package.json +++ b/LearningHub.Nhs.WebUI/package.json @@ -47,7 +47,7 @@ "vue-carousel-3d": "^1.0.1", "vue-clamp": "0.4.1", "vue-click-outside": "1.1.0", - "vue-ctk-date-time-picker": "^2.5.0", + "vue-ctk-date-time-picker": "2.5.0", "vue-router": "^3.6.5", "vue-simple-progress": "^1.1.1", "vue-typeahead": "^2.3.2", From a56c7814c808e3504cc81f09d517a2f0c78e5c45 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Thu, 4 Dec 2025 15:27:58 +0000 Subject: [PATCH 038/106] Permission update --- .../Services/UserService.cs | 1 + .../Services/DatabricksService.cs | 72 +++++++++++-------- .../Services/NavigationPermissionService.cs | 7 +- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Services/UserService.cs b/AdminUI/LearningHub.Nhs.AdminUI/Services/UserService.cs index c84dbc2f2..f494e5287 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Services/UserService.cs +++ b/AdminUI/LearningHub.Nhs.AdminUI/Services/UserService.cs @@ -337,6 +337,7 @@ public async Task SendAdminPasswordResetEmail(int u public async Task ClearUserCachedPermissions(int userId) { await this.cacheService.RemoveAsync($"{userId}:AllRolesWithPermissions"); + await this.cacheService.RemoveAsync($"{userId}:DatabricksReporter"); return new LearningHubValidationResult(true); } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs index 472f46261..ad5baef89 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs @@ -67,47 +67,57 @@ public DatabricksService(IOptions databricksConfig,IOptions public async Task IsUserReporter(int userId) { + bool isReporter = false; string cacheKey = $"{userId}:{CacheKey}"; - var userReportPermission = await this.cachingService.GetAsync(cacheKey); - if (userReportPermission.ResponseEnum == CacheReadResponseEnum.Found) + try { - return userReportPermission.Item; - } - + var userReportPermission = await this.cachingService.GetAsync(cacheKey); + if (userReportPermission.ResponseEnum == CacheReadResponseEnum.Found) + { + return userReportPermission.Item; + } - DatabricksApiHttpClient databricksInstance = new DatabricksApiHttpClient(this.databricksConfig); - var sqlText = $"CALL {this.databricksConfig.Value.UserPermissionEndpoint}({userId});"; - const string requestUrl = "/api/2.0/sql/statements"; + DatabricksApiHttpClient databricksInstance = new DatabricksApiHttpClient(this.databricksConfig); - var requestPayload = new - { - warehouse_id = this.databricksConfig.Value.WarehouseId, - statement = sqlText, - wait_timeout = "30s", - on_wait_timeout = "CANCEL" - }; + var sqlText = $"CALL {this.databricksConfig.Value.UserPermissionEndpoint}({userId});"; + const string requestUrl = "/api/2.0/sql/statements"; - var jsonBody = JsonConvert.SerializeObject(requestPayload); - using var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + var requestPayload = new + { + warehouse_id = this.databricksConfig.Value.WarehouseId, + statement = sqlText, + wait_timeout = "30s", + on_wait_timeout = "CANCEL" + }; - var response = await databricksInstance.GetClient().PostAsync(requestUrl, content); + var jsonBody = JsonConvert.SerializeObject(requestPayload); + using var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); - var databricksResponse = await databricksInstance.GetClient().PostAsync(requestUrl, content); - if (databricksResponse.StatusCode is not HttpStatusCode.OK) - { - //log failure - return false; - } - var responseResult = await databricksResponse.Content.ReadAsStringAsync(); + var response = await databricksInstance.GetClient().PostAsync(requestUrl, content); - responseResult = responseResult.Trim(); - var root = JsonDocument.Parse(responseResult).RootElement; - string data = root.GetProperty("result").GetProperty("data_array")[0][0].GetString(); - bool isReporter = data == "1"; + var databricksResponse = await databricksInstance.GetClient().PostAsync(requestUrl, content); + if (databricksResponse.StatusCode is not HttpStatusCode.OK) + { + //log failure + return false; + } + var responseResult = await databricksResponse.Content.ReadAsStringAsync(); + + responseResult = responseResult.Trim(); + var root = JsonDocument.Parse(responseResult).RootElement; + string data = root.GetProperty("result").GetProperty("data_array")[0][0].GetString(); + isReporter = data == "1"; + + await this.cachingService.SetAsync(cacheKey, isReporter); + return isReporter; - await this.cachingService.SetAsync(cacheKey, isReporter); - return isReporter; + } + catch + { + await this.cachingService.SetAsync(cacheKey, isReporter); + return isReporter; + } } /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs index 9e28c5041..c8f3914a1 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs @@ -48,7 +48,7 @@ public async Task GetNavigationModelAsync(IPrincipal user, bool } else if (user.IsInRole("Administrator")) { - return AuthenticatedAdministrator(controllerName); + return await AuthenticatedAdministrator(controllerName, currentUserId); } else if (user.IsInRole("ReadOnly")) { @@ -97,8 +97,9 @@ public NavigationModel NotAuthenticated() /// The AuthenticatedAdministrator. ///
    /// The controller name. + /// userId. /// The . - private NavigationModel AuthenticatedAdministrator(string controllerName) + private async Task AuthenticatedAdministrator(string controllerName, int userId) { return new NavigationModel() { @@ -115,7 +116,7 @@ private NavigationModel AuthenticatedAdministrator(string controllerName) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, - ShowReports = true, + ShowReports = await this.databricksService.IsUserReporter(userId), }; } From 88f9ce47b7b16b81e3a01f2eb14eaa583e03aac7 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Fri, 5 Dec 2025 09:22:42 +0000 Subject: [PATCH 039/106] Menu Permission Update --- .../Services/NavigationPermissionService.cs | 6 +- .../Services/ReportService.cs | 62 ++++++++++++------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs index bae61e8eb..5934c7570 100644 --- a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs +++ b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs @@ -49,7 +49,7 @@ public async Task GetNavigationModelAsync(IPrincipal user, bool } else if (user.IsInRole("Administrator")) { - return this.AuthenticatedAdministrator(controllerName); + return await this.AuthenticatedAdministrator(controllerName); } else if (user.IsInRole("ReadOnly")) { @@ -100,7 +100,7 @@ public NavigationModel NotAuthenticated() /// /// The controller name. /// The . - private NavigationModel AuthenticatedAdministrator(string controllerName) + private async Task AuthenticatedAdministrator(string controllerName) { return new NavigationModel() { @@ -118,7 +118,7 @@ private NavigationModel AuthenticatedAdministrator(string controllerName) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, - ShowReports = true, + ShowReports = await this.reportService.GetReporterPermission(), }; } diff --git a/LearningHub.Nhs.WebUI/Services/ReportService.cs b/LearningHub.Nhs.WebUI/Services/ReportService.cs index de383e4df..788b56b6b 100644 --- a/LearningHub.Nhs.WebUI/Services/ReportService.cs +++ b/LearningHub.Nhs.WebUI/Services/ReportService.cs @@ -1,16 +1,16 @@ namespace LearningHub.Nhs.WebUI.Services { using System; - using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; - using elfhHub.Nhs.Models.Common; + using LearningHub.Nhs.Caching; using LearningHub.Nhs.Models.Common; using LearningHub.Nhs.Models.Databricks; + using LearningHub.Nhs.Models.Extensions; using LearningHub.Nhs.Models.Paging; - using LearningHub.Nhs.Models.Validation; using LearningHub.Nhs.WebUI.Interfaces; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -19,15 +19,22 @@ /// public class ReportService : BaseService, IReportService { + private readonly ICacheService cacheService; + private readonly IHttpContextAccessor contextAccessor; + /// /// Initializes a new instance of the class. /// + /// The cache service. + /// The contextAccessor. /// The Web Api Http Client. /// The Open Api Http Client. /// logger. - public ReportService(ILearningHubHttpClient learningHubHttpClient, IOpenApiHttpClient openApiHttpClient, ILogger logger) + public ReportService(ICacheService cacheService, IHttpContextAccessor contextAccessor, ILearningHubHttpClient learningHubHttpClient, IOpenApiHttpClient openApiHttpClient, ILogger logger) : base(learningHubHttpClient, openApiHttpClient, logger) { + this.cacheService = cacheService; + this.contextAccessor = contextAccessor; } /// @@ -36,26 +43,10 @@ public ReportService(ILearningHubHttpClient learningHubHttpClient, IOpenApiHttpC /// The . public async Task GetReporterPermission() { - bool viewmodel = false; - - var client = await this.OpenApiHttpClient.GetClientAsync(); - - var request = $"Report/GetReporterPermission"; - var response = await client.GetAsync(request).ConfigureAwait(false); - - if (response.IsSuccessStatusCode) - { - var result = response.Content.ReadAsStringAsync().Result; - viewmodel = JsonConvert.DeserializeObject(result); - } - else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized - || - response.StatusCode == System.Net.HttpStatusCode.Forbidden) - { - throw new Exception("AccessDenied"); - } - - return viewmodel; + bool response = false; + var cacheKey = $"{this.contextAccessor.HttpContext.User.Identity.GetCurrentUserId()}:DatabricksReporter"; + response = await this.cacheService.GetOrFetchAsync(cacheKey, this.FetchReporterPermission); + return response; } /// @@ -190,5 +181,28 @@ public async Task DownloadReport(int reportHistoryId) return apiResponse; } + + private async Task FetchReporterPermission() + { + bool viewmodel = false; + var client = await this.OpenApiHttpClient.GetClientAsync(); + + var request = $"Report/GetReporterPermission"; + var response = await client.GetAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + viewmodel = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized + || + response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + + return viewmodel; + } } } From 1eb04ab656b34afab308af22483fd1b8fa8b563b Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Fri, 5 Dec 2025 12:19:15 +0000 Subject: [PATCH 040/106] corrected the wording --- .../Views/Policies/AcceptableUsePolicy.cshtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml index b141b0129..3105eef83 100644 --- a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml @@ -68,7 +68,7 @@

    4.5.2.2 which requires a subscription or payment to gain access to such content;

    4.5.2.3 in which the user has a commercial interest;

    4.5.2.4 which promotes a business name and/or logo;

    -

    4.5.2.5 which contains a link to an app via iOS or Google Play or similar; or

    +

    4.5.2.5 which contains a link to an app via iOS, Google Play or similar; or

    4.5.2.6 which has as its purpose or effect the collection and sharing of personal data;

    4.5.3 be irrelevant to the purpose or aims of the Platform or while addressing relevant subject matter, contain an irrelevant, unsuitable or inappropriate slant (for example relating to potentially controversial opinions or beliefs of any kind intended to influence others);

    4.5.4 be defamatory of any person;

    @@ -129,7 +129,7 @@

    11.3 Course Managers shall also take responsibility for the following:

    11.3.1 Clear Expectations: Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.

    11.3.2 Inclusive Practice: Encourage participation from all learners/users and foster a respectful, inclusive environment that values diverse perspectives. This should involve reminding local forum users of the appropriate measures of this Acceptable Use Policy and potentially removing Contributions that infringe the rules of this Acceptable Use Policy including under paragraph 4.5 above.

    -

    11.3.3 Data Protection and Safety: Ensure that no sensitive or personal data is shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.

    +

    11.3.3 Data Protection and Safety: Ensure that no sensitive or personal data is shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.

    11.4 All users must comply with this Acceptable Use Policy. Breaches may result in removal of Contributions and/or withdrawal or suspension of user access without notice.

    From 14d0815813d17b1f136acf5f9de610c5bc95d7a2 Mon Sep 17 00:00:00 2001 From: Arunima George Date: Fri, 5 Dec 2025 15:15:33 +0000 Subject: [PATCH 041/106] TD-6653: Fixed pagination issue on update place of work screen. --- LearningHub.Nhs.WebUI/Controllers/MyAccountController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LearningHub.Nhs.WebUI/Controllers/MyAccountController.cs b/LearningHub.Nhs.WebUI/Controllers/MyAccountController.cs index d9ececfed..a91718384 100644 --- a/LearningHub.Nhs.WebUI/Controllers/MyAccountController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/MyAccountController.cs @@ -1398,7 +1398,7 @@ await this.userService.UpdateUserEmployment( } else { - if (!searchSubmission) + if (string.IsNullOrWhiteSpace(viewModel.FilterText)) { viewModel.SelectedWorkPlaceId = profile.LocationId.ToString(); viewModel.FilterText = profile.LocationName; From 4a426a37fbb0a0470a6b1b2c450a92eff563b311 Mon Sep 17 00:00:00 2001 From: Binon Date: Tue, 9 Dec 2025 15:41:41 +0000 Subject: [PATCH 042/106] Td-6646 and TD-6661 --- .../Views/Search/_ResourceSearchResult.cshtml | 18 +++++++++++++----- .../Views/Search/_SearchResult.cshtml | 16 +++++++++++++--- .../AzureSearch/SearchDocument.cs | 6 ++++++ .../Helpers/Search/SearchFilterBuilder.cs | 2 +- .../Helpers/Search/SearchOptionsBuilder.cs | 9 ++++----- .../Services/AzureSearch/AzureSearchService.cs | 12 +++++++++--- 6 files changed, 46 insertions(+), 17 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml index 50c8df8f4..91a16273d 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_ResourceSearchResult.cshtml @@ -15,11 +15,11 @@ var pagingModel = Model.ResourceResultPaging; var index = pagingModel.CurrentPage * pagingModel.PageSize; var searchString = HttpUtility.UrlEncode(Model.SearchString); + string groupId = HttpUtility.UrlEncode(Model.GroupId.ToString()); string GetUrl(int resourceReferenceId, int itemIndex, int nodePathId, SearchClickPayloadModel payload) { - var searchSignal = payload?.SearchSignal; - string groupId = HttpUtility.UrlEncode(Model.GroupId.ToString()); + var searchSignal = payload?.SearchSignal; string searchSignalQueryEncoded = HttpUtility.UrlEncode(HttpUtility.UrlDecode(searchSignal?.Query)); return $@"/search/record-resource-click?url=/Resource/{resourceReferenceId}&nodePathId={nodePathId}&itemIndex={payload?.HitNumber} @@ -28,7 +28,7 @@ &query={searchSignalQueryEncoded}&title={payload?.DocumentFields?.Title}"; } - string GetMoodleCourseUrl(string courseIdWithPrefix) + string GetMoodleCourseUrl(string courseIdWithPrefix, int resourceReferenceId, int itemIndex, int nodePathId, SearchClickPayloadModel payload) { const string prefix = "M"; @@ -46,7 +46,15 @@ if (int.TryParse(courseIdPart, out int courseId)) { - return moodleApiService.GetCourseUrl(courseId); + var searchSignal = payload?.SearchSignal; + + string searchSignalQueryEncoded = HttpUtility.UrlEncode(HttpUtility.UrlDecode(searchSignal?.Query)); + string url = moodleApiService.GetCourseUrl(courseId); + + return $@"/search/record-course-click?url={url}&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}"; } else { @@ -66,7 +74,7 @@

    @if (item.ResourceType == "moodle") { - @item.Title + @item.Title } else { diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml index 3c43d2553..96f2eb15c 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml @@ -29,12 +29,22 @@ &query={searchSignalQueryEncoded}&title={payload?.DocumentFields?.Title}"; } - string GetMoodleCourseUrl(string courseIdWithPrefix) + string GetMoodleCourseUrl(string courseIdWithPrefix, int resourceReferenceId,int itemIndex, int nodePathId, SearchClickPayloadModel payload) { if (int.TryParse(courseIdWithPrefix, out int courseId)) { - return moodleApiService.GetCourseUrl(courseId); + var searchSignal = payload?.SearchSignal; + + string searchSignalQueryEncoded = HttpUtility.UrlEncode(HttpUtility.UrlDecode(searchSignal?.Query)); + string url = moodleApiService.GetCourseUrl(courseId); + + return $@"/search/record-course-click?url={url}&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}"; + + // return moodleApiService.GetCourseUrl(courseId); } else { @@ -61,7 +71,7 @@

    @if (item.ResourceType == "moodle") { - @item.Title + @item.Title } else { diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs index 8f1c69261..3cfc36ed3 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs @@ -44,6 +44,12 @@ public string Id [JsonPropertyName("title")] public string Title { get; set; } = string.Empty; + /// + /// Gets or sets the normalised title. + /// + [JsonPropertyName("normalised_title")] + public string NormalisedTitle { get; set; } = string.Empty; + /// /// Gets or sets the description. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs index b0b43bf7f..e46b51cd2 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchFilterBuilder.cs @@ -24,7 +24,7 @@ public static Dictionary> CombineAndNormaliseFilters(string // Merge filters from both sources MergeFilterDictionary(filters, requestTypeFilters); - // MergeFilterDictionary(filters, providerFilters); + MergeFilterDictionary(filters, providerFilters); //NormaliseFilters(filters); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs index 6d3c62dbc..d3c98b9e9 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs @@ -34,7 +34,6 @@ public static SearchOptions BuildSearchOptions( Skip = offset, Size = pageSize, IncludeTotalCount = true, - // Filter = "is_deleted eq false", ScoringProfile = "boostExactTitle" }; @@ -112,10 +111,10 @@ private static string GetSortOption(Dictionary? sortBy) "authoreddate" => "date_authored", "authoredDate" => "date_authored", - "title" => "title", - "atoz" => "title", - "alphabetical" => "title", - "ztoa" => "title", + "title" => "normalised_title", + "atoz" => "normalised_title", + "alphabetical" => "normalised_title", + "ztoa" => "normalised_title", _ => null // unknown sort → ignore }; diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 0309915fd..877f71f5a 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -91,9 +91,6 @@ public async Task GetSearchResultAsync(SearchRequestModel sea var pageSize = searchRequestModel.PageSize; var offset = searchRequestModel.PageIndex * pageSize; - // Normalize resource_type filter values - var filters = SearchFilterBuilder.CombineAndNormaliseFilters(searchRequestModel.FilterText, searchRequestModel.ProviderFilterText); - // Build query string var query = searchQueryType == SearchQueryType.Full ? LuceneQueryBuilder.BuildLuceneQuery(searchRequestModel.SearchText) @@ -104,6 +101,15 @@ public async Task GetSearchResultAsync(SearchRequestModel sea { searchRequestModel.SortColumn, searchRequestModel.SortDirection } }; + // Normalize resource_type filter values + var filters = SearchFilterBuilder.CombineAndNormaliseFilters(searchRequestModel.FilterText, searchRequestModel.ProviderFilterText); + + if (searchRequestModel.CatalogueId.HasValue) + { + Dictionary> catalogueIdFilter = new Dictionary> { { "catalogue_id", new List { searchRequestModel.CatalogueId.ToString() } } }; + filters = filters == null ? catalogueIdFilter : filters.Concat(catalogueIdFilter).ToDictionary(k => k.Key, v => v.Value); + } + var searchOptions = SearchOptionsBuilder.BuildSearchOptions(searchQueryType, offset, pageSize, filters, sortBy, true); SearchResults filteredResponse = await this.searchClient.SearchAsync(query, searchOptions, cancellationToken); var count = Convert.ToInt32(filteredResponse.TotalCount); From 9d1b38a0e151c4efa401c33ab89469ccda5707dd Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Wed, 10 Dec 2025 16:28:08 +0000 Subject: [PATCH 043/106] TD-6652 fixes --- .../Controllers/AccountController.cs | 10 ++ .../Controllers/ReportsController.cs | 36 +++-- .../Filters/ReporterPermissionFilter.cs | 52 +++++++ .../Helpers/CommonValidationErrorMessages.cs | 5 + .../Helpers/UtilityHelper.cs | 16 ++ .../Report/ReportCreationCourseSelection.cs | 5 +- .../Report/ReportCreationDateSelection.cs | 12 +- .../Startup/ServiceMappings.cs | 1 + .../Styles/nhsuk/pages/reporting.scss | 57 ++++--- .../Views/Account/AccessRestricted.cshtml | 32 ++++ .../Reports/CourseCompletionReport.cshtml | 30 +++- .../CreateReportCourseSelection.cshtml | 6 +- .../Reports/CreateReportDateSelection.cshtml | 141 ++++++++++-------- .../Views/Reports/Index.cshtml | 11 +- .../DynamicCheckboxes/Default.cshtml | 9 +- .../LearningHub.Nhs.OpenApi/appsettings.json | 2 +- .../Tables/Databricks/ReportHistory.sql | 4 +- 17 files changed, 310 insertions(+), 119 deletions(-) create mode 100644 LearningHub.Nhs.WebUI/Filters/ReporterPermissionFilter.cs create mode 100644 LearningHub.Nhs.WebUI/Views/Account/AccessRestricted.cshtml diff --git a/LearningHub.Nhs.WebUI/Controllers/AccountController.cs b/LearningHub.Nhs.WebUI/Controllers/AccountController.cs index d22a8485e..1309ff694 100644 --- a/LearningHub.Nhs.WebUI/Controllers/AccountController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/AccountController.cs @@ -1167,6 +1167,16 @@ public IActionResult InvalidUserAccount() return this.View(); } + /// + /// The user does not have permissions to access a section of the Learning Hub. + /// + /// The . + [HttpGet] + public IActionResult AccessRestricted() + { + return this.View(); + } + /// /// The user already has an already active session. Then prevent concurrent access to the Learning Hub. /// diff --git a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs index 4ea7d3a5b..c45225aaf 100644 --- a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs @@ -5,21 +5,15 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; - using Azure; using GDS.MultiPageFormData; using GDS.MultiPageFormData.Enums; using LearningHub.Nhs.Caching; using LearningHub.Nhs.Models.Databricks; - using LearningHub.Nhs.Models.Moodle; - using LearningHub.Nhs.Models.MyLearning; using LearningHub.Nhs.Models.Paging; using LearningHub.Nhs.WebUI.Configuration; using LearningHub.Nhs.WebUI.Filters; using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; - using LearningHub.Nhs.WebUI.Models; - using LearningHub.Nhs.WebUI.Models.Account; - using LearningHub.Nhs.WebUI.Models.DynamicCheckbox; using LearningHub.Nhs.WebUI.Models.Learning; using LearningHub.Nhs.WebUI.Models.Report; using Microsoft.AspNetCore.Authorization; @@ -27,12 +21,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; - using NHSUKViewComponents.Web.ViewModels; /// /// Defines the . /// - // [ServiceFilter(typeof(LoginWizardFilter))] + [ServiceFilter(typeof(LoginWizardFilter))] + [ServiceFilter(typeof(ReporterPermissionFilter))] [Authorize] [Route("Reports")] public class ReportsController : BaseController @@ -158,7 +152,7 @@ public async Task CreateReportCourseSelection(ReportCreationCours { var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); - if (courseSelection != null) + if (courseSelection.Courses != null) { if (courseSelection.Courses.Any()) { @@ -208,11 +202,19 @@ public async Task CreateReportDateSelection() Skip = 1, }); - DateTime startDate = DateTime.MinValue; - var validDate = DateTime.TryParse(result.MinValidDate, out startDate); - dateVM.StartDay = validDate ? startDate.Day : 0; - dateVM.StartMonth = validDate ? startDate.Month : 0; - dateVM.StartYear = validDate ? startDate.Year : 0; + if (result != null) + { + var validDate = DateTime.TryParse(result.MinValidDate, out DateTime startDate); + dateVM.DataStart = validDate ? startDate : null; + dateVM.HintText = validDate + ? $"For example, {startDate.Day} {startDate.Month} {startDate.Year}" + : $"For example, {DateTime.Now.Day} {DateTime.Now.Month} {DateTime.Now.Year}"; + } + else + { + dateVM.DataStart = null; + dateVM.HintText = $"For example, {DateTime.Now.Day} {DateTime.Now.Month} {DateTime.Now.Year}"; + } } return this.View(dateVM); @@ -232,6 +234,12 @@ public async Task CreateReportSummary(ReportCreationDateSelection // validate date var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); reportCreation.TimePeriod = reportCreationDate.TimePeriod; + + if (!this.ModelState.IsValid) + { + return this.View("CreateReportDateSelection", reportCreationDate); + } + reportCreation.StartDate = reportCreationDate.GetStartDate(); reportCreation.EndDate = reportCreationDate.GetEndDate(); await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); diff --git a/LearningHub.Nhs.WebUI/Filters/ReporterPermissionFilter.cs b/LearningHub.Nhs.WebUI/Filters/ReporterPermissionFilter.cs new file mode 100644 index 000000000..d1549807a --- /dev/null +++ b/LearningHub.Nhs.WebUI/Filters/ReporterPermissionFilter.cs @@ -0,0 +1,52 @@ +namespace LearningHub.Nhs.WebUI.Filters +{ + using System.Threading.Tasks; + using LearningHub.Nhs.WebUI.Interfaces; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using Microsoft.AspNetCore.Routing; + + /// + /// Defines the . + /// + public class ReporterPermissionFilter : ActionFilterAttribute + { + private readonly IReportService reportService; + + /// + /// Initializes a new instance of the class. + /// + /// reportService. + public ReporterPermissionFilter(IReportService reportService) + { + this.reportService = reportService; + } + + /// + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var controller = context.ActionDescriptor.RouteValues["Controller"]; + var action = context.ActionDescriptor.RouteValues["Action"]; + + // Run your cached permission check + var hasPermission = await this.reportService.GetReporterPermission(); + + // If user does NOT have permission and they're not in safe routes + if (!hasPermission + && !(controller == "Account" && action == "AccessRestricted") + && !(controller == "Home" && action == "Logout")) + { + context.Result = new RedirectToRouteResult( + new RouteValueDictionary + { + { "Controller", "Account" }, + { "Action", "AccessRestricted" }, + }); + + return; + } + + await next(); + } + } +} diff --git a/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs b/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs index e763fefa1..1002c86fa 100644 --- a/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs +++ b/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs @@ -75,6 +75,11 @@ public static class CommonValidationErrorMessages ///

    public const string StartDate = "Enter a start date containing a day, month and a year"; + /// + /// Start date Required. + /// + public const string EndDate = "Enter an end date containing a day, month and a year"; + /// /// Workplace Required. /// diff --git a/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs index 762695dd9..2bf1edf88 100644 --- a/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs @@ -522,5 +522,21 @@ public static string GetPillColour(string filename) return breadcrumbs; } + + /// + /// Returns sentence case of input string. + /// + /// input. + /// A sentence case string corresponding to the input string. + public static string ConvertToSentenceCase(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + input = input.ToLower(); + return char.ToUpper(input[0]) + input.Substring(1); + } } } diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs index 8d7600987..f870f9fda 100644 --- a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; + using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Models.DynamicCheckbox; using NHSUKViewComponents.Web.ViewModels; @@ -37,9 +38,9 @@ public List BuildCourses(List new DynamicCheckboxItemViewModel { Value = r.Key.ToString(), - Label = r.Value, + Label = UtilityHelper.ConvertToSentenceCase(r.Value), }).ToList(); - this.AllCources.Insert(0, new DynamicCheckboxItemViewModel { Value = "all", Label = "All Courses", }); + this.AllCources.Insert(0, new DynamicCheckboxItemViewModel { Value = "all", Label = "All courses", }); return this.AllCources; } } diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs index 2263a0aa1..6731eff64 100644 --- a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs @@ -60,6 +60,11 @@ public class ReportCreationDateSelection : IValidatableObject ///
    public bool EndDate { get; set; } + /// + /// Gets or sets a value indicating whether gets or sets the HintText. + /// + public string HintText { get; set; } + /// /// Gets or sets the GetDate. /// @@ -85,11 +90,7 @@ public IEnumerable Validate(ValidationContext validationContex if (this.TimePeriod == "Custom") { this.ValidateStartDate(validationResults); - - if (this.EndDate) - { - this.ValidateEndDate(validationResults); - } + this.ValidateEndDate(validationResults); } return validationResults; @@ -126,6 +127,7 @@ public List PopulateDateRange() new RadiosItemViewModel("7", "7 days", false, null), new RadiosItemViewModel("30", "30 days", false, null), new RadiosItemViewModel("90", "90 days", false, null), + new RadiosItemViewModel("Custom", "Custom date range", false, null), }; // if (string.IsNullOrWhiteSpace(this.TimePeriod)) diff --git a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs index 26f75aeb1..60ff5f634 100644 --- a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs +++ b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs @@ -120,6 +120,7 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } } diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss index 0f6d99905..d5fec1b2a 100644 --- a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss @@ -19,23 +19,10 @@ width: auto; } - .nhsuk-summary-list__row { - display: flex !important; - align-items: flex-start; /* optional: aligns top of key/value */ - } - - .nhsuk-summary-list__row { - border-bottom: none; - } - - .nhsuk-summary-list__key, .nhsuk-summary-list__value, .nhsuk-summary-list__actions { - border: none; - } - .nhsuk-date-inline { display: flex; align-items: center; - gap: 0.5rem; /* space between label and input */ + gap: 0.5rem; } .nhsuk-date-inline .nhsuk-label { @@ -44,13 +31,47 @@ } .nhsuk-button--with-border { - border: 2px solid #005eb8; - background-color: #ffffff; - color: #005eb8; + border: 2px solid #005eb8; + background-color: #ffffff; + color: #005eb8; } .nhsuk-button--with-border:hover { background-color: #f0f8ff; - border-color: #003087; + border-color: #003087; + } + + .date-range-container { + display: flex; + flex-wrap: wrap; + gap: 1rem; + } + + .date-range-item { + flex: 0 1 auto; + } + + @media (max-width: 767px) { + .date-range-container .nhsuk-label { + min-width: 50px; + } + } + + .nhsuk-radios__divider { + text-align: center; + width: 40px; + } + + .nhsuk-hint { + margin-bottom: 0.5rem; + } + + .date-range-container .nhsuk-hint { + display: block; + max-height: 0; + overflow: hidden; + margin: 0; + padding: 0; + visibility: hidden; } } diff --git a/LearningHub.Nhs.WebUI/Views/Account/AccessRestricted.cshtml b/LearningHub.Nhs.WebUI/Views/Account/AccessRestricted.cshtml new file mode 100644 index 000000000..ec8385d39 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Account/AccessRestricted.cshtml @@ -0,0 +1,32 @@ +@{ + ViewData["Title"] = "You do not have the required permissions to access this part of the Learning Hub"; + Layout = ViewData["Layout"].ToString(); +} + +@section styles{ + +} + +
    +
    +
    +
    +

    Access restricted

    +
    +

    You do not have the required permissions to access this part of the Learning Hub yet.

    +
    +
    +
    +
    +
    +
    +
    +

    If you think you should have access, please contact the support team.

    +
    +
    +
    +
    +
    +
    + + diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml index 9677805f1..ac75d6e04 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml @@ -40,7 +40,6 @@

    Course completion report

    -
    Course@(distinctCourses.Count() > 1 ? "s" : "") @@ -92,10 +91,12 @@
    -

    Displaying @startRow–@endRow of @Model.TotalCount filtered row@(Model.TotalCount > 1 ? "s" : "")

    - @if (Model.TotalCount != 0) + + @if (Model.TotalCount > 0) { +

    Displaying @startRow–@endRow of @Model.TotalCount filtered row@(Model.TotalCount > 1 ? "s" : "")

    +

    Request to download this report in a spreadsheet (.xls) format.You will be notified when the report is ready. @@ -119,12 +120,33 @@

    } } + else + { +

    Displaying no results

    + } @if (Model.CourseCompletionRecords.Any()) { @await Html.PartialAsync("_ReportTable", Model) } - + else + { +
      +
    • +
      +
      +

      + No information is available +

      +

      + Please adjust your filters +

      +
      +
      +
    • +
    + } +
    diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml index f6a7e947c..601acf947 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportCourseSelection.cshtml @@ -28,7 +28,7 @@

    Select course(s)

    -
    +
    @if (errorHasOccurred) { @@ -47,7 +47,7 @@ propertyName = nameof(Model.Courses) })
    -
    +
    @@ -60,3 +60,5 @@
    + + diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml index 745e1e0d4..1365a86d7 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml @@ -10,7 +10,7 @@ var errorHasOccurred = !ViewData.ModelState.IsValid; var customId = $"TimePeriod-{Model.PopulateDateRange().Count()}"; var hintTextLines = new List { $"For example, {Model.StartDay} {Model.StartMonth} {Model.StartYear}" }; - var endHintTextLines = new List { $" " }; + var hintTextLine = new List { $" " }; } @section styles { @@ -38,12 +38,12 @@ }
    - + @Html.HiddenFor(x=>x.HintText) + @Html.HiddenFor(x =>x.DataStart)

    -

    @@ -53,80 +53,95 @@ @foreach (var (radio, index) in Model.PopulateDateRange().Select((r, i) => (r, i))) { var radioId = $"TimePeriod-{index}"; -
    - - - @if (radio.HintText != null) - { -
    - @radio.HintText + @if (radio.Value != "Custom") + { + +
    + + + @if (radio.HintText != null) + { +
    + @radio.HintText +
    + } +
    + } + else + { +
    or
    +
    +
    + + +
    + @Model.HintText +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    - } -
    +
    + } } - -
    or
    - -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    + + +
    - -
    +
    -
    - - + + + + + + diff --git a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml index 7e8d5e6b7..a61f12b43 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml @@ -29,7 +29,7 @@

    Reports

    -

    View and manage your reports

    +

    View and manage your reports

    @@ -52,12 +52,13 @@

    Previously run reports

    -

    - Reports are stored for 30 days from the date they’re generated. If you need to keep a copy, make sure you download it before it expires. -

    -
    + @if (Model.ReportHistoryModels.Any()) { +

    + Reports are stored for 30 days from the date they’re generated. If you need to keep a copy, make sure you download it before it expires. +

    +
    @foreach (var entry in Model.ReportHistoryModels) { var matchedCourseNames = new List(); diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml index 8099618a4..f8fd14fff 100644 --- a/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml @@ -6,9 +6,12 @@
    - - @Model.Label - + @if (!string.IsNullOrEmpty(Model.Label)) + { + + @Model.Label + + }
    @for (int i = 0; i < Model.Checkboxes.Count; i++) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index b3b92e8dd..385a7488e 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -111,7 +111,7 @@ "SignOutUrl": "/home/logout", "MyAccountUrl": "/myaccount", "BrowseCataloguesUrl": "/allcatalogue", - "ReportUrl": "" + "ReportUrl": "/reports" }, "LearningHubAPIConfig": { "BaseUrl": "https://learninghub.nhs.uk/api" diff --git a/WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql b/WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql index 61c2cec72..1f41cd695 100644 --- a/WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql +++ b/WebAPI/LearningHub.Nhs.Database/Tables/Databricks/ReportHistory.sql @@ -13,8 +13,8 @@ [ReportStatusId] INT NULL, [FilePath] NVARCHAR(1024) NULL, [DownloadedDate] [datetimeoffset](7) NULL, - [ParentJobRunId] INT NULL, - [JobRunId] INT NULL, + [ParentJobRunId] BIGINT NULL, + [JobRunId] BIGINT NULL, [ProcessingMessage] NVARCHAR(1024) NULL, [Deleted] [bit] NOT NULL, [CreateUserId] INT NOT NULL, From 2add541f10d970e5c62d859de3246a60e9905ebf Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Fri, 12 Dec 2025 12:58:42 +0000 Subject: [PATCH 044/106] Addition of StatMandId config --- OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index 385a7488e..908b53d67 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -111,7 +111,8 @@ "SignOutUrl": "/home/logout", "MyAccountUrl": "/myaccount", "BrowseCataloguesUrl": "/allcatalogue", - "ReportUrl": "/reports" + "ReportUrl": "/reports", + "StatMandId": 0 }, "LearningHubAPIConfig": { "BaseUrl": "https://learninghub.nhs.uk/api" From 217f32b06ad3a46b9390c2951053b48a4b674ed1 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Tue, 16 Dec 2025 21:18:26 +0000 Subject: [PATCH 045/106] TD-6652 Update --- .../Controllers/ReportsController.cs | 79 ++++++++++++------- .../Helpers/CommonValidationErrorMessages.cs | 5 ++ .../DynamicCheckboxItemViewModel.cs | 5 ++ .../Report/ReportCreationCourseSelection.cs | 2 +- .../vuesrc/notification/notification.vue | 2 + .../vuesrc/notification/notificationModel.ts | 1 + .../DynamicCheckboxesViewComponent.cs | 1 + .../Reports/CourseCompletionReport.cshtml | 35 +++++--- .../Reports/CreateReportDateSelection.cshtml | 23 +++--- .../DynamicCheckboxes/Default.cshtml | 5 +- .../Services/NotificationService.cs | 2 + .../LearningHub.Nhs.OpenApi/appsettings.json | 4 +- 12 files changed, 106 insertions(+), 58 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs index c45225aaf..11dfd162f 100644 --- a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs @@ -180,7 +180,7 @@ public async Task CreateReportDateSelection() var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); var dateVM = new ReportCreationDateSelection(); dateVM.TimePeriod = reportCreation.TimePeriod; - if (reportCreation.StartDate.HasValue) + if (reportCreation.StartDate.HasValue && reportCreation.TimePeriod == "Custom") { dateVM.StartDay = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Day : 0; dateVM.StartMonth = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Month : 0; @@ -189,33 +189,11 @@ public async Task CreateReportDateSelection() dateVM.EndMonth = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Month : 0; dateVM.EndYear = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Year : 0; } - else - { - var result = await this.reportService.GetCourseCompletionReport(new DatabricksRequestModel - { - StartDate = reportCreation.StartDate, - EndDate = reportCreation.EndDate, - TimePeriod = reportCreation.TimePeriod, - Courses = reportCreation.Courses, - ReportHistoryId = reportCreation.ReportHistoryId, - Take = 1, - Skip = 1, - }); - - if (result != null) - { - var validDate = DateTime.TryParse(result.MinValidDate, out DateTime startDate); - dateVM.DataStart = validDate ? startDate : null; - dateVM.HintText = validDate - ? $"For example, {startDate.Day} {startDate.Month} {startDate.Year}" - : $"For example, {DateTime.Now.Day} {DateTime.Now.Month} {DateTime.Now.Year}"; - } - else - { - dateVM.DataStart = null; - dateVM.HintText = $"For example, {DateTime.Now.Day} {DateTime.Now.Month} {DateTime.Now.Year}"; - } - } + + var minDate = await this.GetMinDate(); + + dateVM.DataStart = minDate.DataStart; + dateVM.HintText = minDate.HintText; return this.View(dateVM); } @@ -235,8 +213,20 @@ public async Task CreateReportSummary(ReportCreationDateSelection var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); reportCreation.TimePeriod = reportCreationDate.TimePeriod; + if (reportCreation.TimePeriod == null) + { + var minDate = await this.GetMinDate(); + reportCreationDate.DataStart = minDate.DataStart; + reportCreationDate.HintText = minDate.HintText; + this.ModelState.AddModelError("TimePeriod", CommonValidationErrorMessages.ReportingPeriodRequired); + return this.View("CreateReportDateSelection", reportCreationDate); + } + if (!this.ModelState.IsValid) { + var minDate = await this.GetMinDate(); + reportCreationDate.DataStart = minDate.DataStart; + reportCreationDate.HintText = minDate.HintText; return this.View("CreateReportDateSelection", reportCreationDate); } @@ -433,5 +423,38 @@ private async Task>> GetCoursesAsync() return courses; } + + private async Task GetMinDate() + { + var dateVM = new ReportCreationDateSelection(); + var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + + var result = await this.reportService.GetCourseCompletionReport(new DatabricksRequestModel + { + StartDate = null, + EndDate = null, + TimePeriod = reportCreation.TimePeriod, + Courses = reportCreation.Courses, + ReportHistoryId = 0, + Take = 1, + Skip = 1, + }); + + if (result != null) + { + var validDate = DateTime.TryParse(result.MinValidDate, out DateTime startDate); + dateVM.DataStart = validDate ? startDate : null; + dateVM.HintText = validDate + ? $"For example, {startDate.Day} {startDate.Month} {startDate.Year}" + : $"For example, {DateTime.Now.Day} {DateTime.Now.Month} {DateTime.Now.Year}"; + } + else + { + dateVM.DataStart = null; + dateVM.HintText = $"For example, {DateTime.Now.Day} {DateTime.Now.Month} {DateTime.Now.Year}"; + } + + return dateVM; + } } } diff --git a/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs b/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs index 1002c86fa..ddd4b54f4 100644 --- a/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs +++ b/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs @@ -269,5 +269,10 @@ public static class CommonValidationErrorMessages /// Course Required. /// public const string CourseRequired = "Select a course"; + + /// + /// Course Required. + /// + public const string ReportingPeriodRequired = "Select a reporting period"; } } diff --git a/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs index 47cd5ea44..7b1c2759b 100644 --- a/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs +++ b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs @@ -24,5 +24,10 @@ public class DynamicCheckboxItemViewModel /// Gets or sets a value indicating whether gets or sets a selected. /// public bool Selected { get; set; } + + /// + /// Gets or sets a value indicating whether the option is exclusive. + /// + public bool Exclusive { get; set; } = false; } } diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs index f870f9fda..3820fe0f9 100644 --- a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs +++ b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs @@ -40,7 +40,7 @@ public List BuildCourses(ListReports
    -

    Course completion report

    +

    Course progress report

    Course@(distinctCourses.Count() > 1 ? "s" : "")
    -
      - @foreach (var entry in distinctCourses) - { -
    • @entry
    • - } -
    + @if (distinctCourses.Count() > 1) + { +
      + @foreach (var entry in distinctCourses) + { +
    • @entry
    • + } +
    + } + else + { +

    @distinctCourses.FirstOrDefault()

    + } +
    @@ -97,15 +105,16 @@ {

    Displaying @startRow–@endRow of @Model.TotalCount filtered row@(Model.TotalCount > 1 ? "s" : "")

    -

    - Request to download this report in a spreadsheet (.xls) format.You will be notified - when the report is ready. -

    + @if (Model.ReportHistoryModel != null && Model.ReportHistoryModel.DownloadRequest == null) { +

    + Request to download this report in a spreadsheet (.xls) format.You will be notified + when the report is ready. +

    - +
    } else if (Model.ReportHistoryModel != null && Model.ReportHistoryModel.DownloadRequest == true && Model.ReportHistoryModel.ReportStatusId == ((int)Status.Pending)) diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml index 1365a86d7..d09c85e1e 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml @@ -27,8 +27,9 @@
    Create a course completion report
    +

    - Reporting Period + Reporting period

    @@ -36,36 +37,32 @@ { } -
    @Html.HiddenFor(x=>x.HintText) @Html.HiddenFor(x =>x.DataStart) -
    +
    - -

    -

    -
    For the last:
    +
    @foreach (var (radio, index) in Model.PopulateDateRange().Select((r, i) => (r, i))) { - var radioId = $"TimePeriod-{index}"; + var radioId = index == 0 ? "TimePeriod" : $"TimePeriod-{index}"; @if (radio.Value != "Custom") {
    + data-val="false" /> @@ -92,7 +89,7 @@
    - +
    - +
    - +
    diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml index f8fd14fff..3f4ad4c5d 100644 --- a/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Shared/Components/DynamicCheckboxes/Default.cshtml @@ -2,6 +2,7 @@ @model DynamicCheckboxesViewModel @{ var propertyName = ViewData["PropertyName"]?.ToString() ?? "SelectedValues"; + var exclusiveGroup = $"{propertyName}-list"; }
    @@ -17,7 +18,7 @@ @for (int i = 0; i < Model.Checkboxes.Count; i++) { var checkbox = Model.Checkboxes[i]; - var inputId = $"{propertyName}_{i}"; + var inputId = i == 0 ? propertyName : $"{propertyName}_{i}";
    From 8a4e45feb221d35e519b8da0fd77f207ae8dae58 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Mon, 5 Jan 2026 12:28:04 +0000 Subject: [PATCH 052/106] Caps update --- LearningHub.Nhs.WebUI/Controllers/ReportsController.cs | 2 +- LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs index b38d418af..12ad10acd 100644 --- a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs @@ -412,7 +412,7 @@ public async Task CourseProgressReport(CourseCompletionViewModel if (reportCreation.Courses.Count == 0) { - matchedCourseNames = new List { "all courses" }; + matchedCourseNames = new List { "All courses" }; } else { diff --git a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml index 854f7978d..26a7a270f 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml @@ -66,8 +66,8 @@ if (string.IsNullOrWhiteSpace(entry.CourseFilter)) { - matchedCourseNames = "all courses"; - matchedCourseNamesDetails = new List { "all courses" }; + matchedCourseNames = "All courses"; + matchedCourseNamesDetails = new List { "All courses" }; } else { @@ -96,7 +96,7 @@
    - course progress for @matchedCourseNames + Course progress for @matchedCourseNames @if (downloadCheck) { From dbc9e6511d6aca6e7ae0daf5c12bee527ecf7654 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Mon, 5 Jan 2026 13:35:57 +0000 Subject: [PATCH 053/106] Course Casing at the API and report notification upate --- .../Scripts/vuesrc/notification/notifications.vue | 2 ++ .../Services/DatabricksService.cs | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/notification/notifications.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/notification/notifications.vue index 30adc00e0..2df32d390 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/notification/notifications.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/notification/notifications.vue @@ -180,6 +180,8 @@ return [{ text: 'Action required', className: 'fa-solid fa-triangle-exclamation text-warning' }]; case NotificationType.AccessRequest: return [{ text: 'Access request', className: 'fas fa-lock-alt text-dark pt-1' }]; + case NotificationType.ReportProcessed: + return [{ text: 'Report processed', className: 'fa-solid fa-circle-check text-success' }]; default: return [{ text: 'unknown', className: '' }]; } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs index 17c3325b9..922c739ed 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs @@ -400,7 +400,7 @@ public async Task UpdateDatabricksReport(int userId, DatabricksUpdateRequest dat if (string.IsNullOrWhiteSpace(reportHistory.CourseFilter)) { - firstCourse = "all courses"; + firstCourse = "All courses"; } else { @@ -422,7 +422,7 @@ public async Task UpdateDatabricksReport(int userId, DatabricksUpdateRequest dat try { - var notificationId = await this.notificationService.CreateReportNotificationAsync(userId, "course progress", firstCourse); + var notificationId = await this.notificationService.CreateReportNotificationAsync(userId, "Course progress", firstCourse); if (notificationId > 0) { @@ -433,7 +433,7 @@ public async Task UpdateDatabricksReport(int userId, DatabricksUpdateRequest dat new ReportSucessEmailModel { UserFirstName = user.FirstName, - ReportName = "course progress", + ReportName = "Course progress", ReportTitle = firstCourse, ReportUrl = $"{this.learningHubConfig.Value.BaseUrl.TrimEnd('/')}/{this.learningHubConfig.Value.ReportUrl.TrimStart('/')}" }); From dd4392b9319a56da30f753712b0ca306ea971dab Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Mon, 5 Jan 2026 14:31:14 +0000 Subject: [PATCH 054/106] . --- LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml index 26a7a270f..1a5a95122 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml @@ -133,7 +133,7 @@
    Type:
    -
    course progress
    +
    Course progress
    Reporting on:
    From 84107636c96862e40c9516945c7220e5e1033c18 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Tue, 6 Jan 2026 10:41:47 +0000 Subject: [PATCH 055/106] Additional error message and status formating --- .../Helpers/ViewActivityHelper.cs | 15 ++++++++++++++ .../vuesrc/notification/notifications.vue | 2 +- .../Reports/CreateReportDateSelection.cshtml | 20 ++++++++++++++----- .../Views/Reports/_ReportPaging.cshtml | 4 ++-- .../Views/Reports/_ReportTable.cshtml | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs index a357fa371..d7602ee98 100644 --- a/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/ViewActivityHelper.cs @@ -524,5 +524,20 @@ public static bool CanDownloadCertificate(this ActivityDetailedItemViewModel act return false; } + + /// + /// GetReportStatusDisplayText. + /// + /// The status. + /// The string. + public static string GetReportStatusDisplayText(string status) + { + if (status == "Not completed") + { + return "In progress"; + } + + return status; + } } } diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/notification/notifications.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/notification/notifications.vue index 2df32d390..d550e1e27 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/notification/notifications.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/notification/notifications.vue @@ -181,7 +181,7 @@ case NotificationType.AccessRequest: return [{ text: 'Access request', className: 'fas fa-lock-alt text-dark pt-1' }]; case NotificationType.ReportProcessed: - return [{ text: 'Report processed', className: 'fa-solid fa-circle-check text-success' }]; + return [{ text: 'Report', className: 'fa-solid fa-circle-check text-success' }]; default: return [{ text: 'unknown', className: '' }]; } diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml index 62f6be07f..62c733db2 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml @@ -36,16 +36,16 @@
    Create a course progress report
    - + @if (errorHasOccurred) + { + + }

    Reporting period

    - @if (errorHasOccurred) - { - - } + @Html.HiddenFor(x=>x.HintText) @@ -56,6 +56,16 @@ For the last:
    + @if (errorHasOccurred) + { +
    + + + Error: Select a reporting period + +
    + } +
    @foreach (var (radio, index) in Model.PopulateDateRange().Select((r, i) => (r, i))) { diff --git a/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml index 5fe25a280..37af21817 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml @@ -42,7 +42,7 @@ else {
  • - + Previous : @previousMessage @@ -61,7 +61,7 @@ else {
  • - + Next : @nextMessage diff --git a/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml index a5aa336c3..733d9a72b 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml @@ -139,7 +139,7 @@ Status - @entry.CourseStatus + @ViewActivityHelper.GetReportStatusDisplayText(entry.CourseStatus) From e5f931a180ce3fe872d52bf8989b8cdbe71667ad Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Tue, 6 Jan 2026 16:58:54 +0000 Subject: [PATCH 056/106] inject build number in to swagger --- OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json b/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json index 1fd6fa2d1..d0fa21e83 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json @@ -3,7 +3,7 @@ "info": { "title": "LearningHub.NHS.OpenAPI", "version": "1.3.0", - "description": "A set of API endpoints for retrieving learning resource information from the Learning Hub learning platform. The [Learning Hub](https://learninghub.nhs.uk/) is a platform for hosting and sharing learning resources for health and social care provided by Technology Enhanced Learning (TEL) at NHS England. An application API key must be used to authorise calls to the API from external applications. To contact TEL to discuss connecting your external system to the Learning Hub, email england.tel@nhs.net." + "description": "A set of API endpoints for retrieving learning resource information from the Learning Hub learning platform. The [Learning Hub](https://learninghub.nhs.uk/) is a platform for hosting and sharing learning resources for health and social care provided by Technology Enhanced Learning (TEL) at NHS England. An application API key must be used to authorise calls to the API from external applications. To contact TEL to discuss connecting your external system to the Learning Hub, email england.tel@nhs.net. \n\n**Build Number:** __BUILD_NUMBER__\n\n" }, "paths": { "/Bookmark/GetAllByParent": { From 8d632f442f788970ed9f050c5c5a7572bfdf4d9d Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Tue, 6 Jan 2026 16:59:22 +0000 Subject: [PATCH 057/106] Update azure-pipeline-openapi-reportapi-ci.yml inject build number in to swagger --- .../azure-pipeline-openapi-reportapi-ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/azure-pipeline-openapi-reportapi-ci.yml b/.github/azure-pipeline-openapi-reportapi-ci.yml index 1b10679e6..4b39bddf8 100644 --- a/.github/azure-pipeline-openapi-reportapi-ci.yml +++ b/.github/azure-pipeline-openapi-reportapi-ci.yml @@ -25,6 +25,25 @@ jobs: - checkout: self clean: true fetchTags: false + # Inject Build Number into Swagger JSON + - task: PowerShell@2 + displayName: 'Inject build number into Swagger definition' + inputs: + targetType: 'inline' + script: | + $swaggerFile = "$(Build.SourcesDirectory)/SwaggerDefinitions/V1.3.0.json" + + Write-Host "Swagger file: $swaggerFile" + Write-Host "Build Number: $(Build.BuildNumber)" + + if (!(Test-Path $swaggerFile)) { + Write-Error "Swagger file not found" + exit 1 + } + + (Get-Content $swaggerFile -Raw) ` + -replace '__BUILD_NUMBER__', '$(Build.BuildNumber)' | + Set-Content $swaggerFile - task: NuGetToolInstaller@1 displayName: Use NuGet 5.8 inputs: From 42c78d4021cad567985dda8cbf2b91737bdf27d0 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Wed, 7 Jan 2026 11:31:54 +0000 Subject: [PATCH 058/106] Text casing and Email Template variable update --- .../Reports/CreateReportDateSelection.cshtml | 2 +- .../Views/Reports/Index.cshtml | 3 ++- .../Helpers/TextCasingHelper.cs | 24 +++++++++++++++++++ .../Services/DatabricksService.cs | 6 +++-- .../LearningHub.Nhs.OpenApi/appsettings.json | 2 +- .../InPlatform_Report_Predeployment.sql | 4 ++-- 6 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/TextCasingHelper.cs diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml index 62c733db2..04c840a73 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/CreateReportDateSelection.cshtml @@ -56,7 +56,7 @@ For the last:
  • - @if (errorHasOccurred) + @if (errorHasOccurred && string.IsNullOrWhiteSpace(Model.TimePeriod)) {
    diff --git a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml index 1a5a95122..f4b44d8c5 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml @@ -84,8 +84,9 @@ } else { - matchedCourseNames = $"{matched[0]} and {matched.Count - 1} others"; + matchedCourseNames = $"{matched[0].ToLower()} and {matched.Count - 1} other{((matched.Count - 1) > 1 ? "s" : "")}"; } + matchedCourseNames = UtilityHelper.ConvertToSentenceCase(matchedCourseNames); } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/TextCasingHelper.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/TextCasingHelper.cs new file mode 100644 index 000000000..415211a32 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/TextCasingHelper.cs @@ -0,0 +1,24 @@ +namespace LearningHub.Nhs.OpenApi.Services.Helpers +{ + /// + /// TextCasingHelper. + /// + public class TextCasingHelper + { + /// + /// Returns sentence case of input string. + /// + /// input. + /// A sentence case string corresponding to the input string. + public static string ConvertToSentenceCase(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + input = input.ToLower(); + return char.ToUpper(input[0]) + input.Substring(1); + } + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs index 922c739ed..c56bca122 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs @@ -21,9 +21,9 @@ using System.Text.Json; using LearningHub.Nhs.Models.Entities; using LearningHub.Nhs.OpenApi.Services.Interface.Services.Messaging; -using elfhHub.Nhs.Models.Entities; using LearningHub.Nhs.Models.Email.Models; using LearningHub.Nhs.Models.Email; +using LearningHub.Nhs.OpenApi.Services.Helpers; namespace LearningHub.Nhs.OpenApi.Services.Services { @@ -415,10 +415,12 @@ public async Task UpdateDatabricksReport(int userId, DatabricksUpdateRequest dat } else { - firstCourse = $"{matched[0].ToLower()} and {matched.Count - 1} others"; + firstCourse = $"{matched[0].ToLower()} and {matched.Count - 1} other{((matched.Count - 1) > 1 ? "s" : "")}"; + } } + firstCourse = TextCasingHelper.ConvertToSentenceCase(firstCourse); try { diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index 155a27dd4..acab953d8 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -96,7 +96,7 @@ "ResourceReadonlyAccess": "

    You can continue to search for and access resources in the Learning Hub, however you cannot contribute to it.

    If you have any questions about this, please contact the support team.

    ", "ResourceContributeAccess": "

    You can now contribute to the Learning Hub and continue to search for and access resources.

    If you have any questions about this, please contact the support team.

    ", "ReportTitle": "[ReportName] report for [ReportContent] is ready", - "Report": "

    Your report [ReportName] report for [ReportContent] is ready. You can view and download the report in the reports section

    " + "Report": "

    Your [ReportName] report for [ReportContent] is ready. You can view and download the report in the reports section

    " }, "MyContributionsUrl": "/my-contributions", "MyLearningUrl": "/MyLearning", diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Scripts/InPlatform_Report_Predeployment.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Scripts/InPlatform_Report_Predeployment.sql index 4cfef993c..08469490e 100644 --- a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Scripts/InPlatform_Report_Predeployment.sql +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Scripts/InPlatform_Report_Predeployment.sql @@ -3,6 +3,6 @@ GO -INSERT [messaging].[EmailTemplate] ([Id], [LayoutId], [Title], [Subject], [Body], [AvailableTags], [Deleted], [CreateUserId], [CreateDate], [AmendUserId], [AmendDate]) VALUES (2007, 1, N'ReportProcessed', N'[ReportName] report for [ReportContent] is ready', N'

    Dear [AdminFirstName],

    -

    Your report [ReportName] report for [ReportContent] is ready. You can view and download the report in the reports section

    ', N'[UserFirstName][ReportSection][ReportName][ReportContent]', 0, 4, CAST(N'2025-12-22T00:00:00.0000000+00:00' AS DateTimeOffset), 4, CAST(N'2025-12-22T00:00:00.0000000+00:00' AS DateTimeOffset)) +INSERT [messaging].[EmailTemplate] ([Id], [LayoutId], [Title], [Subject], [Body], [AvailableTags], [Deleted], [CreateUserId], [CreateDate], [AmendUserId], [AmendDate]) VALUES (2007, 1, N'ReportProcessed', N'[ReportName] report for [ReportContent] is ready', N'

    Dear [UserFirstName],

    +

    Your [ReportName] report for [ReportContent] is ready. You can view and download the report in the reports section

    ', N'[UserFirstName][ReportSection][ReportName][ReportContent]', 0, 4, CAST(N'2025-12-22T00:00:00.0000000+00:00' AS DateTimeOffset), 4, CAST(N'2025-12-22T00:00:00.0000000+00:00' AS DateTimeOffset)) GO \ No newline at end of file From 01378e4bda93e3bf85d2064442ac97a0ecf13511 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Wed, 7 Jan 2026 11:35:55 +0000 Subject: [PATCH 059/106] . --- LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml index f4b44d8c5..5f03ecbe6 100644 --- a/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Reports/Index.cshtml @@ -84,7 +84,7 @@ } else { - matchedCourseNames = $"{matched[0].ToLower()} and {matched.Count - 1} other{((matched.Count - 1) > 1 ? "s" : "")}"; + matchedCourseNames = $"{matched[0]} and {matched.Count - 1} other{((matched.Count - 1) > 1 ? "s" : "")}"; } matchedCourseNames = UtilityHelper.ConvertToSentenceCase(matchedCourseNames); } From fb05018631f9ffae3f42be86113925e3a1cef566 Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Wed, 7 Jan 2026 12:23:04 +0000 Subject: [PATCH 060/106] Remove build number injection from Swagger definition Removed PowerShell task for injecting build number into Swagger JSON. --- .../azure-pipeline-openapi-reportapi-ci.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/azure-pipeline-openapi-reportapi-ci.yml b/.github/azure-pipeline-openapi-reportapi-ci.yml index 4b39bddf8..1b10679e6 100644 --- a/.github/azure-pipeline-openapi-reportapi-ci.yml +++ b/.github/azure-pipeline-openapi-reportapi-ci.yml @@ -25,25 +25,6 @@ jobs: - checkout: self clean: true fetchTags: false - # Inject Build Number into Swagger JSON - - task: PowerShell@2 - displayName: 'Inject build number into Swagger definition' - inputs: - targetType: 'inline' - script: | - $swaggerFile = "$(Build.SourcesDirectory)/SwaggerDefinitions/V1.3.0.json" - - Write-Host "Swagger file: $swaggerFile" - Write-Host "Build Number: $(Build.BuildNumber)" - - if (!(Test-Path $swaggerFile)) { - Write-Error "Swagger file not found" - exit 1 - } - - (Get-Content $swaggerFile -Raw) ` - -replace '__BUILD_NUMBER__', '$(Build.BuildNumber)' | - Set-Content $swaggerFile - task: NuGetToolInstaller@1 displayName: Use NuGet 5.8 inputs: From aafb214da072d604eaf2f27909c9af3b4ea0cd08 Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Wed, 7 Jan 2026 13:00:08 +0000 Subject: [PATCH 061/106] TD-6752: Include Build Number in OpenAPI Swagger JSON --- OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs | 14 ++++++++------ .../SwaggerDefinitions/v1.3.0.json | 2 +- OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json | 5 +++++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs index 68981d051..4b4f79d03 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs @@ -93,11 +93,16 @@ public void ConfigureServices(IServiceCollection services) services.AddMvc() .AddNewtonsoftJson(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore); + var swaggerTitle = this.Configuration["Swagger:Title"]; + var swaggerVersion = this.Configuration["Swagger:Version"]; + var swaggerDescription = $"A set of API endpoints for retrieving learning resource information from the Learning Hub learning platform. The [Learning Hub](https://learninghub.nhs.uk/) is a platform for hosting and sharing learning resources for health and social care provided by Technology Enhanced Learning (TEL) at NHS England. An application API key must be used to authorise calls to the API from external applications. To contact TEL to discuss connecting your external system to the Learning Hub, email england.tel@nhs.net.\n\n Build Number: {this.Configuration["Swagger:BuildNumber"]} \n\n"; + services.AddSwaggerGen( c => { // For docs see https://github.com/domaindrivendev/Swashbuckle.AspNetCore - c.SwaggerDoc("dev", new OpenApiInfo { Title = "LearningHub.NHS.OpenAPI", Version = "dev" }); + c.SwaggerDoc("dev", new OpenApiInfo { Title = swaggerTitle, Version = swaggerVersion, Description = swaggerDescription }); + c.CustomSchemaIds(type => type.FullName); c.AddSecurityDefinition( "ApiKey", @@ -220,12 +225,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseSwagger(); app.UseSwaggerUI(c => { - if (env.IsDevelopment()) - { - c.SwaggerEndpoint("/swagger/dev/swagger.json", "Auto-generated"); - } + c.SwaggerEndpoint("/swagger/dev/swagger.json", "v1.4.0"); - c.SwaggerEndpoint("/SwaggerDefinitions/v1.3.0.json", "v1.3.0"); + ////c.SwaggerEndpoint("/SwaggerDefinitions/v1.3.0.json", "v1.3.0"); c.OAuthClientId(this.Configuration.GetValue("LearningHubAuthServiceConfig:ClientId")); c.OAuthClientSecret(this.Configuration.GetValue("LearningHubAuthServiceConfig:ClientSecret")); c.OAuthScopes(this.Configuration.GetValue("LearningHubAuthServiceConfig:Scopes")); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json b/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json index d0fa21e83..1fd6fa2d1 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/SwaggerDefinitions/v1.3.0.json @@ -3,7 +3,7 @@ "info": { "title": "LearningHub.NHS.OpenAPI", "version": "1.3.0", - "description": "A set of API endpoints for retrieving learning resource information from the Learning Hub learning platform. The [Learning Hub](https://learninghub.nhs.uk/) is a platform for hosting and sharing learning resources for health and social care provided by Technology Enhanced Learning (TEL) at NHS England. An application API key must be used to authorise calls to the API from external applications. To contact TEL to discuss connecting your external system to the Learning Hub, email england.tel@nhs.net. \n\n**Build Number:** __BUILD_NUMBER__\n\n" + "description": "A set of API endpoints for retrieving learning resource information from the Learning Hub learning platform. The [Learning Hub](https://learninghub.nhs.uk/) is a platform for hosting and sharing learning resources for health and social care provided by Technology Enhanced Learning (TEL) at NHS England. An application API key must be used to authorise calls to the API from external applications. To contact TEL to discuss connecting your external system to the Learning Hub, email england.tel@nhs.net." }, "paths": { "/Bookmark/GetAllByParent": { diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index 155a27dd4..dfaaafa6b 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -7,6 +7,11 @@ "Microsoft.Hosting.Lifetime": "Information" } }, + "Swagger": { + "Title": "Swagger XML DOC LearningHub.NHS.OpenAPI", + "Version": "v4.1.0", + "BuildNumber": "" + }, "AllowedHosts": "*", "ConnectionStrings": { "ElfhHubDbConnection": "", From 78106f06b313581d2f8da8d6aaffec54ca9308b0 Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Wed, 7 Jan 2026 13:04:08 +0000 Subject: [PATCH 062/106] Removed the commented line --- OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs index 4b4f79d03..b2948c8d6 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Startup.cs @@ -226,8 +226,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/dev/swagger.json", "v1.4.0"); - - ////c.SwaggerEndpoint("/SwaggerDefinitions/v1.3.0.json", "v1.3.0"); c.OAuthClientId(this.Configuration.GetValue("LearningHubAuthServiceConfig:ClientId")); c.OAuthClientSecret(this.Configuration.GetValue("LearningHubAuthServiceConfig:ClientSecret")); c.OAuthScopes(this.Configuration.GetValue("LearningHubAuthServiceConfig:Scopes")); From e580cb231dec14981bd68474392a313be277fee8 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Wed, 7 Jan 2026 16:04:03 +0000 Subject: [PATCH 063/106] bugfix for existing report update --- LearningHub.Nhs.WebUI/Controllers/ReportsController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs index 12ad10acd..fdbaffc13 100644 --- a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs @@ -171,8 +171,7 @@ public async Task CreateReportCourseSelection(ReportCreationCours } this.ModelState.AddModelError("Courses", CommonValidationErrorMessages.CourseRequired); - reportCreation.Courses = null; - await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + courseSelection.BuildCourses(await this.GetCoursesAsync()); courseSelection.Courses = reportCreation.Courses; this.ViewBag.ReturnUrl = returnUrl; From 2b071099d3fed4867f0cd03a1d161a618da920fd Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Fri, 9 Jan 2026 11:08:54 +0000 Subject: [PATCH 064/106] Report Downloa issue fix --- LearningHub.Nhs.WebUI/Controllers/ReportsController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs index fdbaffc13..9cfc747b4 100644 --- a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs @@ -400,6 +400,7 @@ public async Task CourseProgressReport(CourseCompletionViewModel response.TotalCount = result.TotalCount; response.CourseCompletionRecords = result.CourseCompletionRecords; response.ReportHistoryModel = await this.reportService.GetReportHistoryById(result.ReportHistoryId); + response.ReportHistoryId = result.ReportHistoryId; reportCreation.ReportHistoryId = result.ReportHistoryId; } From 23f91b20c5fbf14c49ee207c5a2284ecd9fbfec7 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Fri, 9 Jan 2026 13:02:48 +0000 Subject: [PATCH 065/106] Null check to rediect user that are yet to populate form wizard --- LearningHub.Nhs.WebUI/Controllers/ReportsController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs index 9cfc747b4..c48b4db66 100644 --- a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs @@ -348,6 +348,11 @@ public async Task CourseProgressReport(CourseCompletionViewModel // validate date var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); + if (reportCreation.Courses == null) + { + return this.RedirectToAction("Index"); + } + switch (courseCompletion.ReportFormActionType) { case ReportFormActionTypeEnum.NextPageChange: From 177e6b77c1d49fbe30972b24b1ee3ce7d0df887f Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Tue, 13 Jan 2026 14:10:48 +0000 Subject: [PATCH 066/106] Bugfix for error when alternating between predefined date and custom date range --- LearningHub.Nhs.WebUI/Controllers/ReportsController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs index c48b4db66..4cda20cb7 100644 --- a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs @@ -247,8 +247,8 @@ public async Task CreateReportSummary(ReportCreationDateSelection return this.View("CreateReportDateSelection", reportCreationDate); } - reportCreation.StartDate = reportCreationDate.GetStartDate(); - reportCreation.EndDate = reportCreationDate.GetEndDate(); + reportCreation.StartDate = reportCreation.TimePeriod == "Custom" ? reportCreationDate.GetStartDate() : null; + reportCreation.EndDate = reportCreation.TimePeriod == "Custom" ? reportCreationDate.GetEndDate() : null; await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData); return this.RedirectToAction("CourseProgressReport"); } From a787b5572729c43e77d1db847614bc2fa157949a Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Tue, 13 Jan 2026 14:21:02 +0000 Subject: [PATCH 067/106] Td-6760: Duplicate record in synced elfh tables in the LH database --- .../Adf/AdfMergeUserAdminLocation.sql | 60 ++++++++----------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql index 5128603f7..d3dc03fc8 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql @@ -13,50 +13,38 @@ AS BEGIN SET NOCOUNT ON; - -- Deduplicate source first - ;WITH DedupedSource AS ( - SELECT *, - ROW_NUMBER() OVER ( - PARTITION BY userId, adminLocationId, deleted - ORDER BY amendDate DESC, createdDate DESC - ) AS rn - FROM @UserAdminLocationList - ), - CleanSource AS ( - SELECT * FROM DedupedSource WHERE rn = 1 - ) - - MERGE elfh.userAdminLocationTBL AS target - USING CleanSource AS source - ON target.userId = source.userId - AND target.adminLocationId = source.adminLocationId - AND target.deleted = source.deleted -- IMPORTANT! + MERGE [elfh].[userAdminLocationTBL] AS target + USING @UserAdminLocationList AS source + ON target.[userId] = source.[userId] + AND target.[adminLocationId] = source.[adminLocationId] -- composite key match WHEN MATCHED THEN UPDATE SET - target.amendUserId = source.amendUserId, - target.amendDate = source.amendDate, - target.createdUserId = source.createdUserId, - target.createdDate = source.createdDate + target.[deleted] = source.[deleted], + target.[amendUserId] = source.[amendUserId], + target.[amendDate] = source.[amendDate], + target.[createdUserId] = source.[createdUserId], + target.[createdDate] = source.[createdDate] WHEN NOT MATCHED BY TARGET THEN INSERT ( - userId, - adminLocationId, - deleted, - amendUserId, - amendDate, - createdUserId, - createdDate + [userId], + [adminLocationId], + [deleted], + [amendUserId], + [amendDate], + [createdUserId], + [createdDate] ) VALUES ( - source.userId, - source.adminLocationId, - source.deleted, - source.amendUserId, - source.amendDate, - source.createdUserId, - source.createdDate + source.[userId], + source.[adminLocationId], + source.[deleted], + source.[amendUserId], + source.[amendDate], + source.[createdUserId], + source.[createdDate] ); + END GO From 0d595d247e3ae723cd6a90ac26ab03fb9a1359e3 Mon Sep 17 00:00:00 2001 From: AnjuJose011 <154979799+AnjuJose011@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:45:05 +0000 Subject: [PATCH 068/106] Revert "Td-6760: Duplicate record in synced elfh tables in the LH database" --- .../Adf/AdfMergeUserAdminLocation.sql | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql index d3dc03fc8..5128603f7 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql @@ -13,38 +13,50 @@ AS BEGIN SET NOCOUNT ON; - MERGE [elfh].[userAdminLocationTBL] AS target - USING @UserAdminLocationList AS source - ON target.[userId] = source.[userId] - AND target.[adminLocationId] = source.[adminLocationId] -- composite key match + -- Deduplicate source first + ;WITH DedupedSource AS ( + SELECT *, + ROW_NUMBER() OVER ( + PARTITION BY userId, adminLocationId, deleted + ORDER BY amendDate DESC, createdDate DESC + ) AS rn + FROM @UserAdminLocationList + ), + CleanSource AS ( + SELECT * FROM DedupedSource WHERE rn = 1 + ) + + MERGE elfh.userAdminLocationTBL AS target + USING CleanSource AS source + ON target.userId = source.userId + AND target.adminLocationId = source.adminLocationId + AND target.deleted = source.deleted -- IMPORTANT! WHEN MATCHED THEN UPDATE SET - target.[deleted] = source.[deleted], - target.[amendUserId] = source.[amendUserId], - target.[amendDate] = source.[amendDate], - target.[createdUserId] = source.[createdUserId], - target.[createdDate] = source.[createdDate] + target.amendUserId = source.amendUserId, + target.amendDate = source.amendDate, + target.createdUserId = source.createdUserId, + target.createdDate = source.createdDate WHEN NOT MATCHED BY TARGET THEN INSERT ( - [userId], - [adminLocationId], - [deleted], - [amendUserId], - [amendDate], - [createdUserId], - [createdDate] + userId, + adminLocationId, + deleted, + amendUserId, + amendDate, + createdUserId, + createdDate ) VALUES ( - source.[userId], - source.[adminLocationId], - source.[deleted], - source.[amendUserId], - source.[amendDate], - source.[createdUserId], - source.[createdDate] + source.userId, + source.adminLocationId, + source.deleted, + source.amendUserId, + source.amendDate, + source.createdUserId, + source.createdDate ); - END GO From 8e6a6deb0bfcf649964c1e81b58073eedb113181 Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Wed, 14 Jan 2026 14:28:24 +0000 Subject: [PATCH 069/106] TD-6760: Duplicate record in synced elfh tables in the LH database --- .../Adf/AdfMergeUserAdminLocation.sql | 97 ++++++++++++------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql index d3dc03fc8..97d446338 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Adf/AdfMergeUserAdminLocation.sql @@ -6,6 +6,7 @@ -- Modification History -- -- 04-11-2025 Sarathlal Initial Revision +-- 14-01-2026 Swapna TD-6760: To handle duplicate records in the Sync table ------------------------------------------------------------------------------- CREATE PROCEDURE [dbo].[AdfMergeUserAdminLocation] @UserAdminLocationList dbo.UserAdminLocationType READONLY @@ -13,38 +14,68 @@ AS BEGIN SET NOCOUNT ON; - MERGE [elfh].[userAdminLocationTBL] AS target - USING @UserAdminLocationList AS source - ON target.[userId] = source.[userId] - AND target.[adminLocationId] = source.[adminLocationId] -- composite key match + -------------------------------------------------------------------- + -- 1. Remove deleted duplicates when an active row exists + -------------------------------------------------------------------- + DELETE tgt + FROM elfh.userAdminLocationTBL tgt + INNER JOIN @UserAdminLocationList src + ON tgt.userId = src.userId + AND tgt.adminLocationId = src.adminLocationId + WHERE tgt.deleted = 1 + AND EXISTS ( + SELECT 1 + FROM elfh.userAdminLocationTBL a + WHERE a.userId = src.userId + AND a.adminLocationId = src.adminLocationId + AND a.deleted = 0 + ); - WHEN MATCHED THEN - UPDATE SET - target.[deleted] = source.[deleted], - target.[amendUserId] = source.[amendUserId], - target.[amendDate] = source.[amendDate], - target.[createdUserId] = source.[createdUserId], - target.[createdDate] = source.[createdDate] + -------------------------------------------------------------------- + -- 2. Update existing ACTIVE rows only + -- (do NOT overwrite deleted rows) + -------------------------------------------------------------------- + UPDATE tgt + SET + tgt.amendUserId = src.amendUserId, + tgt.amendDate = src.amendDate, + tgt.createdUserId = src.createdUserId, + tgt.createdDate = src.createdDate, + tgt.deleted = src.deleted + FROM elfh.userAdminLocationTBL tgt + INNER JOIN @UserAdminLocationList src + ON tgt.userId = src.userId + AND tgt.adminLocationId = src.adminLocationId + WHERE tgt.deleted = 0; - WHEN NOT MATCHED BY TARGET THEN - INSERT ( - [userId], - [adminLocationId], - [deleted], - [amendUserId], - [amendDate], - [createdUserId], - [createdDate] - ) - VALUES ( - source.[userId], - source.[adminLocationId], - source.[deleted], - source.[amendUserId], - source.[amendDate], - source.[createdUserId], - source.[createdDate] - ); - -END -GO + -------------------------------------------------------------------- + -- 3. Insert rows that do not exist + -- (both deleted = 0 and deleted = 1 allowed) + -------------------------------------------------------------------- + INSERT INTO elfh.userAdminLocationTBL ( + userId, + adminLocationId, + deleted, + amendUserId, + amendDate, + createdUserId, + createdDate + ) + SELECT + src.userId, + src.adminLocationId, + src.deleted, + src.amendUserId, + src.amendDate, + src.createdUserId, + src.createdDate + FROM @UserAdminLocationList src + WHERE NOT EXISTS ( + SELECT 1 + FROM elfh.userAdminLocationTBL tgt WITH (UPDLOCK, HOLDLOCK) + WHERE tgt.userId = src.userId + AND tgt.adminLocationId = src.adminLocationId + AND tgt.deleted = src.deleted + ); +END; +GO \ No newline at end of file From 0246cd69de3ea449e53ad8c8f401f0f3c58dc3f5 Mon Sep 17 00:00:00 2001 From: AnjuJose011 <154979799+AnjuJose011@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:31:58 +0000 Subject: [PATCH 070/106] changes --- .../Pre-Deploy/Script.PreDeployment.sql | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql index 52e090048..cb86dc519 100644 --- a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql @@ -18,3 +18,45 @@ BEGIN RAISERROR (N'TD-2902 Add resource types to Content Server.sql must be run manually before release.', 16, 127) WITH NOWAIT END GO +-- ========================================= +-- Pre-deployment: Disable Change Tracking for all tables +-- ========================================= + +DECLARE @schemaName NVARCHAR(128), @tableName NVARCHAR(128); +DECLARE ct_cursor CURSOR FOR +SELECT s.name AS schema_name, t.name AS table_name +FROM sys.change_tracking_tables ct +JOIN sys.tables t ON ct.object_id = t.object_id +JOIN sys.schemas s ON t.schema_id = s.schema_id; + +OPEN ct_cursor; +FETCH NEXT FROM ct_cursor INTO @schemaName, @tableName; + +WHILE @@FETCH_STATUS = 0 +BEGIN + PRINT 'Disabling Change Tracking for table: ' + @schemaName + '.' + @tableName; + EXEC('ALTER TABLE [' + @schemaName + '].[' + @tableName + '] DISABLE CHANGE_TRACKING;'); + + FETCH NEXT FROM ct_cursor INTO @schemaName, @tableName; +END + +CLOSE ct_cursor; +DEALLOCATE ct_cursor; + +PRINT 'Change Tracking has been disabled for all tables.'; + + +-- 2️⃣ Disable Change Tracking if enabled +IF EXISTS (SELECT * FROM sys.change_tracking_databases WHERE database_id = DB_ID()) +BEGIN + PRINT 'Change Tracking is enabled. Disabling Change Tracking for the database...'; + ALTER DATABASE CURRENT + SET CHANGE_TRACKING = OFF; + PRINT 'Change Tracking has been disabled.'; +END +ELSE +BEGIN + PRINT 'Change Tracking is not enabled on this database.'; +END + +PRINT 'Change Tracking disable completed.'; From 381056c2520ee7b0052ded9c8b4b967a1394f7e9 Mon Sep 17 00:00:00 2001 From: AnjuJose011 <154979799+AnjuJose011@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:41:06 +0000 Subject: [PATCH 071/106] Update ActivityController.cs --- WebAPI/LearningHub.Nhs.API/Controllers/ActivityController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebAPI/LearningHub.Nhs.API/Controllers/ActivityController.cs b/WebAPI/LearningHub.Nhs.API/Controllers/ActivityController.cs index 49a9d01eb..d42757b5d 100644 --- a/WebAPI/LearningHub.Nhs.API/Controllers/ActivityController.cs +++ b/WebAPI/LearningHub.Nhs.API/Controllers/ActivityController.cs @@ -29,7 +29,7 @@ public class ActivityController : ApiControllerBase private readonly IResourceService resourceService; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The elfh user service. /// The activity service. From c99b0e581e3562f5798faf701a6e528110e09c70 Mon Sep 17 00:00:00 2001 From: AnjuJose011 <154979799+AnjuJose011@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:53:46 +0000 Subject: [PATCH 072/106] Update Script.PreDeployment.sql --- .../Pre-Deploy/Script.PreDeployment.sql | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql index cb86dc519..783a888f5 100644 --- a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql @@ -18,13 +18,17 @@ BEGIN RAISERROR (N'TD-2902 Add resource types to Content Server.sql must be run manually before release.', 16, 127) WITH NOWAIT END GO +PRINT '*** PRE-DEPLOYMENT: Disable Change Tracking STARTED ***'; +GO + -- ========================================= --- Pre-deployment: Disable Change Tracking for all tables +-- Disable Change Tracking on all tables -- ========================================= +DECLARE @schemaName SYSNAME, @tableName SYSNAME; +DECLARE @sql NVARCHAR(MAX); -DECLARE @schemaName NVARCHAR(128), @tableName NVARCHAR(128); -DECLARE ct_cursor CURSOR FOR -SELECT s.name AS schema_name, t.name AS table_name +DECLARE ct_cursor CURSOR FAST_FORWARD FOR +SELECT s.name, t.name FROM sys.change_tracking_tables ct JOIN sys.tables t ON ct.object_id = t.object_id JOIN sys.schemas s ON t.schema_id = s.schema_id; @@ -34,29 +38,30 @@ FETCH NEXT FROM ct_cursor INTO @schemaName, @tableName; WHILE @@FETCH_STATUS = 0 BEGIN + SET @sql = N'ALTER TABLE [' + @schemaName + '].[' + @tableName + '] DISABLE CHANGE_TRACKING;'; PRINT 'Disabling Change Tracking for table: ' + @schemaName + '.' + @tableName; - EXEC('ALTER TABLE [' + @schemaName + '].[' + @tableName + '] DISABLE CHANGE_TRACKING;'); + EXEC (@sql); FETCH NEXT FROM ct_cursor INTO @schemaName, @tableName; END CLOSE ct_cursor; DEALLOCATE ct_cursor; +GO -PRINT 'Change Tracking has been disabled for all tables.'; - +PRINT 'Table-level Change Tracking disabled.'; +GO --- 2️⃣ Disable Change Tracking if enabled -IF EXISTS (SELECT * FROM sys.change_tracking_databases WHERE database_id = DB_ID()) -BEGIN - PRINT 'Change Tracking is enabled. Disabling Change Tracking for the database...'; - ALTER DATABASE CURRENT - SET CHANGE_TRACKING = OFF; - PRINT 'Change Tracking has been disabled.'; -END -ELSE +-- ========================================= +-- Disable Change Tracking at database level +-- MUST be in its own batch +-- ========================================= +IF EXISTS (SELECT 1 FROM sys.change_tracking_databases WHERE database_id = DB_ID()) BEGIN - PRINT 'Change Tracking is not enabled on this database.'; + PRINT 'Disabling Change Tracking at database level...'; + ALTER DATABASE CURRENT SET CHANGE_TRACKING = OFF; END +GO -PRINT 'Change Tracking disable completed.'; +PRINT '*** PRE-DEPLOYMENT: Disable Change Tracking COMPLETED ***'; +GO From 8af36fed2a3601e80234d0ef02b9634bba189e81 Mon Sep 17 00:00:00 2001 From: AnjuJose011 <154979799+AnjuJose011@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:21:27 +0000 Subject: [PATCH 073/106] Update Script.PreDeployment.sql --- .../Pre-Deploy/Script.PreDeployment.sql | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql index 783a888f5..067f95493 100644 --- a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql @@ -14,54 +14,3 @@ IF (NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'resources' AND TABLE_NAME = 'ResourceReferenceEvent')) -BEGIN - RAISERROR (N'TD-2902 Add resource types to Content Server.sql must be run manually before release.', 16, 127) WITH NOWAIT -END -GO -PRINT '*** PRE-DEPLOYMENT: Disable Change Tracking STARTED ***'; -GO - --- ========================================= --- Disable Change Tracking on all tables --- ========================================= -DECLARE @schemaName SYSNAME, @tableName SYSNAME; -DECLARE @sql NVARCHAR(MAX); - -DECLARE ct_cursor CURSOR FAST_FORWARD FOR -SELECT s.name, t.name -FROM sys.change_tracking_tables ct -JOIN sys.tables t ON ct.object_id = t.object_id -JOIN sys.schemas s ON t.schema_id = s.schema_id; - -OPEN ct_cursor; -FETCH NEXT FROM ct_cursor INTO @schemaName, @tableName; - -WHILE @@FETCH_STATUS = 0 -BEGIN - SET @sql = N'ALTER TABLE [' + @schemaName + '].[' + @tableName + '] DISABLE CHANGE_TRACKING;'; - PRINT 'Disabling Change Tracking for table: ' + @schemaName + '.' + @tableName; - EXEC (@sql); - - FETCH NEXT FROM ct_cursor INTO @schemaName, @tableName; -END - -CLOSE ct_cursor; -DEALLOCATE ct_cursor; -GO - -PRINT 'Table-level Change Tracking disabled.'; -GO - --- ========================================= --- Disable Change Tracking at database level --- MUST be in its own batch --- ========================================= -IF EXISTS (SELECT 1 FROM sys.change_tracking_databases WHERE database_id = DB_ID()) -BEGIN - PRINT 'Disabling Change Tracking at database level...'; - ALTER DATABASE CURRENT SET CHANGE_TRACKING = OFF; -END -GO - -PRINT '*** PRE-DEPLOYMENT: Disable Change Tracking COMPLETED ***'; -GO From 49351247bdef2ed5e81d78e667bc4aac933de2fe Mon Sep 17 00:00:00 2001 From: AnjuJose011 <154979799+AnjuJose011@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:21:30 +0000 Subject: [PATCH 074/106] Update Script.PreDeployment.sql --- .../Scripts/Pre-Deploy/Script.PreDeployment.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql b/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql index 067f95493..52e090048 100644 --- a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql +++ b/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Script.PreDeployment.sql @@ -14,3 +14,7 @@ IF (NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'resources' AND TABLE_NAME = 'ResourceReferenceEvent')) +BEGIN + RAISERROR (N'TD-2902 Add resource types to Content Server.sql must be run manually before release.', 16, 127) WITH NOWAIT +END +GO From f4bfb05543b817ba49c07f4d85d73ecb154ec33f Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Thu, 5 Feb 2026 16:47:59 +0000 Subject: [PATCH 075/106] TD-6860: My Learning history page issue --- .../Activity/GetUserInProgressLearningActivities.sql | 3 ++- .../Activity/GetUserRecentLearningActivities.sql | 3 ++- .../Stored Procedures/Activity/GetUsersLearningHistory.sql | 3 ++- .../Activity/GetUsersLearningHistory_Search.sql | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserInProgressLearningActivities.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserInProgressLearningActivities.sql index fdd153a7a..12357075e 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserInProgressLearningActivities.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserInProgressLearningActivities.sql @@ -5,6 +5,7 @@ -- -- Modification History -- 01-10-2025 SA added assesment score and passmark and provider details +-- 05-02-2025 SA TD-6860 : Fixed the null issue with the search history ------------------------------------------------------------------------------- CREATE PROCEDURE [activity].[GetUserInProgressLearningActivities] ( @userId INT @@ -23,7 +24,7 @@ BEGIN ( SELECT TOP 1 rr.OriginalResourceReferenceId FROM [resources].[ResourceReference] rr - WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0 + WHERE rr.ResourceId = rv.ResourceId --AND rr.Deleted = 0 ) AS ResourceReferenceID, ra.MajorVersion AS MajorVersion, ra.MinorVersion AS MinorVersion, diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserRecentLearningActivities.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserRecentLearningActivities.sql index 6e5160787..2626862f2 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserRecentLearningActivities.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUserRecentLearningActivities.sql @@ -7,6 +7,7 @@ -- 02-Sep-2025 SA Incorrect Syntax -- 23-09-2025 SA Added new columns for displaying video/Audio Progress -- 01-10-2025 SA added assesment score and passmark and provider details +-- 05-02-2025 SA TD-6860 : Fixed the null issue with the search history ------------------------------------------------------------------------------- CREATE PROCEDURE [activity].[GetUserRecentLearningActivities] ( @userId INT, @@ -26,7 +27,7 @@ BEGIN ( SELECT TOP 1 rr.OriginalResourceReferenceId FROM [resources].[ResourceReference] rr - WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0 + WHERE rr.ResourceId = rv.ResourceId --AND rr.Deleted = 0 ) AS ResourceReferenceID, ra.MajorVersion AS MajorVersion, ra.MinorVersion AS MinorVersion, diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory.sql index 81669c476..c60018b71 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory.sql @@ -6,6 +6,7 @@ -- Modification History -- 23-09-2025 SA Added new columns for displaying video/Audio Progress -- 01-10-2025 SA added assesment score and passmark and provider details +-- 05-02-2025 SA TD-6860 : Fixed the null issue with the search history ------------------------------------------------------------------------------- CREATE PROCEDURE [activity].[GetUsersLearningHistory] ( @userId INT, @@ -24,7 +25,7 @@ BEGIN ( SELECT TOP 1 rr.OriginalResourceReferenceId FROM [resources].[ResourceReference] rr - WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0 + WHERE rr.ResourceId = rv.ResourceId --AND rr.Deleted = 0 ) AS ResourceReferenceId, ra.MajorVersion AS MajorVersion, ra.MinorVersion AS MinorVersion, diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory_Search.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory_Search.sql index ba4d565dd..80c9f79d3 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory_Search.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetUsersLearningHistory_Search.sql @@ -6,6 +6,7 @@ -- Modification History -- 23-09-2025 SA Added new columns for displaying video/Audio Progress -- 01-10-2025 SA added assesment score and passmark and provider details +-- 05-02-2025 SA TD-6860 : Fixed the null issue with the search history ------------------------------------------------------------------------------- CREATE PROCEDURE [activity].[GetUsersLearningHistory_Search] ( @userId INT, @@ -27,7 +28,7 @@ BEGIN ( SELECT TOP 1 rr.OriginalResourceReferenceId FROM [resources].[ResourceReference] rr - WHERE rr.ResourceId = rv.ResourceId AND rr.Deleted = 0 + WHERE rr.ResourceId = rv.ResourceId --AND rr.Deleted = 0 ) AS ResourceReferenceId, ra.MajorVersion AS MajorVersion, ra.MinorVersion AS MinorVersion, From 210a90dd1a7a62f34d30e6dfc5933599905ce133 Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 6 Feb 2026 15:46:57 +0000 Subject: [PATCH 076/106] TD-6865, removed the dupliate DI and moved the view --- OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs | 1 - .../LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj | 2 +- .../Scripts => Views}/TD-6212.Azure Search new views. sql.sql | 0 3 files changed, 1 insertion(+), 2 deletions(-) rename WebAPI/LearningHub.Nhs.Database/{Scripts/Pre-Deploy/Scripts => Views}/TD-6212.Azure Search new views. sql.sql (100%) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs index f1c5a756c..07baa0438 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs @@ -40,7 +40,6 @@ public static void AddServices(this IServiceCollection services, IConfiguration services.AddHttpClient(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index dc3753739..79c3890de 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -210,7 +210,6 @@ - @@ -694,6 +693,7 @@ + diff --git a/WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Scripts/TD-6212.Azure Search new views. sql.sql b/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.Azure Search new views. sql.sql similarity index 100% rename from WebAPI/LearningHub.Nhs.Database/Scripts/Pre-Deploy/Scripts/TD-6212.Azure Search new views. sql.sql rename to WebAPI/LearningHub.Nhs.Database/Views/TD-6212.Azure Search new views. sql.sql From dbc53c19affa5fd083bbbdeeef737046364bf220 Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 6 Feb 2026 16:12:41 +0000 Subject: [PATCH 077/106] Split the views --- .../LearningHub.Nhs.Database.sqlproj | 4 +- ...2.1. Azure Search-SearchCataloguesView.sql | 49 +++++++ ...2.2. Azure Search-SearchResourcesView.sql} | 121 +----------------- ...6212.3.Azure Search-SupersetSearchView.sql | 66 ++++++++++ 4 files changed, 119 insertions(+), 121 deletions(-) create mode 100644 WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1. Azure Search-SearchCataloguesView.sql rename WebAPI/LearningHub.Nhs.Database/Views/{TD-6212.Azure Search new views. sql.sql => TD-6212.2. Azure Search-SearchResourcesView.sql} (50%) create mode 100644 WebAPI/LearningHub.Nhs.Database/Views/TD-6212.3.Azure Search-SupersetSearchView.sql diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index 79c3890de..fb366fac3 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -693,7 +693,9 @@ - + + + diff --git a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1. Azure Search-SearchCataloguesView.sql b/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1. Azure Search-SearchCataloguesView.sql new file mode 100644 index 000000000..d8e7c1f7a --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1. Azure Search-SearchCataloguesView.sql @@ -0,0 +1,49 @@ +CREATE VIEW [dbo].[SearchCataloguesView] AS +WITH Catalogues AS ( + SELECT + nv.NodeId AS Id, + cnv.Name, + cnv.Description, + cnv.URL, + n.Hidden, + amend.MaxAmendDate AS AmendDate, + cnv.Deleted, + cnv.Id AS CatalogueNodeVersionId + FROM hierarchy.CatalogueNodeVersion cnv + INNER JOIN hierarchy.Node n ON n.CurrentNodeVersionId = cnv.NodeVersionId + INNER JOIN hierarchy.NodeVersion nv ON nv.NodeId = n.Id + CROSS APPLY ( + SELECT MAX(v) AS MaxAmendDate + FROM (VALUES (cnv.AmendDate), (nv.AmendDate), (n.AmendDate)) AS value(v) + ) AS amend + WHERE nv.VersionStatusId = 2 + AND cnv.Deleted = 0 + AND n.Deleted = 0 + AND nv.Deleted = 0 +) +SELECT + c.Id, + c.Name, + c.Description, + c.URL, + c.Hidden, + c.CatalogueNodeVersionId, + c.AmendDate, + c.Deleted, + -- Aggregate keywords into JSON array + ( + SELECT STRING_AGG(cnvk.Keyword, ',') + FROM hierarchy.CatalogueNodeVersionKeyword cnvk + WHERE cnvk.CatalogueNodeVersionId = c.CatalogueNodeVersionId AND cnvk.Deleted = 0 + ) AS Keywords, + + -- Aggregate providers into JSON array + ( + SELECT STRING_AGG(CAST(cvp.ProviderId AS varchar(10)), ',') + FROM hierarchy.CatalogueNodeVersionProvider cvp + WHERE cvp.CatalogueNodeVersionId = c.CatalogueNodeVersionId AND cvp.Deleted = 0 + ) AS Providers + +FROM Catalogues c; +GO; + diff --git a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.Azure Search new views. sql.sql b/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.2. Azure Search-SearchResourcesView.sql similarity index 50% rename from WebAPI/LearningHub.Nhs.Database/Views/TD-6212.Azure Search new views. sql.sql rename to WebAPI/LearningHub.Nhs.Database/Views/TD-6212.2. Azure Search-SearchResourcesView.sql index 481f419fe..ec3162312 100644 --- a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.Azure Search new views. sql.sql +++ b/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.2. Azure Search-SearchResourcesView.sql @@ -1,55 +1,4 @@ -CREATE VIEW [dbo].[SearchCataloguesView] AS -WITH Catalogues AS ( - SELECT - nv.NodeId AS Id, - cnv.Name, - cnv.Description, - cnv.URL, - n.Hidden, - amend.MaxAmendDate AS AmendDate, - cnv.Deleted, - cnv.Id AS CatalogueNodeVersionId - FROM hierarchy.CatalogueNodeVersion cnv - INNER JOIN hierarchy.Node n ON n.CurrentNodeVersionId = cnv.NodeVersionId - INNER JOIN hierarchy.NodeVersion nv ON nv.NodeId = n.Id - CROSS APPLY ( - SELECT MAX(v) AS MaxAmendDate - FROM (VALUES (cnv.AmendDate), (nv.AmendDate), (n.AmendDate)) AS value(v) - ) AS amend - WHERE nv.VersionStatusId = 2 - AND cnv.Deleted = 0 - AND n.Deleted = 0 - AND nv.Deleted = 0 -) -SELECT - c.Id, - c.Name, - c.Description, - c.URL, - c.Hidden, - c.CatalogueNodeVersionId, - c.AmendDate, - c.Deleted, - -- Aggregate keywords into JSON array - ( - SELECT STRING_AGG(cnvk.Keyword, ',') - FROM hierarchy.CatalogueNodeVersionKeyword cnvk - WHERE cnvk.CatalogueNodeVersionId = c.CatalogueNodeVersionId AND cnvk.Deleted = 0 - ) AS Keywords, - - -- Aggregate providers into JSON array - ( - SELECT STRING_AGG(CAST(cvp.ProviderId AS varchar(10)), ',') - FROM hierarchy.CatalogueNodeVersionProvider cvp - WHERE cvp.CatalogueNodeVersionId = c.CatalogueNodeVersionId AND cvp.Deleted = 0 - ) AS Providers - -FROM Catalogues c; -GO; - ---------------------------------------------------------------------------------------------------------------------- - -CREATE VIEW [dbo].[SearchResourcesView] +CREATE VIEW [dbo].[SearchResourcesView] AS WITH BaseResource AS ( SELECT @@ -161,71 +110,3 @@ SELECT FROM BaseResource b WHERE b.RowNumber = 1; GO; - -------------------------------------------------------------------------------------------------------------------------------- - -CREATE VIEW [dbo].[SupersetSearchView] -AS - --- ============================ --- Catalogue Rows --- ============================ -SELECT - 'cat-'+ CAST(c.Id AS NVARCHAR(50)) AS id, - c.Name AS title, - LOWER(c.Name) AS normalised_title, - c.Description AS description, - 'catalogue' AS resource_collection, - c.Keywords as manual_tag, -- e.g., comma-separated or JSON if multiple - 'catalogue' AS resource_type, - NULL AS publication_date, - NULL AS date_authored, - NULL AS rating, - NULL AS catalogue_id, - NULL AS resource_reference_id, - NULL AS location_paths, - CAST(0 AS bit) AS statutory_mandatory, - c.Providers AS provider_ids, -- e.g., comma-separated or JSON if multiple - NULL AS author, - CAST(HIdden AS bit) AS hidden, - URL AS url, - --c.AmendDate AS last_modified, - SWITCHOFFSET(CAST(c.AmendDate AS datetimeoffset), '+00:00') AS last_modified, - CAST(HIdden AS bit) is_deleted -FROM dbo.SearchCataloguesView c - -UNION ALL - --- ============================ --- Resource Rows --- ============================ -SELECT - 'res-'+ CAST(r.ResourceId AS NVARCHAR(50)) AS id, - r.title, - LOWER(r.title) AS normalised_title, - r.Description AS description, - 'resource' AS resource_collection, - r.Keywords AS manual_tag, - r.ContentType AS resource_type, - r.PublicationDate AS publication_date, - r.AuthoredDate AS date_authored, - CAST(r.AverageRating AS FLOAT) AS rating, - JSON_VALUE(r.Catalogues, '$[0].CatalogueNodeId') AS catalogue_id, - JSON_VALUE(r.Catalogues, '$[0].OriginalResourceReferenceId') AS resource_reference_id, - JSON_VALUE(r.Catalogues, '$[0].LocationPaths') AS location_paths, - CAST(0 AS bit) AS statutory_mandatory, - r.Providers AS provider_ids, - r.Authors AS author, - CAST(0 AS bit) AS hidden, - NULL AS url, - --r.AmendDate AS last_modified, - SWITCHOFFSET(CAST(r.AmendDate AS datetimeoffset), '+00:00') AS last_modified, - r.Deleted AS is_deleted -FROM dbo.SearchResourcesView r; -GO; - - ------------------------------------------------------------------------------------------------------------------ - - - diff --git a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.3.Azure Search-SupersetSearchView.sql b/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.3.Azure Search-SupersetSearchView.sql new file mode 100644 index 000000000..661b744a3 --- /dev/null +++ b/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.3.Azure Search-SupersetSearchView.sql @@ -0,0 +1,66 @@ + +CREATE VIEW [dbo].[SupersetSearchView] +AS + +-- ============================ +-- Catalogue Rows +-- ============================ +SELECT + 'cat-'+ CAST(c.Id AS NVARCHAR(50)) AS id, + c.Name AS title, + LOWER(c.Name) AS normalised_title, + c.Description AS description, + 'catalogue' AS resource_collection, + c.Keywords as manual_tag, -- e.g., comma-separated or JSON if multiple + 'catalogue' AS resource_type, + NULL AS publication_date, + NULL AS date_authored, + NULL AS rating, + NULL AS catalogue_id, + NULL AS resource_reference_id, + NULL AS location_paths, + CAST(0 AS bit) AS statutory_mandatory, + c.Providers AS provider_ids, -- e.g., comma-separated or JSON if multiple + NULL AS author, + CAST(HIdden AS bit) AS hidden, + URL AS url, + --c.AmendDate AS last_modified, + SWITCHOFFSET(CAST(c.AmendDate AS datetimeoffset), '+00:00') AS last_modified, + CAST(HIdden AS bit) is_deleted +FROM dbo.SearchCataloguesView c + +UNION ALL + +-- ============================ +-- Resource Rows +-- ============================ +SELECT + 'res-'+ CAST(r.ResourceId AS NVARCHAR(50)) AS id, + r.title, + LOWER(r.title) AS normalised_title, + r.Description AS description, + 'resource' AS resource_collection, + r.Keywords AS manual_tag, + r.ContentType AS resource_type, + r.PublicationDate AS publication_date, + r.AuthoredDate AS date_authored, + CAST(r.AverageRating AS FLOAT) AS rating, + JSON_VALUE(r.Catalogues, '$[0].CatalogueNodeId') AS catalogue_id, + JSON_VALUE(r.Catalogues, '$[0].OriginalResourceReferenceId') AS resource_reference_id, + JSON_VALUE(r.Catalogues, '$[0].LocationPaths') AS location_paths, + CAST(0 AS bit) AS statutory_mandatory, + r.Providers AS provider_ids, + r.Authors AS author, + CAST(0 AS bit) AS hidden, + NULL AS url, + --r.AmendDate AS last_modified, + SWITCHOFFSET(CAST(r.AmendDate AS datetimeoffset), '+00:00') AS last_modified, + r.Deleted AS is_deleted +FROM dbo.SearchResourcesView r; +GO; + + +----------------------------------------------------------------------------------------------------------------- + + + From c2aedf32de0dde49bd736faf697a35b8deaa2574 Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 6 Feb 2026 16:16:53 +0000 Subject: [PATCH 078/106] Filename consistency --- .../LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj | 4 ++-- ...ew.sql => TD-6212.1.Azure Search-SearchCataloguesView.sql} | 0 ...iew.sql => TD-6212.2.Azure Search-SearchResourcesView.sql} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename WebAPI/LearningHub.Nhs.Database/Views/{TD-6212.1. Azure Search-SearchCataloguesView.sql => TD-6212.1.Azure Search-SearchCataloguesView.sql} (100%) rename WebAPI/LearningHub.Nhs.Database/Views/{TD-6212.2. Azure Search-SearchResourcesView.sql => TD-6212.2.Azure Search-SearchResourcesView.sql} (100%) diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index fb366fac3..519c620f0 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -693,9 +693,9 @@ - + - + diff --git a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1. Azure Search-SearchCataloguesView.sql b/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1.Azure Search-SearchCataloguesView.sql similarity index 100% rename from WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1. Azure Search-SearchCataloguesView.sql rename to WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1.Azure Search-SearchCataloguesView.sql diff --git a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.2. Azure Search-SearchResourcesView.sql b/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.2.Azure Search-SearchResourcesView.sql similarity index 100% rename from WebAPI/LearningHub.Nhs.Database/Views/TD-6212.2. Azure Search-SearchResourcesView.sql rename to WebAPI/LearningHub.Nhs.Database/Views/TD-6212.2.Azure Search-SearchResourcesView.sql From c903dc38d586896ae4867feced2053ab4e3194eb Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 6 Feb 2026 16:39:39 +0000 Subject: [PATCH 079/106] Updated the file names according to the naming standard --- .../LearningHub.Nhs.Database.sqlproj | 6 +++--- ...rchCataloguesView.sql => SearchCataloguesView.sql} | 11 ++++++++++- ...earchResourcesView.sql => SearchResourcesView.sql} | 11 ++++++++++- ...-SupersetSearchView.sql => SupersetSearchView.sql} | 10 +++++++++- 4 files changed, 32 insertions(+), 6 deletions(-) rename WebAPI/LearningHub.Nhs.Database/Views/{TD-6212.1.Azure Search-SearchCataloguesView.sql => SearchCataloguesView.sql} (75%) rename WebAPI/LearningHub.Nhs.Database/Views/{TD-6212.2.Azure Search-SearchResourcesView.sql => SearchResourcesView.sql} (89%) rename WebAPI/LearningHub.Nhs.Database/Views/{TD-6212.3.Azure Search-SupersetSearchView.sql => SupersetSearchView.sql} (82%) diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index 519c620f0..e91f5ae12 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -693,9 +693,9 @@ - - - + + + diff --git a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1.Azure Search-SearchCataloguesView.sql b/WebAPI/LearningHub.Nhs.Database/Views/SearchCataloguesView.sql similarity index 75% rename from WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1.Azure Search-SearchCataloguesView.sql rename to WebAPI/LearningHub.Nhs.Database/Views/SearchCataloguesView.sql index d8e7c1f7a..57b36273f 100644 --- a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.1.Azure Search-SearchCataloguesView.sql +++ b/WebAPI/LearningHub.Nhs.Database/Views/SearchCataloguesView.sql @@ -1,4 +1,13 @@ -CREATE VIEW [dbo].[SearchCataloguesView] AS +------------------------------------------------------------------------------- +-- Author Binon Yesudhas +-- Created 06-02-2026 +-- Purpose View of catalogues for Azure AI search +-- +-- Modification History +-- TD-6212 - https://hee-tis.atlassian.net/browse/TD-6212 +-- 06-02-2026 Binon Yesudhas Initial Revision +------------------------------------------------------------------------------- +CREATE VIEW [dbo].[SearchCataloguesView] AS WITH Catalogues AS ( SELECT nv.NodeId AS Id, diff --git a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.2.Azure Search-SearchResourcesView.sql b/WebAPI/LearningHub.Nhs.Database/Views/SearchResourcesView.sql similarity index 89% rename from WebAPI/LearningHub.Nhs.Database/Views/TD-6212.2.Azure Search-SearchResourcesView.sql rename to WebAPI/LearningHub.Nhs.Database/Views/SearchResourcesView.sql index ec3162312..d8eb0a019 100644 --- a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.2.Azure Search-SearchResourcesView.sql +++ b/WebAPI/LearningHub.Nhs.Database/Views/SearchResourcesView.sql @@ -1,4 +1,13 @@ -CREATE VIEW [dbo].[SearchResourcesView] +------------------------------------------------------------------------------- +-- Author Binon Yesudhas +-- Created 06-02-2026 +-- Purpose View of resources for Azure AI search +-- +-- Modification History +-- TD-6212 - https://hee-tis.atlassian.net/browse/TD-6212 +-- 06-02-2026 Binon Yesudhas Initial Revision +------------------------------------------------------------------------------- +CREATE VIEW [dbo].[SearchResourcesView] AS WITH BaseResource AS ( SELECT diff --git a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.3.Azure Search-SupersetSearchView.sql b/WebAPI/LearningHub.Nhs.Database/Views/SupersetSearchView.sql similarity index 82% rename from WebAPI/LearningHub.Nhs.Database/Views/TD-6212.3.Azure Search-SupersetSearchView.sql rename to WebAPI/LearningHub.Nhs.Database/Views/SupersetSearchView.sql index 661b744a3..2861f55c2 100644 --- a/WebAPI/LearningHub.Nhs.Database/Views/TD-6212.3.Azure Search-SupersetSearchView.sql +++ b/WebAPI/LearningHub.Nhs.Database/Views/SupersetSearchView.sql @@ -1,4 +1,12 @@ - +------------------------------------------------------------------------------- +-- Author Binon Yesudhas +-- Created 06-02-2026 +-- Purpose Combined view of both catalogues and resources for Azure AI search +-- +-- Modification History +-- TD-6212 - https://hee-tis.atlassian.net/browse/TD-6212 +-- 06-02-2026 Binon Yesudhas Initial Revision +------------------------------------------------------------------------------- CREATE VIEW [dbo].[SupersetSearchView] AS From 86f38e7613869588b476a2ef30cd894781e97a2e Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 6 Feb 2026 16:47:26 +0000 Subject: [PATCH 080/106] chnages the view to build from None --- .../LearningHub.Nhs.Database.sqlproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj index e91f5ae12..6be19efc9 100644 --- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj +++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj @@ -693,9 +693,9 @@ - - - + + + From 213945caf171e58bd9f224aa313f21d3339718cd Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Wed, 11 Feb 2026 08:45:21 +0000 Subject: [PATCH 081/106] TD-6864 Feature flag for InPlatform reporting --- .../Filters/ReporterPermissionFilter.cs | 12 ++- LearningHub.Nhs.WebUI/Helpers/FeatureFlags.cs | 5 ++ .../Services/NavigationPermissionService.cs | 21 +++-- LearningHub.Nhs.WebUI/appsettings.json | 13 +-- .../Configuration/FeatureFlagsConfig.cs | 14 ++++ .../Services/DatabricksService.cs | 1 + .../DatabricksServiceNoImplementation.cs | 84 +++++++++++++++++++ .../Services/NavigationPermissionService.cs | 26 ++++-- .../Startup.cs | 11 ++- .../Configuration/ConfigurationExtensions.cs | 7 ++ .../LearningHub.Nhs.OpenApi/appsettings.json | 3 +- 11 files changed, 176 insertions(+), 21 deletions(-) create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/FeatureFlagsConfig.cs create mode 100644 OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksServiceNoImplementation.cs diff --git a/LearningHub.Nhs.WebUI/Filters/ReporterPermissionFilter.cs b/LearningHub.Nhs.WebUI/Filters/ReporterPermissionFilter.cs index d1549807a..f121ea1a8 100644 --- a/LearningHub.Nhs.WebUI/Filters/ReporterPermissionFilter.cs +++ b/LearningHub.Nhs.WebUI/Filters/ReporterPermissionFilter.cs @@ -1,10 +1,12 @@ namespace LearningHub.Nhs.WebUI.Filters { using System.Threading.Tasks; + using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; + using Microsoft.FeatureManagement; /// /// Defines the . @@ -12,14 +14,17 @@ public class ReporterPermissionFilter : ActionFilterAttribute { private readonly IReportService reportService; + private IFeatureManager featureManager; /// /// Initializes a new instance of the class. /// /// reportService. - public ReporterPermissionFilter(IReportService reportService) + /// featureManager. + public ReporterPermissionFilter(IReportService reportService, IFeatureManager featureManager) { this.reportService = reportService; + this.featureManager = featureManager; } /// @@ -28,11 +33,14 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context var controller = context.ActionDescriptor.RouteValues["Controller"]; var action = context.ActionDescriptor.RouteValues["Action"]; + // Check if in-platform report is active. + var reportActive = Task.Run(() => this.featureManager.IsEnabledAsync(FeatureFlags.InPlatformReport)).Result; + // Run your cached permission check var hasPermission = await this.reportService.GetReporterPermission(); // If user does NOT have permission and they're not in safe routes - if (!hasPermission + if (!hasPermission && !reportActive && !(controller == "Account" && action == "AccessRestricted") && !(controller == "Home" && action == "Logout")) { diff --git a/LearningHub.Nhs.WebUI/Helpers/FeatureFlags.cs b/LearningHub.Nhs.WebUI/Helpers/FeatureFlags.cs index 43ce54c78..8c3e9bd76 100644 --- a/LearningHub.Nhs.WebUI/Helpers/FeatureFlags.cs +++ b/LearningHub.Nhs.WebUI/Helpers/FeatureFlags.cs @@ -24,5 +24,10 @@ public static class FeatureFlags /// The AzureSearch. /// public const string AzureSearch = "AzureSearch"; + + /// + /// The InPlatformReport. + /// + public const string InPlatformReport = "InPlatformReport"; } } diff --git a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs index 687cf9a0c..ce88580c2 100644 --- a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs +++ b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs @@ -2,8 +2,10 @@ { using System.Security.Principal; using System.Threading.Tasks; + using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.Models; + using Microsoft.FeatureManagement; /// /// Defines the . @@ -13,6 +15,7 @@ public class NavigationPermissionService : INavigationPermissionService private readonly IResourceService resourceService; private readonly IUserGroupService userGroupService; private readonly IReportService reportService; + private IFeatureManager featureManager; /// /// Initializes a new instance of the class. @@ -20,14 +23,17 @@ public class NavigationPermissionService : INavigationPermissionService /// Resource service. /// UserGroup service. /// Report Service. + /// Feature Manager. public NavigationPermissionService( IResourceService resourceService, IUserGroupService userGroupService, - IReportService reportService) + IReportService reportService, + IFeatureManager featureManager) { this.resourceService = resourceService; this.userGroupService = userGroupService; this.reportService = reportService; + this.featureManager = featureManager; } /// @@ -118,7 +124,7 @@ private async Task AuthenticatedAdministrator(string controller ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, - ShowReports = await this.reportService.GetReporterPermission(), + ShowReports = this.DisplayReportMenu() ? await this.reportService.GetReporterPermission() : false, }; } @@ -145,7 +151,7 @@ private async Task AuthenticatedBlueUser(string controllerName) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, - ShowReports = await this.reportService.GetReporterPermission(), + ShowReports = this.DisplayReportMenu() ? await this.reportService.GetReporterPermission() : false, }; } @@ -198,7 +204,7 @@ private async Task AuthenticatedReadOnly(string controllerName) ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = true, - ShowReports = await this.reportService.GetReporterPermission(), + ShowReports = this.DisplayReportMenu() ? await this.reportService.GetReporterPermission() : false, }; } @@ -224,7 +230,7 @@ private async Task AuthenticatedBasicUserOnly() ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, - ShowReports = await this.reportService.GetReporterPermission(), + ShowReports = this.DisplayReportMenu() ? await this.reportService.GetReporterPermission() : false, }; } @@ -253,5 +259,10 @@ private NavigationModel InLoginWizard() ShowReports = false, }; } + + private bool DisplayReportMenu() + { + return Task.Run(() => this.featureManager.IsEnabledAsync(FeatureFlags.InPlatformReport)).Result; + } } } diff --git a/LearningHub.Nhs.WebUI/appsettings.json b/LearningHub.Nhs.WebUI/appsettings.json index a0c06821c..ab5887246 100644 --- a/LearningHub.Nhs.WebUI/appsettings.json +++ b/LearningHub.Nhs.WebUI/appsettings.json @@ -165,12 +165,13 @@ "ClientId": "", "ClientIdentityKey": "" }, - "FeatureManagement": { - "ContributeAudioVideoResource": true, - "DisplayAudioVideoResource": true, - "EnableMoodle": false, - "AzureSearch": false - }, + "FeatureManagement": { + "ContributeAudioVideoResource": true, + "DisplayAudioVideoResource": true, + "EnableMoodle": false, + "AzureSearch": false, + "InPlatformReport": false + }, "IpRateLimiting": { "EnableEndpointRateLimiting": true, "StackBlockedRequests": false, diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/FeatureFlagsConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/FeatureFlagsConfig.cs new file mode 100644 index 000000000..4c3f3b8b4 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/FeatureFlagsConfig.cs @@ -0,0 +1,14 @@ +namespace LearningHub.Nhs.OpenApi.Models.Configuration +{ + /// + /// Defines the . + /// + public class FeatureFlagsConfig + { + + /// + /// The InPlatformReport. + /// + public string InPlatformReport { get; set; } = null!; + } +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs index c56bca122..e7037e78a 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksService.cs @@ -24,6 +24,7 @@ using LearningHub.Nhs.Models.Email.Models; using LearningHub.Nhs.Models.Email; using LearningHub.Nhs.OpenApi.Services.Helpers; +using System.Configuration; namespace LearningHub.Nhs.OpenApi.Services.Services { diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksServiceNoImplementation.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksServiceNoImplementation.cs new file mode 100644 index 000000000..72d373339 --- /dev/null +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/DatabricksServiceNoImplementation.cs @@ -0,0 +1,84 @@ +using LearningHub.Nhs.Models.Common; +using LearningHub.Nhs.Models.Databricks; +using LearningHub.Nhs.OpenApi.Models.ViewModels; +using LearningHub.Nhs.OpenApi.Services.Interface.Services; +using System.Collections.Generic; + +using System.Threading.Tasks; + +namespace LearningHub.Nhs.OpenApi.Services.Services +{ + /// + /// DatabricksServiceNoImplementation + /// + public class DatabricksServiceNoImplementation : IDatabricksService + { + /// + public Task IsUserReporter(int userId) + { + // Feature disabled → always return false + return Task.FromResult(false); + } + + /// + public Task CourseCompletionReport( + int userId, + DatabricksRequestModel model) + { + // Return an empty model to avoid null reference issues + return Task.FromResult(new DatabricksDetailedViewModel()); + } + + /// + public Task> GetPagedReportHistory( + int userId, + int page, + int pageSize) + { + // Return an empty paged result + return Task.FromResult(new PagedResultSet + { + Items = new List(), + TotalItemCount = 0 + }); + } + + /// + public Task GetPagedReportHistoryById( + int userId, + int reportHistoryId) + { + // Return an empty model + return Task.FromResult(new ReportHistoryModel()); + } + + /// + public Task QueueReportDownload(int userId, int reportHistoryId) + { + // Pretend the queue operation succeeded + return Task.FromResult(true); + } + + /// + public Task DownloadReport(int userId, int reportHistoryId) + { + // Return an empty model + return Task.FromResult(new ReportHistoryModel()); + } + + /// + public Task DatabricksJobUpdate(int userId, DatabricksNotification databricksNotification) + { + // No-op + return Task.CompletedTask; + } + + /// + public Task UpdateDatabricksReport(int userId, DatabricksUpdateRequest databricksUpdateRequest) + { + // No-op + return Task.CompletedTask; + } + } + +} diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs index eb460f2fc..d43382725 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/NavigationPermissionService.cs @@ -2,9 +2,10 @@ { using System.Security.Principal; using System.Threading.Tasks; - using LearningHub.Nhs.Models.Extensions; + using LearningHub.Nhs.OpenApi.Models.Configuration; using LearningHub.Nhs.OpenApi.Models.ViewModels; using LearningHub.Nhs.OpenApi.Services.Interface.Services; + using Microsoft.Extensions.Options; /// /// Defines the . @@ -14,6 +15,7 @@ public class NavigationPermissionService : INavigationPermissionService private readonly IResourceService resourceService; private readonly IUserGroupService userGroupService; private readonly IDatabricksService databricksService; + private readonly IOptions featureFlagsConfig; /// /// Initializes a new instance of the class. @@ -21,11 +23,13 @@ public class NavigationPermissionService : INavigationPermissionService /// Resource service. /// userGroup service. /// databricksService. - public NavigationPermissionService(IResourceService resourceService, IUserGroupService userGroupService, IDatabricksService databricksService) + /// featureFlags + public NavigationPermissionService(IResourceService resourceService, IUserGroupService userGroupService, IDatabricksService databricksService, IOptions featureFlagsConfig) { this.resourceService = resourceService; this.userGroupService = userGroupService; this.databricksService = databricksService; + this.featureFlagsConfig = featureFlagsConfig; } /// @@ -116,7 +120,7 @@ private async Task AuthenticatedAdministrator(string controller ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, - ShowReports = await this.databricksService.IsUserReporter(userId), + ShowReports = this.IsInplatformReportActive() ? await this.databricksService.IsUserReporter(userId) : false, }; } @@ -143,7 +147,7 @@ private async Task AuthenticatedBlueUser(string controllerName, ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, - ShowReports = await this.databricksService.IsUserReporter(userId), + ShowReports = this.IsInplatformReportActive() ? await this.databricksService.IsUserReporter(userId) : false, }; } @@ -194,7 +198,7 @@ private async Task AuthenticatedReadOnly(string controllerName, ShowSignOut = true, ShowMyAccount = false, ShowBrowseCatalogues = true, - ShowReports = await this.databricksService.IsUserReporter(userId), + ShowReports = this.IsInplatformReportActive() ? await this.databricksService.IsUserReporter(userId) : false, }; } @@ -219,7 +223,7 @@ private async Task AuthenticatedBasicUserOnly(int userId) ShowSignOut = true, ShowMyAccount = true, ShowBrowseCatalogues = true, - ShowReports = await this.databricksService.IsUserReporter(userId), + ShowReports = this.IsInplatformReportActive() ? await this.databricksService.IsUserReporter(userId) : false, }; } @@ -247,5 +251,15 @@ private NavigationModel InLoginWizard() ShowReports = false, }; } + + private bool IsInplatformReportActive() + { + bool.TryParse(this.featureFlagsConfig.Value.InPlatformReport, out bool inPlatformReport); + if (inPlatformReport) + { + return true; + } + return false; + } } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs index 07baa0438..73c887df7 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Startup.cs @@ -51,7 +51,16 @@ public static void AddServices(this IServiceCollection services, IConfiguration services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + + var reportingEnabled = configuration.GetValue("FeatureFlags:InPlatformReport"); + if (reportingEnabled) + { + services.AddScoped(); + } + else + { + services.AddScoped(); + } services.AddScoped(); services.AddScoped(); diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs index fd269b4e6..309eb8bd5 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi/Configuration/ConfigurationExtensions.cs @@ -55,6 +55,11 @@ public static class ConfigurationExtensions /// public const string DatabricksSectionName = "Databricks"; + /// + /// The FeatureFlagsSectionName. + /// + public const string FeatureFlagsSectionName = "FeatureFlags"; + /// /// Adds config. /// @@ -79,6 +84,8 @@ public static void AddConfig(this IServiceCollection services, IConfiguration co services.AddOptions().Bind(config.GetSection(MoodleSectionName)); services.AddOptions().Bind(config.GetSection(DatabricksSectionName)); + + services.AddOptions().Bind(config.GetSection(FeatureFlagsSectionName)); } private static OptionsBuilder RegisterPostConfigure(this OptionsBuilder builder) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index d4eb27e5a..198403227 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -55,7 +55,8 @@ "AzureStorageQueueConnectionString": "" }, "FeatureFlags": { - "UseAzureSearch": false + "UseAzureSearch": false, + "InPlatformReport": false }, "AzureSearch": { "ServiceEndpoint": "", From bcbf080660715364c9f6f2ec38d8d20134fea14a Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 12 Feb 2026 10:13:58 +0000 Subject: [PATCH 082/106] TD-6869, fix the issue showing unpublished resources on auto suggestion --- .../Views/Search/_AutoSuggest.cshtml | 21 ++++++++++++------- .../AzureSearch/SearchDocument.cs | 6 ++++++ .../AzureSearch/AzureSearchService.cs | 9 +++++--- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml index ccb1ff5fd..cea9b793d 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml @@ -9,7 +9,7 @@ var counter_res = 0; string GetUrl(string term, string searchType, AutoSuggestionClickPayloadModel payload, string reference, int? resourceReferenceId) { - + var url = string.Empty; if (!string.IsNullOrEmpty(reference) && searchType == "catalogue") { @@ -43,8 +43,8 @@ @foreach (var item in Model.ConceptDocument.ConceptDocumentList) { counter++; -
  • - +
  • + @@ -55,16 +55,21 @@ @foreach (var item in Model.ResourceCollectionDocument.DocumentList) { counter_res++; -
  • +
  • - Type: - @(item.ResourceType == "resource" ? "Learning resource" : item.ResourceType) + Type: @* + @(item.ResourceType == "resource" ? "Learning resource" : item.ResourceType) *@ + + @(item.ResourceType == "resource" + ? "Learning resource" + : !string.IsNullOrEmpty(item.ResourceType) + ? char.ToUpper(item.ResourceType[0]) + item.ResourceType.Substring(1) + : string.Empty)

    -
  • - } + } } \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs index 3cfc36ed3..fc5e03849 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs @@ -129,6 +129,12 @@ public string Description [JsonPropertyName("url")] public string Url { get; set; } = string.Empty; + // + /// Gets or sets the url. + /// + [JsonPropertyName("is_deleted")] + public bool IsDeleted { get; set; } + /// /// Strips paragraph tags from input string. /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 877f71f5a..e0d1ced13 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -600,12 +600,14 @@ public async Task GetAutoSuggestionResultsAsync(string term suggestOptions.Select.Add("resource_type"); suggestOptions.Select.Add("resource_collection"); suggestOptions.Select.Add("url"); - suggestOptions.Select.Add("resource_reference_id"); + suggestOptions.Select.Add("resource_reference_id"); + suggestOptions.Select.Add("is_deleted"); var autoOptions = new AutocompleteOptions { Mode = AutocompleteMode.OneTermWithContext, - Size = 5 + Size = 5, + Filter = "is_deleted eq false" }; var searchText = LuceneQueryBuilder.EscapeLuceneSpecialCharacters(term); @@ -621,7 +623,8 @@ public async Task GetAutoSuggestionResultsAsync(string term var autoResponse = await autoTask; var suggestResults = suggestResponse.Value.Results - .Where(r => !string.IsNullOrEmpty(r.Document?.Title)) + .Where(r => !string.IsNullOrEmpty(r.Document?.Title) && + r.Document?.IsDeleted == false) .Select(r => new { Id = r.Document.Id, From 87cdc5ed956091d61d1d7e1f2d695f0f5a4ffba6 Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 12 Feb 2026 16:39:12 +0000 Subject: [PATCH 083/106] Postporcessing the sort for azure sematic search --- .../AzureSearch/AzureSearchService.cs | 90 ++++++++++++++++--- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index e0d1ced13..05d61c505 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -24,6 +24,7 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; + using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -101,16 +102,33 @@ public async Task GetSearchResultAsync(SearchRequestModel sea { searchRequestModel.SortColumn, searchRequestModel.SortDirection } }; + // Determine if we need to apply post-processing sort + bool needsPostProcessingSort = searchQueryType == SearchQueryType.Semantic && + !string.IsNullOrEmpty(searchRequestModel.SortColumn) && + searchRequestModel.SortColumn != "relevance"; + + // For semantic search with sorting, retrieve buffer size results for post-processing + int queryPageSize = pageSize; + int queryOffset = offset; + int semanticResultBufferSize = 50; //this.azureSearchConfig.SemanticResultBufferSize; + + if (needsPostProcessingSort) + { + // Retrieve semantic buffer size results from the start + queryPageSize = semanticResultBufferSize; + queryOffset = 0; + } + // Normalize resource_type filter values var filters = SearchFilterBuilder.CombineAndNormaliseFilters(searchRequestModel.FilterText, searchRequestModel.ProviderFilterText); if (searchRequestModel.CatalogueId.HasValue) { Dictionary> catalogueIdFilter = new Dictionary> { { "catalogue_id", new List { searchRequestModel.CatalogueId.ToString() } } }; - filters = filters == null ? catalogueIdFilter : filters.Concat(catalogueIdFilter).ToDictionary(k => k.Key, v => v.Value); + filters = filters == null ? catalogueIdFilter : filters.Concat(catalogueIdFilter).ToDictionary(k => k.Key, v => v.Value); } - var searchOptions = SearchOptionsBuilder.BuildSearchOptions(searchQueryType, offset, pageSize, filters, sortBy, true); + var searchOptions = SearchOptionsBuilder.BuildSearchOptions(searchQueryType, queryOffset, queryPageSize, filters, sortBy, true); SearchResults filteredResponse = await this.searchClient.SearchAsync(query, searchOptions, cancellationToken); var count = Convert.ToInt32(filteredResponse.TotalCount); @@ -148,6 +166,15 @@ public async Task GetSearchResultAsync(SearchRequestModel sea }) .ToList(); + // Apply post-processing sort if needed + if (needsPostProcessingSort) + { + documents = ApplyPostProcessingSort(documents, searchRequestModel.SortColumn, searchRequestModel.SortDirection); + + // Apply pagination after sorting + documents = documents.Skip(offset).Take(pageSize).ToList(); + } + viewmodel.DocumentList = new Documentlist { Documents = documents.ToArray() @@ -177,6 +204,45 @@ public async Task GetSearchResultAsync(SearchRequestModel sea } } + /// + /// Applies post-processing sort to a list of documents. + /// Used when semantic search is active with non-relevance sorting. + /// + /// The documents to sort. + /// The column to sort by (title, rating, authored_date). + /// The sort direction (ascending/descending). + /// The sorted list of documents. + private List ApplyPostProcessingSort(List documents, string sortColumn, string sortDirection) + { + if (documents == null || documents.Count == 0) + { + return documents; + } + + bool isDescending = sortDirection != null && + (sortDirection.Equals("desc", StringComparison.OrdinalIgnoreCase) || + sortDirection.Equals("descending", StringComparison.OrdinalIgnoreCase)); + + IOrderedEnumerable sortedDocuments = sortColumn?.ToLowerInvariant() switch + { + "title" => isDescending + ? documents.OrderByDescending(d => d.Title, StringComparer.OrdinalIgnoreCase) + : documents.OrderBy(d => d.Title, StringComparer.OrdinalIgnoreCase), + "rating" => isDescending + ? documents.OrderByDescending(d => d.Rating) + : documents.OrderBy(d => d.Rating), + "authored_date" or "dateauthored" => isDescending + ? documents.OrderByDescending(d => + DateTime.TryParse(d.AuthoredDate, out var dt) ? dt : DateTime.MinValue) + : documents.OrderBy(d => + DateTime.TryParse(d.AuthoredDate, out var dt) ? dt : DateTime.MinValue), + _ => isDescending + ? documents.OrderByDescending(d => d.Title, StringComparer.OrdinalIgnoreCase) + : documents.OrderBy(d => d.Title, StringComparer.OrdinalIgnoreCase) + }; + + return sortedDocuments.ToList(); + } /// public async Task GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId, CancellationToken cancellationToken = default) { @@ -634,16 +700,16 @@ public async Task GetAutoSuggestionResultsAsync(string term Type = r.Document.ResourceCollection ?? "Suggestion" }); - var autoResults = autoResponse.Value.Results - .Where(r => !string.IsNullOrWhiteSpace(r.Text)) - .Select((r, index) => new - { - Id = "A" + (index + 1), - Text = r.Text.Trim(), - URL = string.Empty, - ResourceReferenceId = (string?)null, - Type = "AutoComplete" - }); + var autoResults = autoResponse.Value.Results + .Where(r => !string.IsNullOrWhiteSpace(r.Text)) + .Select((r, index) => new + { + Id = "A" + (index + 1), + Text = r.Text.Trim(), + URL = string.Empty, + ResourceReferenceId = (string?)null, + Type = "AutoComplete" + }); var combined = suggestResults .Concat(autoResults) From 01a764c7f637dd1ac006b7b4f71c4a10f1a04af6 Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 13 Feb 2026 11:27:14 +0000 Subject: [PATCH 084/106] Refactored and moved some configs to app settings --- .../Configuration/AzureSearchConfig.cs | 51 +++++- .../Helpers/Search/SearchOptionsBuilder.cs | 158 ++++++++++++++---- .../AzureSearch/AzureSearchService.cs | 52 +----- .../LearningHub.Nhs.OpenApi/appsettings.json | 4 + 4 files changed, 186 insertions(+), 79 deletions(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs index cf050977c..cc18f45b7 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace LearningHub.Nhs.OpenApi.Models.Configuration { /// @@ -43,11 +45,58 @@ public class AzureSearchConfig /// /// Gets or sets the suggester name for auto-complete and suggestions. /// - public string SuggesterName { get; set; } = "test-search-suggester"; + public string SuggesterName { get; set; } = null!; + + /// + /// Gets or sets the suggester size for auto-complete and suggestions. + /// + public int ConceptsSuggesterSize { get; set; } = 5; + + /// + /// Gets or sets the resource collection size (catalogue, course and resources) for auto-complete and suggestions. + /// + public int ResourceCollectionSuggesterSize { get; set; } = 5; /// /// Gets or sets the search query type (semantic, full, or simple). /// public string SearchQueryType { get; set; } = "Semantic"; + + /// + /// Gets or sets the semantic result buffer size for post-processing sorts. + /// When sorting is applied to semantic search results, this many results are retrieved + /// before applying the sort and pagination. Default is 50. + /// + public int SemanticResultBufferSize { get; set; } = 55; + + /// + /// Gets or sets the scoring profile name used for boosting search results. + /// Default is "boostExactTitle". + /// + public string ScoringProfile { get; set; } = "boostExactTitle"; + + /// + /// Gets or sets the semantic configuration name for semantic search. + /// Default is "default". + /// + public string SemanticConfigurationName { get; set; } = "default"; + + /// + /// Gets or sets the facet fields to include in search results. + /// Default is ["resource_type", "resource_collection", "provider_ids"]. + /// + public List FacetFields { get; set; } = new List { "resource_type", "resource_collection", "provider_ids" }; + + /// + /// Gets or sets the field name for the deleted filter. + /// Default is "is_deleted". + /// + public string DeletedFilterField { get; set; } = "is_deleted"; + + /// + /// Gets or sets the value for the deleted filter. + /// Default is "false". + /// + public string DeletedFilterValue { get; set; } = "false"; } } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs index d3c98b9e9..bad252c1d 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs @@ -11,6 +11,110 @@ /// public static class SearchOptionsBuilder { + /// + /// Determines if a sort direction is descending. + /// + /// The sort direction string. + /// True if descending, false otherwise. + public static bool IsDescendingSort(string? sortDirection) + { + return sortDirection != null && + (sortDirection.Equals("desc", StringComparison.OrdinalIgnoreCase) || + sortDirection.Equals("descending", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Maps a UI sort column name to the corresponding Azure Search field name. + /// + /// The UI sort column name. + /// The Azure Search field name, or null if no mapping exists. + public static string? MapSortColumnToSearchField(string? uiSortColumn) + { + if (string.IsNullOrWhiteSpace(uiSortColumn)) + return null; + + return uiSortColumn.Trim().ToLowerInvariant() switch + { + "avgrating" => "rating", + "rating" => "rating", + "authored_date" => "date_authored", + "authoreddate" => "date_authored", + "authoredDate" => "date_authored", + "title" => "normalised_title", + "atoz" => "normalised_title", + "alphabetical" => "normalised_title", + "ztoa" => "normalised_title", + _ => null + }; + } + + /// + /// Maps a UI sort column name to the corresponding Document property name for in-memory sorting. + /// + /// The UI sort column name. + /// The Document property name. + public static string? MapSortColumnToDocumentProperty(string? uiSortColumn) + { + if (string.IsNullOrWhiteSpace(uiSortColumn)) + return null; + + return uiSortColumn.Trim().ToLowerInvariant() switch + { + "avgrating" => "rating", + "rating" => "rating", + "authored_date" => "authored_date", + "authoreddate" => "authored_date", + "authoredDate" => "authored_date", + "title" => "title", + "atoz" => "title", + "alphabetical" => "title", + "ztoa" => "title", + _ => "title" // Default to title + }; + } + + /// + /// Applies post-processing sort to a list of documents. + /// Used when semantic search is active with non-relevance sorting. + /// + /// The documents to sort. + /// The column to sort by (title, rating, authored_date). + /// The sort direction (ascending/descending). + /// The sorted list of documents. + public static List ApplyPostProcessingSort( + List documents, + string? sortColumn, + string? sortDirection) + { + if (documents == null || documents.Count == 0) + { + return documents; + } + + bool isDescending = IsDescendingSort(sortDirection); + string? mappedColumn = MapSortColumnToDocumentProperty(sortColumn); + + IOrderedEnumerable sortedDocuments = mappedColumn?.ToLowerInvariant() switch + { + "title" => isDescending + ? documents.OrderByDescending(d => d.Title, StringComparer.OrdinalIgnoreCase) + : documents.OrderBy(d => d.Title, StringComparer.OrdinalIgnoreCase), + "rating" => isDescending + ? documents.OrderByDescending(d => d.Rating) + : documents.OrderBy(d => d.Rating), + "authored_date" => isDescending + ? documents.OrderByDescending(d => + DateTime.TryParse(d.AuthoredDate, out var dt) ? dt : DateTime.MinValue) + : documents.OrderBy(d => + DateTime.TryParse(d.AuthoredDate, out var dt) ? dt : DateTime.MinValue), + _ => isDescending + ? documents.OrderByDescending(d => d.Title, StringComparer.OrdinalIgnoreCase) + : documents.OrderBy(d => d.Title, StringComparer.OrdinalIgnoreCase) + }; + + return sortedDocuments.ToList(); + } + /// /// Builds search options for Azure Search queries. /// @@ -20,6 +124,7 @@ public static class SearchOptionsBuilder /// The filters to apply. /// The sort to apply. /// Whether to include facets. + /// The Azure Search configuration. /// The configured search options. public static SearchOptions BuildSearchOptions( SearchQueryType searchQueryType, @@ -27,14 +132,15 @@ public static SearchOptions BuildSearchOptions( int pageSize, Dictionary>? filters, Dictionary? sortBy, - bool includeFacets) + bool includeFacets, + Models.Configuration.AzureSearchConfig config) { var searchOptions = new SearchOptions { Skip = offset, Size = pageSize, IncludeTotalCount = true, - ScoringProfile = "boostExactTitle" + ScoringProfile = config.ScoringProfile }; string sortByFinal = GetSortOption(sortBy); @@ -45,7 +151,7 @@ public static SearchOptions BuildSearchOptions( searchOptions.QueryType = SearchQueryType.Semantic; searchOptions.SemanticSearch = new SemanticSearchOptions { - SemanticConfigurationName = "default" + SemanticConfigurationName = config.SemanticConfigurationName }; } else if (searchQueryType == SearchQueryType.Simple) @@ -61,15 +167,20 @@ public static SearchOptions BuildSearchOptions( } // Add facets - if (includeFacets) + if (includeFacets && config.FacetFields != null) { - searchOptions.Facets.Add("resource_type"); - searchOptions.Facets.Add("resource_collection"); - searchOptions.Facets.Add("provider_ids"); + foreach (var facet in config.FacetFields) + { + searchOptions.Facets.Add(facet); + } } - Dictionary> deletFilter = new Dictionary> {{ "is_deleted", new List {"false"} }}; - filters = filters == null ? deletFilter : filters.Concat(deletFilter).ToDictionary(k => k.Key, v => v.Value); + // Add deleted filter + Dictionary> deleteFilter = new Dictionary> + { + { config.DeletedFilterField, new List { config.DeletedFilterValue } } + }; + filters = filters == null ? deleteFilter : filters.Concat(deleteFilter).ToDictionary(k => k.Key, v => v.Value); // Apply filters if (filters?.Any() == true) @@ -78,7 +189,7 @@ public static SearchOptions BuildSearchOptions( } return searchOptions; - } + } private static string GetSortOption(Dictionary? sortBy) { @@ -94,30 +205,11 @@ private static string GetSortOption(Dictionary? sortBy) if (string.IsNullOrWhiteSpace(uiSortKey)) return string.Empty; - // Determine direction safely - string sortDirection = - directionInput != null && - directionInput.StartsWith("desc", StringComparison.OrdinalIgnoreCase) - ? "desc" - : "asc"; - - // Map UI values to search fields - string? sortColumn = uiSortKey.Trim().ToLowerInvariant() switch - { - "avgrating" => "rating", - "rating" => "rating", + // Determine direction using shared helper + string sortDirection = IsDescendingSort(directionInput) ? "desc" : "asc"; - "authored_date" => "date_authored", - "authoreddate" => "date_authored", - "authoredDate" => "date_authored", - - "title" => "normalised_title", - "atoz" => "normalised_title", - "alphabetical" => "normalised_title", - "ztoa" => "normalised_title", - - _ => null // unknown sort → ignore - }; + // Map UI values to search fields using shared helper + string? sortColumn = MapSortColumnToSearchField(uiSortKey); // No valid mapping → fall back to relevance if (string.IsNullOrWhiteSpace(sortColumn)) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 05d61c505..a5a8ce5a4 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -110,7 +110,7 @@ public async Task GetSearchResultAsync(SearchRequestModel sea // For semantic search with sorting, retrieve buffer size results for post-processing int queryPageSize = pageSize; int queryOffset = offset; - int semanticResultBufferSize = 50; //this.azureSearchConfig.SemanticResultBufferSize; + int semanticResultBufferSize = this.azureSearchConfig.SemanticResultBufferSize; if (needsPostProcessingSort) { @@ -128,7 +128,7 @@ public async Task GetSearchResultAsync(SearchRequestModel sea filters = filters == null ? catalogueIdFilter : filters.Concat(catalogueIdFilter).ToDictionary(k => k.Key, v => v.Value); } - var searchOptions = SearchOptionsBuilder.BuildSearchOptions(searchQueryType, queryOffset, queryPageSize, filters, sortBy, true); + var searchOptions = SearchOptionsBuilder.BuildSearchOptions(searchQueryType, queryOffset, queryPageSize, filters, sortBy, true, this.azureSearchConfig); SearchResults filteredResponse = await this.searchClient.SearchAsync(query, searchOptions, cancellationToken); var count = Convert.ToInt32(filteredResponse.TotalCount); @@ -169,7 +169,7 @@ public async Task GetSearchResultAsync(SearchRequestModel sea // Apply post-processing sort if needed if (needsPostProcessingSort) { - documents = ApplyPostProcessingSort(documents, searchRequestModel.SortColumn, searchRequestModel.SortDirection); + documents = SearchOptionsBuilder.ApplyPostProcessingSort(documents, searchRequestModel.SortColumn, searchRequestModel.SortDirection); // Apply pagination after sorting documents = documents.Skip(offset).Take(pageSize).ToList(); @@ -204,45 +204,6 @@ public async Task GetSearchResultAsync(SearchRequestModel sea } } - /// - /// Applies post-processing sort to a list of documents. - /// Used when semantic search is active with non-relevance sorting. - /// - /// The documents to sort. - /// The column to sort by (title, rating, authored_date). - /// The sort direction (ascending/descending). - /// The sorted list of documents. - private List ApplyPostProcessingSort(List documents, string sortColumn, string sortDirection) - { - if (documents == null || documents.Count == 0) - { - return documents; - } - - bool isDescending = sortDirection != null && - (sortDirection.Equals("desc", StringComparison.OrdinalIgnoreCase) || - sortDirection.Equals("descending", StringComparison.OrdinalIgnoreCase)); - - IOrderedEnumerable sortedDocuments = sortColumn?.ToLowerInvariant() switch - { - "title" => isDescending - ? documents.OrderByDescending(d => d.Title, StringComparer.OrdinalIgnoreCase) - : documents.OrderBy(d => d.Title, StringComparer.OrdinalIgnoreCase), - "rating" => isDescending - ? documents.OrderByDescending(d => d.Rating) - : documents.OrderBy(d => d.Rating), - "authored_date" or "dateauthored" => isDescending - ? documents.OrderByDescending(d => - DateTime.TryParse(d.AuthoredDate, out var dt) ? dt : DateTime.MinValue) - : documents.OrderBy(d => - DateTime.TryParse(d.AuthoredDate, out var dt) ? dt : DateTime.MinValue), - _ => isDescending - ? documents.OrderByDescending(d => d.Title, StringComparer.OrdinalIgnoreCase) - : documents.OrderBy(d => d.Title, StringComparer.OrdinalIgnoreCase) - }; - - return sortedDocuments.ToList(); - } /// public async Task GetCatalogueSearchResultAsync(CatalogueSearchRequestModel catalogSearchRequestModel, int userId, CancellationToken cancellationToken = default) { @@ -672,7 +633,7 @@ public async Task GetAutoSuggestionResultsAsync(string term var autoOptions = new AutocompleteOptions { Mode = AutocompleteMode.OneTermWithContext, - Size = 5, + Size = this.azureSearchConfig.ConceptsSuggesterSize, Filter = "is_deleted eq false" }; @@ -734,10 +695,11 @@ public async Task GetAutoSuggestionResultsAsync(string term Title = item.Text, Click = BuildAutoSuggestClickModel(item.Id, item.Text, 0, 0, term, suggestResults.Count()) }) - .Take(5) + .Take(this.azureSearchConfig.ResourceCollectionSuggesterSize) .ToList() }; + // We couldn't pass null value so just etting to empty collection with 0 hits, as we are using one resouce colection in auto-suggest var autoSuggestionCatalogue = new AutoSuggestionCatalogue { TotalHits = suggestResults.Count(), @@ -768,7 +730,7 @@ public async Task GetAutoSuggestionResultsAsync(string term }; viewmodel.ResourceCollectionDocument = autoSuggestion; - viewmodel.CatalogueDocument = autoSuggestionCatalogue; + viewmodel.CatalogueDocument = autoSuggestionCatalogue; // We coundt pass null value so just etting to empty collection with 0 hits, as we are using one resouce colection in auto-suggest viewmodel.ConceptDocument = autoSuggestionConcept; return viewmodel; diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json index d4eb27e5a..a74d1b408 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json +++ b/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.json @@ -63,7 +63,11 @@ "QueryApiKey": "", "IndexName": "", "SuggesterName": "", + "ConceptsSuggesterSize": 5, + "ResourceCollectionSuggesterSize": 5, "SearchQueryType": "semantic", //semantic, full, or simple + "SemanticResultBufferSize": 50, + "ScoringProfile": "boostExactTitle", "DefaultItemLimitForSearch": 10, "DescriptionLengthLimit": 3000, "MaximumDescriptionLength": 150 From 12cf750d04cc0dd2a214283074439456314339ce Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 13 Feb 2026 16:33:45 +0000 Subject: [PATCH 085/106] fixed the issue with general user --- .../Services/SearchService.cs | 5 +- .../Views/Search/_AutoSuggest.cshtml | 62 ++++++++++--------- .../Configuration/AzureSearchConfig.cs | 2 +- .../AzureSearch/AzureSearchService.cs | 4 +- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Services/SearchService.cs b/LearningHub.Nhs.WebUI/Services/SearchService.cs index ef7a31707..f259f2c4e 100644 --- a/LearningHub.Nhs.WebUI/Services/SearchService.cs +++ b/LearningHub.Nhs.WebUI/Services/SearchService.cs @@ -151,10 +151,9 @@ public async Task PerformSearch(IPrincipal user, SearchRe { var accessLevelFilters = resourceResult.Facets.Where(x => x.Id == "resource_access_level").First().Filters; - var generalAccessValue = (int)ResourceAccessibilityEnum.GeneralAccess; - var basicUserAudienceFilterItem = accessLevelFilters.Where(x => x.DisplayName == generalAccessValue.ToString()).FirstOrDefault(); + var basicUserAudienceFilterItem = accessLevelFilters.Where(x => x.DisplayName == "2").FirstOrDefault(); // GeneralAccess var basicResourceAccesslevelCount = basicUserAudienceFilterItem?.Count ?? 0; - var basicUserAudienceFilter = new SearchFilterModel() { DisplayName = ResourceAccessLevelHelper.GetPrettifiedResourceAccessLevelOptionDisplayName(ResourceAccessibilityEnum.GeneralAccess), Count = basicResourceAccesslevelCount, Value = generalAccessValue.ToString(), Selected = (searchRequest.ResourceAccessLevelId ?? 0) == generalAccessValue }; + var basicUserAudienceFilter = new SearchFilterModel() { DisplayName = ResourceAccessLevelHelper.GetPrettifiedResourceAccessLevelOptionDisplayName(ResourceAccessibilityEnum.GeneralAccess), Count = basicResourceAccesslevelCount, Value = "2", Selected = (searchRequest.ResourceAccessLevelId ?? 0) == 2 }; resourceAccessLevelFilters.Add(basicUserAudienceFilter); } diff --git a/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml index cea9b793d..3b5582254 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_AutoSuggest.cshtml @@ -40,36 +40,42 @@ } @if (Model != null) { - @foreach (var item in Model.ConceptDocument.ConceptDocumentList) + @if (Model.ConceptDocument != null) { - counter++; -
  • - - - - - - -
  • + @foreach (var item in Model.ConceptDocument.ConceptDocumentList) + { + counter++; +
  • + + + + + + +
  • + } } - @foreach (var item in Model.ResourceCollectionDocument.DocumentList) + @if (Model.ResourceCollectionDocument != null) { - counter_res++; -
  • - - -

    - Type: @* + @foreach (var item in Model.ResourceCollectionDocument.DocumentList) + { + counter_res++; +

  • + + +

    + Type: @* @(item.ResourceType == "resource" ? "Learning resource" : item.ResourceType) *@ - - @(item.ResourceType == "resource" - ? "Learning resource" - : !string.IsNullOrEmpty(item.ResourceType) - ? char.ToUpper(item.ResourceType[0]) + item.ResourceType.Substring(1) - : string.Empty) - -

    -
    -
  • - } + + @(item.ResourceType == "resource" + ? "Learning resource" + : !string.IsNullOrEmpty(item.ResourceType) + ? char.ToUpper(item.ResourceType[0]) + item.ResourceType.Substring(1) + : string.Empty) + +

    + + + } + } } \ No newline at end of file diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs index cc18f45b7..15f0921ee 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/Configuration/AzureSearchConfig.cs @@ -85,7 +85,7 @@ public class AzureSearchConfig /// Gets or sets the facet fields to include in search results. /// Default is ["resource_type", "resource_collection", "provider_ids"]. ///
    - public List FacetFields { get; set; } = new List { "resource_type", "resource_collection", "provider_ids" }; + public List FacetFields { get; set; } = new List { "resource_type", "resource_collection", "provider_ids", "resource_access_level" }; /// /// Gets or sets the field name for the deleted filter. diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index a5a8ce5a4..ffaa1e6ef 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -114,8 +114,8 @@ public async Task GetSearchResultAsync(SearchRequestModel sea if (needsPostProcessingSort) { - // Retrieve semantic buffer size results from the start - queryPageSize = semanticResultBufferSize; + // Retrieve semantic buffer size results from the starta nd add offset for post-processing sort and pagination + queryPageSize = semanticResultBufferSize + offset; queryOffset = 0; } From b2aacaf261afe826fc887cc4e1b4b0e8be5c10c5 Mon Sep 17 00:00:00 2001 From: Arunima George Date: Mon, 16 Feb 2026 13:04:32 +0000 Subject: [PATCH 086/106] TD-6873: Fixed style issues on auto suggestion results during tabbing --- .../Styles/nhsuk/layout.scss | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss index 67cdb0895..accbcfa37 100644 --- a/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss @@ -246,7 +246,7 @@ button[data-toggle="modal"] { } .autosuggestion-menu { - padding: 16px 16px 0px 16px !important; + padding: 0; background-color: $color_nhsuk-white; border-bottom: 1px solid $color_nhsuk-grey-4; border-radius: 0px 0px 4px 4px; @@ -263,12 +263,11 @@ button[data-toggle="modal"] { } .autosuggestion-option { - margin-bottom: 0; + margin-bottom: 0 !important; border-bottom: 1px solid $color_nhsuk-grey-4; color: $color_nhsuk-blue; cursor: pointer; font-size: 16px; - padding-bottom: 12px; text-align: left; text-decoration: none; } @@ -297,6 +296,32 @@ li.autosuggestion-option:last-of-type { border-bottom: none !important; } +.autosuggestion-option a { + display: block; + width: 100%; + padding: 8px 16px; +} + +.autosuggestion-option a:hover, +.autosuggestion-option a:focus-visible { + background-color: #ffeb3b; + box-shadow: 0 4px 0 0 #212b32; + z-index: 5; + outline: none !important; + text-decoration: none; + padding-bottom: 10px +} + +.autosuggestion-option a:focus .autosuggestion-icon, .autosuggestion-option a:hover .autosuggestion-icon { + fill: #212b32 !important; +} + +.autosuggestion-option a:focus .autosuggestion-link, .autosuggestion-option a:hover .autosuggestion-link { + font-weight: 700; + color: #212b32; + text-decoration: none +} + /* side navigation styles */ .side-nav__list { From 32c76cd85afdff7a7c2f4291194e1f2090dde448 Mon Sep 17 00:00:00 2001 From: Arunima George Date: Wed, 18 Feb 2026 12:10:05 +0000 Subject: [PATCH 087/106] TD-6873: Fixed keyboard arrows accessing the auto suggestion results. --- .../Views/Search/_SearchBar.cshtml | 39 +++++++++++++++++++ .../NavigationItems/Searchbar.cshtml | 39 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchBar.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchBar.cshtml index da4a475e9..cf5ed2f8b 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchBar.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchBar.cshtml @@ -19,6 +19,7 @@ var searchInput = document.getElementById("sub-search-field"); var suggestionsList = document.getElementById("sub-search-field_listbox"); var minLengthAutoComplete = 3; + let currentIndex = -1; function fetchSuggestions(term) { var xhr = new XMLHttpRequest(); @@ -69,9 +70,47 @@ } } + function handleArrowKeys(e) { + const items = suggestionsList.querySelectorAll('.autosuggestion-option > a'); + if (!items.length) return; + + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + currentIndex = (currentIndex + 1) % items.length; + items[currentIndex].focus(); + break; + + case "ArrowUp": + e.preventDefault(); + currentIndex = (currentIndex - 1 + items.length) % items.length; + items[currentIndex].focus(); + break; + + case "Tab": + if (currentIndex >= items.length - 1) { + closeAllLists(); + currentIndex = -1; + } + else { + e.preventDefault(); + currentIndex++; + items[currentIndex].focus(); + } + break; + } + } + + searchInput.addEventListener("focus", function () { + currentIndex = -1; + }); + searchInput.addEventListener("keydown", handleArrowKeys); + suggestionsList.addEventListener("keydown", handleArrowKeys); + function closeAllLists() { suggestionsList.innerHTML = ''; suggestionsList.style.display = "none"; + currentIndex = -1; } autocomplete(searchInput, minLengthAutoComplete); diff --git a/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Searchbar.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Searchbar.cshtml index f3a69f8e6..a2839223d 100644 --- a/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Searchbar.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Shared/Components/NavigationItems/Searchbar.cshtml @@ -43,6 +43,7 @@ var searchInput = document.getElementById("search-field"); var suggestionsList = document.getElementById("search-field_listbox"); var minLengthAutoComplete = 3; + let currentIndex = -1; function fetchSuggestions(term) { var xhr = new XMLHttpRequest(); @@ -93,9 +94,47 @@ } } + function handleArrowKeys(e) { + const items = suggestionsList.querySelectorAll('.autosuggestion-option > a'); + if (!items.length) return; + + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + currentIndex = (currentIndex + 1) % items.length; + items[currentIndex].focus(); + break; + + case "ArrowUp": + e.preventDefault(); + currentIndex = (currentIndex - 1 + items.length) % items.length; + items[currentIndex].focus(); + break; + + case "Tab": + if (currentIndex >= items.length - 1) { + closeAllLists(); + currentIndex = -1; + } + else { + e.preventDefault(); + currentIndex++; + items[currentIndex].focus(); + } + break; + } + } + + searchInput.addEventListener("focus", function () { + currentIndex = -1; + }); + searchInput.addEventListener("keydown", handleArrowKeys); + suggestionsList.addEventListener("keydown", handleArrowKeys); + function closeAllLists() { suggestionsList.innerHTML = ''; suggestionsList.style.display = "none"; + currentIndex = -1; } autocomplete(searchInput, minLengthAutoComplete); From dc23e97e77e0fa0350c152c4953872cdb0b79ffb Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Tue, 10 Feb 2026 14:58:02 +0000 Subject: [PATCH 088/106] Merge pull request #1680 from TechnologyEnhancedLearning/Develop/Fixes/TD-6848-Certificate-Generation-Does-Not-Trigger-After-Successful-Completion-of-the-Resources Develop/fixes/td 6848 certificate generation does not trigger after successful completion of the resources --- .../Controllers/Api/ScormController.cs | 3 ++ .../Interfaces/IActivityService.cs | 7 ++++ .../Services/ActivityService.cs | 33 ++++++++++++++++++ .../Controllers/ActivityController.cs | 21 ++++++++++++ .../Activity/ScormActivityComplete.sql | 7 ++-- .../Resources/GetUsercertificateDetails.sql | 15 ++++---- .../IActivityService.cs | 8 +++++ .../ActivityService.cs | 34 +++++++++++++++++++ 8 files changed, 119 insertions(+), 9 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Controllers/Api/ScormController.cs b/LearningHub.Nhs.WebUI/Controllers/Api/ScormController.cs index 6a8f7d699..690094c16 100644 --- a/LearningHub.Nhs.WebUI/Controllers/Api/ScormController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/Api/ScormController.cs @@ -261,6 +261,9 @@ private async Task Commit(SCO scoObject) // Persist update. await this.activityService.UpdateScormActivityAsync(scoObject); + // Create Activity Complete event. (TODO process event using service bus queue - perform any longer running async status re-calc). + await this.activityService.ScormCompleteActivity(scoObject); + return true; } catch (Exception ex) diff --git a/LearningHub.Nhs.WebUI/Interfaces/IActivityService.cs b/LearningHub.Nhs.WebUI/Interfaces/IActivityService.cs index 8dff7ae97..38e3bef61 100644 --- a/LearningHub.Nhs.WebUI/Interfaces/IActivityService.cs +++ b/LearningHub.Nhs.WebUI/Interfaces/IActivityService.cs @@ -66,6 +66,13 @@ public interface IActivityService /// The . Task CompleteScormActivity(ScormActivityViewModel scormActivityViewModel); + /// + /// The CompleteScormActivity. + /// + /// The updateScormActivityViewModel. + /// The . + Task ScormCompleteActivity(ScormActivityViewModel scormActivityViewModel); + /// /// The ResolveScormActivity. /// diff --git a/LearningHub.Nhs.WebUI/Services/ActivityService.cs b/LearningHub.Nhs.WebUI/Services/ActivityService.cs index 185dc3a2c..9f1e43f3d 100644 --- a/LearningHub.Nhs.WebUI/Services/ActivityService.cs +++ b/LearningHub.Nhs.WebUI/Services/ActivityService.cs @@ -294,6 +294,39 @@ public async Task CompleteScormActivity(ScormActivi return validationResult; } + /// + /// The CompleteScormActivity. + /// + /// The updateScormActivityViewModel. + /// The . + public async Task ScormCompleteActivity(ScormActivityViewModel scormActivityViewModel) + { + var json = JsonConvert.SerializeObject(scormActivityViewModel); + var stringContent = new StringContent(json, Encoding.UTF8, "application/json"); + + var client = await this.LearningHubHttpClient.GetClientAsync(); + + var request = $"Activity/ScormCompleteActivity"; + var response = await client.PostAsync(request, stringContent).ConfigureAwait(false); + + LearningHubValidationResult validationResult; + if (response.IsSuccessStatusCode) + { + var result = response.Content.ReadAsStringAsync().Result; + validationResult = JsonConvert.DeserializeObject(result); + } + else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + throw new Exception("AccessDenied"); + } + else + { + throw new Exception("Complete Scorm Activity failed!"); + } + + return validationResult; + } + /// /// The ResolveScormActivity. /// diff --git a/WebAPI/LearningHub.Nhs.API/Controllers/ActivityController.cs b/WebAPI/LearningHub.Nhs.API/Controllers/ActivityController.cs index d42757b5d..fc5767d6a 100644 --- a/WebAPI/LearningHub.Nhs.API/Controllers/ActivityController.cs +++ b/WebAPI/LearningHub.Nhs.API/Controllers/ActivityController.cs @@ -211,6 +211,27 @@ public async Task CompleteScormActivity(ScormActivityViewModel co } } + /// + /// Complete Scorm Activity. + /// + /// The scorm activity. + /// The . + [HttpPost] + [Route("ScormCompleteActivity")] + public async Task ScormCompleteActivity(ScormActivityViewModel completeScormActivityViewModel) + { + var vr = await this.activityService.ScormCompleteActivity(this.CurrentUserId, completeScormActivityViewModel); + + if (vr.IsValid) + { + return this.Ok(new ApiResponse(true, vr)); + } + else + { + return this.BadRequest(new ApiResponse(false, vr)); + } + } + /// /// Launch Scorm Activity. /// diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/ScormActivityComplete.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/ScormActivityComplete.sql index c04238a5a..db09d0c3a 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/ScormActivityComplete.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/ScormActivityComplete.sql @@ -7,6 +7,7 @@ -- -- 11-06-2021 Killian Davies Initial Revision -- 17-04-2024 Swapna Abraham Reverted TD-1325 changes +-- 10-02-2026 Swapna Abraham TD-6848 Certificate Generation Does Not Trigger After Successful Completion of the Resources ------------------------------------------------------------------------------- CREATE PROCEDURE [activity].[ScormActivityComplete] ( @@ -45,13 +46,14 @@ BEGIN IF EXISTS (SELECT 'X' FROM activity.ResourceActivity WHERE LaunchResourceActivityId = @ScormResourceActivityId AND ActivityStatusId IN (3, 4, 5)) BEGIN DECLARE @ActivityStatusErrorMessage nvarchar(1024) - SELECT @ActivityStatusErrorMessage = 'ResourceActivity entry with Completed status already exists for ScormActivityId=' + @ScormActivityId + SELECT @ActivityStatusErrorMessage = 'ResourceActivity entry with Completed status already exists for ScormActivityId=' + CONVERT(nvarchar(20), @ScormActivityId); RAISERROR (@ActivityStatusErrorMessage, 16, -- Severity. 1 -- State. ); END - +ELSE +BEGIN -- Validation ported from e-LfH: completed status requires duration > 0 IF (@ActivityStatusId IN (3, 4, 5) AND @DurationSeconds > 0) BEGIN TRY @@ -123,5 +125,6 @@ BEGIN ); END CATCH END +END GO \ No newline at end of file diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetUsercertificateDetails.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetUsercertificateDetails.sql index 50e795c87..eec995f79 100644 --- a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetUsercertificateDetails.sql +++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Resources/GetUsercertificateDetails.sql @@ -48,13 +48,14 @@ BEGIN AND mar.PercentComplete = 100 ) OR (r.ResourceTypeId = 6 AND ( - EXISTS ( - SELECT 1 - FROM activity.ScormActivity sa - WHERE sa.ResourceActivityId = ra.Id - AND sa.CmiCoreLesson_status IN (3,5) - ) - OR ra.ActivityStatusId IN (3,5) + --EXISTS ( + -- SELECT 1 + -- FROM activity.ScormActivity sa + -- WHERE sa.ResourceActivityId = ra.Id + -- AND sa.CmiCoreLesson_status IN (3,5) + --) + --OR + ra.ActivityStatusId IN (3,5) )) OR ( r.ResourceTypeId = 11 diff --git a/WebAPI/LearningHub.Nhs.Services.Interface/IActivityService.cs b/WebAPI/LearningHub.Nhs.Services.Interface/IActivityService.cs index 5cbeb453d..99a9d0f0e 100644 --- a/WebAPI/LearningHub.Nhs.Services.Interface/IActivityService.cs +++ b/WebAPI/LearningHub.Nhs.Services.Interface/IActivityService.cs @@ -106,6 +106,14 @@ public interface IActivityService /// The . Task CompleteScormActivity(int currentUserId, ScormActivityViewModel completeScormActivityViewModel); + /// + /// Complete scorm activity. + /// + /// The user Id. + /// The update scorm Activity View Model. + /// The . + Task ScormCompleteActivity(int currentUserId, ScormActivityViewModel completeScormActivityViewModel); + /// /// The resolve scorm activity. /// Resolves any completed active content that does not have associated completion events. diff --git a/WebAPI/LearningHub.Nhs.Services/ActivityService.cs b/WebAPI/LearningHub.Nhs.Services/ActivityService.cs index 19c98e376..9b5245504 100644 --- a/WebAPI/LearningHub.Nhs.Services/ActivityService.cs +++ b/WebAPI/LearningHub.Nhs.Services/ActivityService.cs @@ -18,6 +18,7 @@ using LearningHub.Nhs.Repository.Interface.Resources; using LearningHub.Nhs.Services.Helpers; using LearningHub.Nhs.Services.Interface; + using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Rest; @@ -526,6 +527,39 @@ public async Task CompleteScormActivity(int current return new LearningHubValidationResult(true); } + /// + /// Complete scorm activity. + /// + /// The user Id. + /// The update scorm Activity View Model. + /// The . + public async Task ScormCompleteActivity(int currentUserId, ScormActivityViewModel completeScormActivityViewModel) + { + try + { + if (completeScormActivityViewModel.LessonStatusId.HasValue + && (completeScormActivityViewModel.LessonStatusId.Value == (int)ActivityStatusEnum.Completed + || completeScormActivityViewModel.LessonStatusId == (int)ActivityStatusEnum.Passed + || completeScormActivityViewModel.LessonStatusId == (int)ActivityStatusEnum.Failed)) + { + // Handle activity "complete" event - create new ResourceActivity record & perform any re-calc status updates. + this.scormActivityRepository.Complete(currentUserId, completeScormActivityViewModel.InstanceId); + } + } + catch (SqlException ex) + { + if (!ex.Message.Contains( + "ResourceActivity entry with Completed status already exists", + StringComparison.OrdinalIgnoreCase)) + { + throw; + } + // else: intentionally ignore + } + + return new LearningHubValidationResult(true); + } + /// /// The resolve scorm activity. /// Resolves any completed active content that does not have associated completion events. From 6deba0937f76826b7487c457a8d479f2774210b6 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Mon, 23 Feb 2026 17:51:48 +0000 Subject: [PATCH 089/106] . --- .../Scripts/vuesrc/activity.ts | 34 ++++++++++++++++--- .../resource/CaseOrAssessmentResource.vue | 19 +++++++++-- .../vuesrc/resource/ResourceContent.vue | 19 ++++++++--- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/activity.ts b/LearningHub.Nhs.WebUI/Scripts/vuesrc/activity.ts index bc5a08bbb..91fd70738 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/activity.ts +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/activity.ts @@ -10,13 +10,18 @@ const recordActivityLaunched = async function ( resourceVersionId: number, nodePathId: number, activityDatetime: Date, + isMultiPageCase: boolean = false, extraAttemptReason?: string): Promise { var data = { resourceVersionId: resourceVersionId, nodePathId: nodePathId, - activityStatus: (resourceType == ResourceType.ASSESSMENT || resourceType == ResourceType.VIDEO || resourceType == ResourceType.AUDIO || - resourceType == ResourceType.SCORM) ? ActivityStatus.Incomplete : ActivityStatus.Completed, + activityStatus: (resourceType == ResourceType.ASSESSMENT || + resourceType == ResourceType.VIDEO || + resourceType == ResourceType.AUDIO || + resourceType == ResourceType.SCORM || + (resourceType == ResourceType.CASE && isMultiPageCase) + ) ? ActivityStatus.Incomplete : ActivityStatus.Completed, activityStart: activityDatetime, extraAttemptReason }; @@ -35,8 +40,10 @@ const recordActivityLaunched = async function ( console.log('recordActivityLaunched:' + e); throw e; }); - }; + + + const recordActivity = async function ( resourceVersionId: number, nodePathId: number, @@ -215,6 +222,24 @@ const recordAssessmentResourceActivityInteraction = async function ( }); } +// handle case completion +const recordCaseActivityComplete = async function ( + resourceVersionId: number, + nodePathId: number, + activityStart: Date, + activityEnd: Date, + launchResourceActivityId: number +): Promise { + return await recordActivity( + resourceVersionId, + nodePathId, + activityStart, + activityEnd, + ActivityStatus.Completed, + launchResourceActivityId + ); +}; + export const activityRecorder = { recordActivityLaunched, recordActivity, @@ -222,5 +247,6 @@ export const activityRecorder = { recordMediaResourceActivityInteraction, recordActivityAndInteractionTogether, recordAssessmentResourceActivity, - recordAssessmentResourceActivityInteraction + recordAssessmentResourceActivityInteraction, + recordCaseActivityComplete }; diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue index a7c20515c..86185c557 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue @@ -116,6 +116,7 @@ props: { resourceItem: { type: Object } as PropOptions, resourceActivityId: { type: Number } as PropOptions, + activityStart: { type: Date }, assessmentProgress: { type: Object } as PropOptions, keepUserSessionAliveIntervalSeconds: { type: Number } as PropOptions, }, @@ -234,10 +235,24 @@ this.questionsInFocus = [...this.questionsInFocus]; this.selectedQuestionValues = [...this.selectedQuestionValues]; }, - updateProgress(page: number, isCompleted: boolean) { + async updateProgress(page: number, isCompleted: boolean) { if (isCompleted) { this.pagesProgress.completePage(page); } + if (this.resourceItem.resourceTypeEnum === ResourceType.CASE && + isCompleted && + page === this.pageCount - 1 && + this.resourceActivityId > 0) { + + await activityRecorder.recordCaseActivityComplete( + this.resourceItem.resourceVersionId, + this.resourceItem.nodePathId, + this.activityStart, + new Date(), + this.resourceActivityId + ); + } + if (this.allPagesCompleted && this.isAssessment) { this.allAssessmentInteractionsSubmitted = true; } @@ -307,7 +322,7 @@ // Only make a new activity if the latest activity is finished if (typeof latest.userScore === 'number') { - const result = await activityRecorder.recordActivityLaunched(this.resourceItem.resourceTypeEnum, this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, new Date(), reason); + const result = await activityRecorder.recordActivityLaunched(this.resourceItem.resourceTypeEnum, this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, new Date() as Date, false, reason); this.shuffleMatchQuestionsState(); await activityRecorder.recordAssessmentResourceActivity(result.createdId, this.matchQuestionsState, reason); } diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/ResourceContent.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/ResourceContent.vue index 1f966cfac..cba006b24 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/ResourceContent.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/ResourceContent.vue @@ -72,7 +72,7 @@
    - +
    @@ -102,6 +102,7 @@ import { MKPlayer } from '@mediakind/mkplayer'; import { MKPlayerType, MKStreamType } from '../MKPlayerConfigEnum'; import { MKPlayerControlbar } from '../mkioplayer-controlbar'; + import { BlockCollectionModel } from '../models/contribute-resource/blocks/blockCollectionModel'; Vue.use(Vuelidate as any); @@ -122,6 +123,7 @@ ResourceAccessibility: ResourceAccessibility, launchedResourceActivityId: 0, mediaResourceActivityId: 0, + activityStart: null, activityLogged: false, activityEndLogged: false, // Applies to media only. mediaResourceActivityLogged: false, @@ -410,10 +412,16 @@ const userAgent = navigator.userAgent || navigator.vendor; this.isIphone = /iPhone/i.test(userAgent); }, - initialise(): void { + async initialise(): Promise { // record activity on page created for resource article if (this.userAuthenticated && this.resourceItem.resourceTypeEnum === ResourceType.CASE) { - this.recordActivityLaunched(); + let isMultiPageCase = false; + if (this.resourceItem.resourceTypeEnum === ResourceType.CASE && this.resourceItem.caseDetails) { + const blockCollection = new BlockCollectionModel(this.resourceItem.caseDetails.blockCollection); + isMultiPageCase = blockCollection.getPages().length > 1; + } + + await this.recordActivityLaunched(isMultiPageCase); } else if (this.userAuthenticated && this.resourceItem.resourceTypeEnum === ResourceType.ASSESSMENT) { this.getCurrentAssessmentActivity(); @@ -456,11 +464,12 @@ hasResourceAccess(): boolean { return this.userAuthenticated && (!(this.isGeneralUser && this.resourceItem.resourceAccessibilityEnum == this.ResourceAccessibility.FullAccess)) }, - async recordActivityLaunched(): Promise { + async recordActivityLaunched(isMultiPageCase: boolean = false): Promise { if (!this.activityLogged) { this.activityLogged = true; - await activityRecorder.recordActivityLaunched(this.resourceItem.resourceTypeEnum, this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, new Date()) + this.activityStart = new Date(); + await activityRecorder.recordActivityLaunched(this.resourceItem.resourceTypeEnum, this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, this.activityStart,isMultiPageCase) // await activityRecorder.recordActivityLaunched(this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, new Date()) .then(response => { this.launchedResourceActivityId = response.createdId; From 214d0f5e5a674e83ae0770fd3f02ac1ed6597765 Mon Sep 17 00:00:00 2001 From: Tobi Awe Date: Mon, 23 Feb 2026 21:15:33 +0000 Subject: [PATCH 090/106] update --- .../vuesrc/resource/CaseOrAssessmentResource.vue | 12 ++++++++++-- LearningHub.Nhs.WebUI/package-lock.json | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue index 86185c557..ec011ffb6 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue @@ -244,10 +244,12 @@ page === this.pageCount - 1 && this.resourceActivityId > 0) { + let startDate = new Date(this.activityStart as any); + await activityRecorder.recordCaseActivityComplete( this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, - this.activityStart, + startDate, new Date(), this.resourceActivityId ); @@ -322,7 +324,13 @@ // Only make a new activity if the latest activity is finished if (typeof latest.userScore === 'number') { - const result = await activityRecorder.recordActivityLaunched(this.resourceItem.resourceTypeEnum, this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, new Date() as Date, false, reason); + const result = await activityRecorder.recordActivityLaunched( + this.resourceItem.resourceTypeEnum, + this.resourceItem.resourceVersionId, + this.resourceItem.nodePathId, + new Date(), + false as boolean + ); this.shuffleMatchQuestionsState(); await activityRecorder.recordAssessmentResourceActivity(result.createdId, this.matchQuestionsState, reason); } diff --git a/LearningHub.Nhs.WebUI/package-lock.json b/LearningHub.Nhs.WebUI/package-lock.json index 1b09c4094..d6ecd758e 100644 --- a/LearningHub.Nhs.WebUI/package-lock.json +++ b/LearningHub.Nhs.WebUI/package-lock.json @@ -36,7 +36,7 @@ "vue-carousel-3d": "^1.0.1", "vue-clamp": "0.4.1", "vue-click-outside": "1.1.0", - "vue-ctk-date-time-picker": "^2.5.0", + "vue-ctk-date-time-picker": "2.5.0", "vue-router": "^3.6.5", "vue-simple-progress": "^1.1.1", "vue-typeahead": "^2.3.2", From 5b3808d5099f6be4cf129955d27adc83e95d93a2 Mon Sep 17 00:00:00 2001 From: AnjuJose011 <154979799+AnjuJose011@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:44:15 +0000 Subject: [PATCH 091/106] fixes-6889 --- .../Views/Search/_ResourceFilter.cshtml | 45 ++-------------- .../Views/Search/_SearchFilter.cshtml | 54 +++---------------- 2 files changed, 12 insertions(+), 87 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Views/Search/_ResourceFilter.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_ResourceFilter.cshtml index b8bbc8807..51fc9223e 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_ResourceFilter.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_ResourceFilter.cshtml @@ -4,12 +4,11 @@ @{ var resourceResult = Model.ResourceSearchResult; var filtersApplied = resourceResult.SortItemSelected.Value != string.Empty - || resourceResult.SearchFilters.Any(f => f.Selected) || resourceResult.SearchResourceAccessLevelFilters.Any(f => f.Selected) - || resourceResult.SearchProviderFilters.Any(f =>f.Selected); + || resourceResult.SearchFilters.Any(f => f.Selected) || resourceResult.SearchResourceAccessLevelFilters.Any(f => f.Selected); var queryParams = QueryHelpers.ParseQuery(Context.Request.QueryString.ToString().ToLower()); queryParams["actiontype"] = "sort-filter"; - var pageUrl = Model.CatalogueId > 0 ? "/catalogue/" + Model.CatalogueUrl +"/search" : "/search/results"; + var pageUrl = Model.CatalogueId > 0 ? "/catalogue/" + Model.CatalogueUrl + "/search" : "/search/results"; var actionUrl = QueryHelpers.AddQueryString(pageUrl, queryParams); var pageFragment = "#search-filters"; @@ -25,17 +24,10 @@ .Select(f => $"{char.ToUpper(f.DisplayName[0])}{f.DisplayName[1..]}"); if (resourceAccessLevelFilters.Any()) - { + { summary += $" and Filtered by Audience access level {string.Join(" ", resourceAccessLevelFilters)}"; } - - - var providerfilters = resourceResult.SearchProviderFilters.Where(f => f.Selected).Select(f => $"{f.DisplayName}"); - if (providerfilters.Any()) - { - summary += $" and Filtered by Provider {string.Join(" ", providerfilters)}"; - } if (filters.Any()) { @@ -128,7 +120,7 @@
    + value="@filter.Value" checked="@filter.Selected" class="@(filter.Count > 0 ? "" : "disabled")"> @@ -140,35 +132,6 @@
    } - @if (resourceResult.SearchProviderFilters.Count > 0) - { -
    - -
    -
    - -

    Filter by provider:

    -
    - -
    - @foreach (var filter in resourceResult.SearchProviderFilters) - { -
    - -
    - - -
    -
    - } -
    -
    -
    - } -
    diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml index 592f2620f..03a1518a9 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchFilter.cshtml @@ -4,8 +4,7 @@ @{ var resourceResult = Model.ResourceSearchResult; var filtersApplied = resourceResult.SortItemSelected.Value != string.Empty - || resourceResult.SearchFilters.Any(f => f.Selected) || resourceResult.SearchResourceAccessLevelFilters.Any(f => f.Selected) - || resourceResult.SearchProviderFilters.Any(f => f.Selected); + || resourceResult.SearchFilters.Any(f => f.Selected) || resourceResult.SearchResourceAccessLevelFilters.Any(f => f.Selected); var queryParams = QueryHelpers.ParseQuery(Context.Request.QueryString.ToString().ToLower()); queryParams["actiontype"] = "sort-filter"; @@ -29,14 +28,6 @@ summary += $" and Filtered by Audience access level {string.Join(" ", resourceAccessLevelFilters)}"; } - - var providerfilters = resourceResult.SearchProviderFilters.Where(f => f.Selected).Select(f => $"{f.DisplayName}"); - - if (providerfilters.Any()) - { - summary += $" and Filtered by Provider {string.Join(" ", providerfilters)}"; - } - if (filters.Any()) { summary += $" and Filtered by Type {string.Join(" ", filters)}"; @@ -73,12 +64,12 @@
    @foreach (var option in new[] - { - new { Id = "all", Label = "All" }, - new { Id = "catalogue", Label = "Catalogues" }, - new { Id = "course", Label = "Courses" }, - new { Id = "resource", Label = "Learning Resources" } - }) + { + new { Id = "all", Label = "All" }, + new { Id = "catalogue", Label = "Catalogues" }, + new { Id = "course", Label = "Courses" }, + new { Id = "resource", Label = "Learning Resources" } + }) { // Look up count from your real model var filter = Model.ResourceCollectionFilter @@ -95,7 +86,7 @@ type="checkbox" value="@option.Id" checked="@(selectedResourceCollections.Contains(option.Id))" - @(count == 0 ? "disabled" : "") + @(count == 0 ? "disabled" : "") onchange="this.form.submit()" />
    /// The search text. /// The facet results. + /// The resource access level filter, if any, used to further differentiate cache entries. /// Cancellation token. /// The unfiltered facet results. private async Task>> GetUnfilteredFacetsAsync( string searchText, IDictionary> facets, + string? resourceAccessLevel, CancellationToken cancellationToken) { - var cacheKey = $"AllFacets_{searchText?.ToLowerInvariant() ?? "*"}"; + var normalizedSearch = searchText?.ToLowerInvariant() ?? "*"; + var accessLevelKey = resourceAccessLevel?.ToString() ?? "null"; + var cacheKey = $"AllFacets_{normalizedSearch}_ral_{accessLevelKey}"; + var cacheResponse = await this.cachingService.GetAsync>>(cacheKey); if (cacheResponse.ResponseEnum == CacheReadResponseEnum.Found) @@ -885,6 +891,24 @@ private async Task>> GetUnfilteredFacetsA return facets ?? new Dictionary>(); } + private static int? ExtractResourceAccessLevel(IDictionary> facets) + { + if (facets == null) + return null; + + if (!facets.TryGetValue("resource_access_level", out var accessFacet)) + return null; + + if (accessFacet == null || accessFacet.Count == 0) + return null; + + var rawValue = accessFacet.FirstOrDefault()?.Value; + + var stringValue = rawValue?.ToString(); + + return int.TryParse(stringValue, out var parsed) ? parsed : (int?)null; + } + private ResourceMetadataViewModel MapToViewModel(Resource resource, List resourceActivities) { var hasCurrentResourceVersion = resource.CurrentResourceVersion != null; From dfcccf2d9349a975a84b83c55aee0625191590a5 Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 2 Mar 2026 16:25:45 +0000 Subject: [PATCH 096/106] Tidied up some code --- .../Views/Search/_SearchResult.cshtml | 6 ++---- .../AzureSearch/SearchDocument.cs | 2 +- .../Services/AzureSearch/AzureSearchService.cs | 18 ------------------ 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml index 9c2d9be51..842884c10 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml @@ -16,6 +16,8 @@ var index = pagingModel.CurrentPage * pagingModel.PageSize; var searchString = HttpUtility.UrlEncode(Model.SearchString); string groupId = HttpUtility.UrlEncode(Model.GroupId.ToString()); + string[] formats = { "dd/MM/yyyy HH:mm:ss", "dd/MM/yyyy", "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd","dd MMM yyyy HH:mm:ss","dd MMM yyyy" }; string GetUrl(int resourceReferenceId, int itemIndex, int nodePathId, SearchClickPayloadModel payload) { @@ -144,10 +146,6 @@ { string displayDate = item.AuthoredDate; { - // Define expected input formats - string[] formats = { "dd/MM/yyyy HH:mm:ss", "dd/MM/yyyy", "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd","dd MMM yyyy HH:mm:ss","dd MMM yyyy" }; - if (DateTime.TryParseExact( item.AuthoredDate, formats, diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs index 225d46a93..a59bdf9f2 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs @@ -97,7 +97,7 @@ public string Description /// Gets or sets the resource access level. ///
    [JsonPropertyName("resource_access_level")] - public string? ResourceAccessLevel { get; set; } = string.Empty; + public int? ResourceAccessLevel { get; set; } /// /// Gets or sets the date authored. diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 62a607407..b8efa8fc0 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -891,24 +891,6 @@ private async Task>> GetUnfilteredFacetsA return facets ?? new Dictionary>(); } - private static int? ExtractResourceAccessLevel(IDictionary> facets) - { - if (facets == null) - return null; - - if (!facets.TryGetValue("resource_access_level", out var accessFacet)) - return null; - - if (accessFacet == null || accessFacet.Count == 0) - return null; - - var rawValue = accessFacet.FirstOrDefault()?.Value; - - var stringValue = rawValue?.ToString(); - - return int.TryParse(stringValue, out var parsed) ? parsed : (int?)null; - } - private ResourceMetadataViewModel MapToViewModel(Resource resource, List resourceActivities) { var hasCurrentResourceVersion = resource.CurrentResourceVersion != null; From 6da78ce154665a4a459635c9cc2d3b471396298e Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 2 Mar 2026 17:09:06 +0000 Subject: [PATCH 097/106] Refactored the code --- .../ServiceModels/AzureSearch/SearchDocument.cs | 2 +- .../Services/AzureSearch/AzureSearchService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs index a59bdf9f2..37b1b52d2 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ServiceModels/AzureSearch/SearchDocument.cs @@ -97,7 +97,7 @@ public string Description /// Gets or sets the resource access level. /// [JsonPropertyName("resource_access_level")] - public int? ResourceAccessLevel { get; set; } + public string? ResourceAccessLevel { get; set; } /// /// Gets or sets the date authored. diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index b8efa8fc0..904eed11c 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -860,7 +860,7 @@ private string MapToResourceType(string resourceType) /// /// The search text. /// The facet results. - /// The resource access level filter, if any, used to further differentiate cache entries. + /// The resource access level filter, if any, used to further differentiate cache entries. /// Cancellation token. /// The unfiltered facet results. private async Task>> GetUnfilteredFacetsAsync( From d359e6abc47c139370a98512e35b4f837771603d Mon Sep 17 00:00:00 2001 From: Binon Date: Tue, 3 Mar 2026 09:45:58 +0000 Subject: [PATCH 098/106] refactore authored date time --- .../Helpers/UtilityHelper.cs | 45 +++++++++++++++++++ .../Views/Search/_SearchResult.cshtml | 18 ++------ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs index 5a2bc5c68..1c2970a96 100644 --- a/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs @@ -385,6 +385,51 @@ public static string GetAuthoredDate(int? day, int? month, int? year) return authoredDate; } + /// + /// Formats an authored date string into a human-readable date if possible. + /// + /// If the input does not match any of the supported date formats, the method returns the + /// original input string unchanged. This method does not validate whether the date is a valid calendar date + /// beyond format matching. + /// The date string to format. Must be in one of the supported date formats, such as "yyyy-MM-dd", "dd/MM/yyyy", + /// "MM/dd/yyyy", "yyyyMMdd", "yyyy-MM-ddTHH:mm:ss", or "yyyy-MM-ddTHH:mm:ssZ". If null, empty, or whitespace, + /// an empty string is returned. + /// A formatted date string in the form "dd MMM yyyy" if parsing succeeds; otherwise, the original input string + /// or an empty string if the input is null, empty, or whitespace. + public static string GetFormattedAuthoredDate(string authoredDate) + { + string[] dateFormats = + { + "dd/MM/yyyy", + "dd/MM/yyyy HH:mm:ss", + "yyyy-MM-dd", + "yyyy-MM-dd HH:mm:ss", + "MM/dd/yyyy", + "MM/dd/yyyy HH:mm:ss", + "yyyyMMdd", + "yyyyMMdd HH:mm:ss", + }; + + if (string.IsNullOrWhiteSpace(authoredDate)) + { + return string.Empty; + } + + string displayDate = authoredDate; + + if (DateTime.TryParseExact( + authoredDate, + dateFormats, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out DateTime parsedDate)) + { + displayDate = parsedDate.ToString("dd MMM yyyy"); + } + + return displayDate; + } + /// /// Gets the text to display on a generic file download button according to the file extension. /// diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml index 842884c10..1c43d33c6 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml @@ -144,22 +144,10 @@ @if (!string.IsNullOrWhiteSpace(item.AuthoredDate)) { - string displayDate = item.AuthoredDate; - { - if (DateTime.TryParseExact( - item.AuthoredDate, - formats, - System.Globalization.CultureInfo.InvariantCulture, - System.Globalization.DateTimeStyles.None, - out DateTime parsedDate)) - { - displayDate = parsedDate.ToString("dd MMM yyyy"); // consistent format - } - } - + string formattedAuthoredDate = @UtilityHelper.GetFormattedAuthoredDate(item.AuthoredDate); @* Render helper text plus formatted date *@ - @UtilityHelper.GetInOn(displayDate) - @: @displayDate + @UtilityHelper.GetInOn(formattedAuthoredDate) + @: @formattedAuthoredDate }
    From 4d856c4ff3fde9ba03514e78c455891a5a251365 Mon Sep 17 00:00:00 2001 From: Binon Date: Tue, 3 Mar 2026 09:49:08 +0000 Subject: [PATCH 099/106] Deleted unwanted code --- LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml index 1c43d33c6..e251ebca8 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchResult.cshtml @@ -15,9 +15,7 @@ var pagingModel = Model.ResourceResultPaging; var index = pagingModel.CurrentPage * pagingModel.PageSize; var searchString = HttpUtility.UrlEncode(Model.SearchString); - string groupId = HttpUtility.UrlEncode(Model.GroupId.ToString()); - string[] formats = { "dd/MM/yyyy HH:mm:ss", "dd/MM/yyyy", "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd","dd MMM yyyy HH:mm:ss","dd MMM yyyy" }; + string groupId = HttpUtility.UrlEncode(Model.GroupId.ToString()); string GetUrl(int resourceReferenceId, int itemIndex, int nodePathId, SearchClickPayloadModel payload) { From 0be3ae5a1fe49affee744955c6e64eb49ac45f39 Mon Sep 17 00:00:00 2001 From: swapnamol-abraham Date: Tue, 3 Mar 2026 11:08:01 +0000 Subject: [PATCH 100/106] TD-6914:fixed the alignment of Catalogue tag showing on 'Catalogue' search results # Removed and replaced inline styles with nhsuk design system --- .../Styles/nhsuk/layout.scss | 12 ++++++++++- .../Search/_SearchCatalogueResult.cshtml | 21 ++----------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss index caf5555a6..1e4cfee04 100644 --- a/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss +++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/layout.scss @@ -338,7 +338,17 @@ li.autosuggestion-option:last-of-type { } - +.search-catalogue-badge { + position: absolute; + top: 0; + right: 0; + background-color: #0056b3; + color: white; + padding: 5px 15px; + border-top-right-radius: 8px; + border-bottom-left-radius: 8px; + font-size: 0.8em; +} .side-nav__item:last-child { border-bottom: none; diff --git a/LearningHub.Nhs.WebUI/Views/Search/_SearchCatalogueResult.cshtml b/LearningHub.Nhs.WebUI/Views/Search/_SearchCatalogueResult.cshtml index 64dc2e7b9..86a7784c1 100644 --- a/LearningHub.Nhs.WebUI/Views/Search/_SearchCatalogueResult.cshtml +++ b/LearningHub.Nhs.WebUI/Views/Search/_SearchCatalogueResult.cshtml @@ -24,30 +24,13 @@ &query={searchSignalQueryEncoded}&name={payload?.DocumentFields?.Name}"; return url; } - // var catalogueResult = Model.ResourceSearchResult; - // var pagingModel = Model.CatalogueResultPaging; - // var searchString = HttpUtility.UrlEncode(Model.SearchString); - // var suggestedSearchString = Model.DidYouMeanEnabled ? HttpUtility.UrlEncode(Model.SuggestedCatalogue) : 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={suggestedSearchString}&catalogueId={catalogueId} - // &GroupId={groupId}&searchId={searchSignal?.SearchId}&timeOfSearch={searchSignal?.TimeOfSearch}&userQuery={HttpUtility.UrlEncode(searchSignal?.UserQuery)} - // &query={searchSignalQueryEncoded}&name={payload?.DocumentFields?.Name}"; - // return url; }
    -
    +
    -
    Catalogue
    +
    Catalogue

    @item.Title From dfa33bcab0314cbb8f4fa8c814035d55643f89d5 Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 5 Mar 2026 10:52:42 +0000 Subject: [PATCH 101/106] Cache causing filter issue, sorted it now --- .../Services/AzureSearch/AzureSearchService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 904eed11c..572c4d8ae 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -875,7 +875,7 @@ private async Task>> GetUnfilteredFacetsA var cacheResponse = await this.cachingService.GetAsync>>(cacheKey); - if (cacheResponse.ResponseEnum == CacheReadResponseEnum.Found) + if (cacheResponse.ResponseEnum == CacheReadResponseEnum.Found && string.IsNullOrEmpty(accessLevelKey)) { // Convert cached DTO back to FacetResult dictionary return AzureSearchFacetHelper.ConvertFromCacheable(cacheResponse.Item); From 83caf8c5b08b33e56fc006b2de1c00c83414513d Mon Sep 17 00:00:00 2001 From: AnjuJose011 <154979799+AnjuJose011@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:13:02 +0000 Subject: [PATCH 102/106] Revert "TD-6899 Refactor of Case Completion status" --- .../Scripts/vuesrc/activity.ts | 34 +++---------------- .../resource/CaseOrAssessmentResource.vue | 27 ++------------- .../vuesrc/resource/ResourceContent.vue | 19 +++-------- LearningHub.Nhs.WebUI/package-lock.json | 2 +- 4 files changed, 12 insertions(+), 70 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/activity.ts b/LearningHub.Nhs.WebUI/Scripts/vuesrc/activity.ts index 91fd70738..bc5a08bbb 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/activity.ts +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/activity.ts @@ -10,18 +10,13 @@ const recordActivityLaunched = async function ( resourceVersionId: number, nodePathId: number, activityDatetime: Date, - isMultiPageCase: boolean = false, extraAttemptReason?: string): Promise { var data = { resourceVersionId: resourceVersionId, nodePathId: nodePathId, - activityStatus: (resourceType == ResourceType.ASSESSMENT || - resourceType == ResourceType.VIDEO || - resourceType == ResourceType.AUDIO || - resourceType == ResourceType.SCORM || - (resourceType == ResourceType.CASE && isMultiPageCase) - ) ? ActivityStatus.Incomplete : ActivityStatus.Completed, + activityStatus: (resourceType == ResourceType.ASSESSMENT || resourceType == ResourceType.VIDEO || resourceType == ResourceType.AUDIO || + resourceType == ResourceType.SCORM) ? ActivityStatus.Incomplete : ActivityStatus.Completed, activityStart: activityDatetime, extraAttemptReason }; @@ -40,10 +35,8 @@ const recordActivityLaunched = async function ( console.log('recordActivityLaunched:' + e); throw e; }); -}; - - +}; const recordActivity = async function ( resourceVersionId: number, nodePathId: number, @@ -222,24 +215,6 @@ const recordAssessmentResourceActivityInteraction = async function ( }); } -// handle case completion -const recordCaseActivityComplete = async function ( - resourceVersionId: number, - nodePathId: number, - activityStart: Date, - activityEnd: Date, - launchResourceActivityId: number -): Promise { - return await recordActivity( - resourceVersionId, - nodePathId, - activityStart, - activityEnd, - ActivityStatus.Completed, - launchResourceActivityId - ); -}; - export const activityRecorder = { recordActivityLaunched, recordActivity, @@ -247,6 +222,5 @@ export const activityRecorder = { recordMediaResourceActivityInteraction, recordActivityAndInteractionTogether, recordAssessmentResourceActivity, - recordAssessmentResourceActivityInteraction, - recordCaseActivityComplete + recordAssessmentResourceActivityInteraction }; diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue index ec011ffb6..a7c20515c 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/CaseOrAssessmentResource.vue @@ -116,7 +116,6 @@ props: { resourceItem: { type: Object } as PropOptions, resourceActivityId: { type: Number } as PropOptions, - activityStart: { type: Date }, assessmentProgress: { type: Object } as PropOptions, keepUserSessionAliveIntervalSeconds: { type: Number } as PropOptions, }, @@ -235,26 +234,10 @@ this.questionsInFocus = [...this.questionsInFocus]; this.selectedQuestionValues = [...this.selectedQuestionValues]; }, - async updateProgress(page: number, isCompleted: boolean) { + updateProgress(page: number, isCompleted: boolean) { if (isCompleted) { this.pagesProgress.completePage(page); } - if (this.resourceItem.resourceTypeEnum === ResourceType.CASE && - isCompleted && - page === this.pageCount - 1 && - this.resourceActivityId > 0) { - - let startDate = new Date(this.activityStart as any); - - await activityRecorder.recordCaseActivityComplete( - this.resourceItem.resourceVersionId, - this.resourceItem.nodePathId, - startDate, - new Date(), - this.resourceActivityId - ); - } - if (this.allPagesCompleted && this.isAssessment) { this.allAssessmentInteractionsSubmitted = true; } @@ -324,13 +307,7 @@ // Only make a new activity if the latest activity is finished if (typeof latest.userScore === 'number') { - const result = await activityRecorder.recordActivityLaunched( - this.resourceItem.resourceTypeEnum, - this.resourceItem.resourceVersionId, - this.resourceItem.nodePathId, - new Date(), - false as boolean - ); + const result = await activityRecorder.recordActivityLaunched(this.resourceItem.resourceTypeEnum, this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, new Date(), reason); this.shuffleMatchQuestionsState(); await activityRecorder.recordAssessmentResourceActivity(result.createdId, this.matchQuestionsState, reason); } diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/ResourceContent.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/ResourceContent.vue index cba006b24..1f966cfac 100644 --- a/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/ResourceContent.vue +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/resource/ResourceContent.vue @@ -72,7 +72,7 @@
    - +
    @@ -102,7 +102,6 @@ import { MKPlayer } from '@mediakind/mkplayer'; import { MKPlayerType, MKStreamType } from '../MKPlayerConfigEnum'; import { MKPlayerControlbar } from '../mkioplayer-controlbar'; - import { BlockCollectionModel } from '../models/contribute-resource/blocks/blockCollectionModel'; Vue.use(Vuelidate as any); @@ -123,7 +122,6 @@ ResourceAccessibility: ResourceAccessibility, launchedResourceActivityId: 0, mediaResourceActivityId: 0, - activityStart: null, activityLogged: false, activityEndLogged: false, // Applies to media only. mediaResourceActivityLogged: false, @@ -412,16 +410,10 @@ const userAgent = navigator.userAgent || navigator.vendor; this.isIphone = /iPhone/i.test(userAgent); }, - async initialise(): Promise { + initialise(): void { // record activity on page created for resource article if (this.userAuthenticated && this.resourceItem.resourceTypeEnum === ResourceType.CASE) { - let isMultiPageCase = false; - if (this.resourceItem.resourceTypeEnum === ResourceType.CASE && this.resourceItem.caseDetails) { - const blockCollection = new BlockCollectionModel(this.resourceItem.caseDetails.blockCollection); - isMultiPageCase = blockCollection.getPages().length > 1; - } - - await this.recordActivityLaunched(isMultiPageCase); + this.recordActivityLaunched(); } else if (this.userAuthenticated && this.resourceItem.resourceTypeEnum === ResourceType.ASSESSMENT) { this.getCurrentAssessmentActivity(); @@ -464,12 +456,11 @@ hasResourceAccess(): boolean { return this.userAuthenticated && (!(this.isGeneralUser && this.resourceItem.resourceAccessibilityEnum == this.ResourceAccessibility.FullAccess)) }, - async recordActivityLaunched(isMultiPageCase: boolean = false): Promise { + async recordActivityLaunched(): Promise { if (!this.activityLogged) { this.activityLogged = true; - this.activityStart = new Date(); - await activityRecorder.recordActivityLaunched(this.resourceItem.resourceTypeEnum, this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, this.activityStart,isMultiPageCase) + await activityRecorder.recordActivityLaunched(this.resourceItem.resourceTypeEnum, this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, new Date()) // await activityRecorder.recordActivityLaunched(this.resourceItem.resourceVersionId, this.resourceItem.nodePathId, new Date()) .then(response => { this.launchedResourceActivityId = response.createdId; diff --git a/LearningHub.Nhs.WebUI/package-lock.json b/LearningHub.Nhs.WebUI/package-lock.json index d6ecd758e..1b09c4094 100644 --- a/LearningHub.Nhs.WebUI/package-lock.json +++ b/LearningHub.Nhs.WebUI/package-lock.json @@ -36,7 +36,7 @@ "vue-carousel-3d": "^1.0.1", "vue-clamp": "0.4.1", "vue-click-outside": "1.1.0", - "vue-ctk-date-time-picker": "2.5.0", + "vue-ctk-date-time-picker": "^2.5.0", "vue-router": "^3.6.5", "vue-simple-progress": "^1.1.1", "vue-typeahead": "^2.3.2", From bb8cabc5c148d4808178b3fde389ebc92e9083e3 Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 5 Mar 2026 15:34:43 +0000 Subject: [PATCH 103/106] Added new parameter -resource access level for generic user --- .../LearningHub.Nhs.Database/Views/SupersetSearchView.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebAPI/LearningHub.Nhs.Database/Views/SupersetSearchView.sql b/WebAPI/LearningHub.Nhs.Database/Views/SupersetSearchView.sql index 2861f55c2..f99e0c8e7 100644 --- a/WebAPI/LearningHub.Nhs.Database/Views/SupersetSearchView.sql +++ b/WebAPI/LearningHub.Nhs.Database/Views/SupersetSearchView.sql @@ -6,6 +6,7 @@ -- Modification History -- TD-6212 - https://hee-tis.atlassian.net/browse/TD-6212 -- 06-02-2026 Binon Yesudhas Initial Revision +-- 05-03-2026 Binon Yesudhas Added new parameter, ResourceAccessLevel to the view ------------------------------------------------------------------------------- CREATE VIEW [dbo].[SupersetSearchView] AS @@ -21,6 +22,7 @@ SELECT 'catalogue' AS resource_collection, c.Keywords as manual_tag, -- e.g., comma-separated or JSON if multiple 'catalogue' AS resource_type, + NULL as resource_access_level, NULL AS publication_date, NULL AS date_authored, NULL AS rating, @@ -50,6 +52,7 @@ SELECT 'resource' AS resource_collection, r.Keywords AS manual_tag, r.ContentType AS resource_type, + r.ResourceAccessLevel as resource_access_level, r.PublicationDate AS publication_date, r.AuthoredDate AS date_authored, CAST(r.AverageRating AS FLOAT) AS rating, @@ -68,7 +71,4 @@ FROM dbo.SearchResourcesView r; GO; ------------------------------------------------------------------------------------------------------------------ - - From e41c32d72bf5b0e9adeefb30ecd6468fc9ada565 Mon Sep 17 00:00:00 2001 From: Binon Date: Wed, 11 Mar 2026 12:02:06 +0000 Subject: [PATCH 104/106] Fixed the issue with filter combos --- .../Helpers/UtilityHelper.cs | 17 +++--- .../Helpers/Search/AzureSearchFacetHelper.cs | 58 ++++++++++++++----- .../AzureSearch/AzureSearchService.cs | 6 +- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs index 1c2970a96..4c444cbcf 100644 --- a/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs +++ b/LearningHub.Nhs.WebUI/Helpers/UtilityHelper.cs @@ -408,6 +408,9 @@ public static string GetFormattedAuthoredDate(string authoredDate) "MM/dd/yyyy HH:mm:ss", "yyyyMMdd", "yyyyMMdd HH:mm:ss", + "M/d/yyyy", + "M/d/yyyy h:mm:ss tt", + "M/d/yyyy h:mm tt", }; if (string.IsNullOrWhiteSpace(authoredDate)) @@ -415,19 +418,13 @@ public static string GetFormattedAuthoredDate(string authoredDate) return string.Empty; } - string displayDate = authoredDate; - - if (DateTime.TryParseExact( - authoredDate, - dateFormats, - CultureInfo.InvariantCulture, - DateTimeStyles.None, - out DateTime parsedDate)) + if (DateTime.TryParse(authoredDate, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var date) || + DateTime.TryParseExact(authoredDate, dateFormats, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out date)) { - displayDate = parsedDate.ToString("dd MMM yyyy"); + return date.ToString("dd MMM yyyy"); } - return displayDate; + return authoredDate; } /// diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/AzureSearchFacetHelper.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/AzureSearchFacetHelper.cs index 6818b0ecb..066c6aae3 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/AzureSearchFacetHelper.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/AzureSearchFacetHelper.cs @@ -115,26 +115,54 @@ public static Facet[] MergeFacets( ? filteredFacets[facetKey].ToDictionary(f => f.Value?.ToString()?.ToLower() ?? "", f => (int)f.Count) : new Dictionary(); - facets[index++] = new Facet + var filters = facetGroup.Value.Select(f => { - Id = facetKey, - Filters = facetGroup.Value.Select(f => + var displayName = f.Value?.ToString()?.ToLower() ?? ""; + var isSelected = appliedValues.Any(av => av.Equals(f.Value?.ToString(), StringComparison.OrdinalIgnoreCase)); + + // Default to the unfiltered count. This is used when: + // - no filtered search has been performed, OR + // - the filter value is selected (keep count so the option remains visible for deselection), OR + // - this facet group itself has an applied filter (multi-select pattern: a group's own + // counts are not narrowed down by its own selections so the user can still pick other values). + // Only update counts using the filtered results when a filter from a DIFFERENT group + // is driving the narrowing of this group. + var count = (int)f.Count; + if (!isSelected && filteredFacets != null && !hasAppliedFilter) + { + count = filteredFacetValues.TryGetValue(displayName, out var filteredCount) ? filteredCount : 0; + } + + return new Filter { - var displayName = f.Value?.ToString()?.ToLower() ?? ""; - var isSelected = appliedValues.Any(av => av.Equals(f.Value?.ToString(), StringComparison.OrdinalIgnoreCase)); + DisplayName = displayName, + Count = count, + Selected = isSelected + }; + }).ToList(); - // Use filtered count if available and filter is not selected, otherwise use unfiltered count - var count = !isSelected && filteredFacetValues.ContainsKey(displayName) - ? filteredFacetValues[displayName] - : (int)f.Count; + // Ensure all selected filter values remain visible even if absent from the unfiltered + // baseline (e.g. a resource selected under a resource-level filter that yields no results). + foreach (var selectedValue in appliedValues) + { + var alreadyPresent = filters.Any(f => + f.DisplayName.Equals(selectedValue, StringComparison.OrdinalIgnoreCase)); - return new Filter + if (!alreadyPresent) + { + filters.Add(new Filter { - DisplayName = displayName, - Count = count, - Selected = isSelected - }; - }).ToArray() + DisplayName = selectedValue.ToLower(), + Count = 0, + Selected = true, + }); + } + } + + facets[index++] = new Facet + { + Id = facetKey, + Filters = filters.ToArray() }; } diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs index 572c4d8ae..25a98541e 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/AzureSearch/AzureSearchService.cs @@ -866,12 +866,14 @@ private string MapToResourceType(string resourceType) private async Task>> GetUnfilteredFacetsAsync( string searchText, IDictionary> facets, - string? resourceAccessLevel, + string? resourceAccessLevel, CancellationToken cancellationToken) { var normalizedSearch = searchText?.ToLowerInvariant() ?? "*"; var accessLevelKey = resourceAccessLevel?.ToString() ?? "null"; - var cacheKey = $"AllFacets_{normalizedSearch}_ral_{accessLevelKey}"; + var cacheKey = $"Facets_{normalizedSearch}"; + if (!string.IsNullOrWhiteSpace(accessLevelKey)) + cacheKey += $"_{accessLevelKey.Replace("=", "_")}"; var cacheResponse = await this.cachingService.GetAsync>>(cacheKey); From 724a94fc8c200f87c19b0edb26c429c5e2271650 Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 12 Mar 2026 09:37:24 +0000 Subject: [PATCH 105/106] Fixed the facets are limited to 10 --- .../Helpers/Search/SearchOptionsBuilder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs index bad252c1d..40686cd46 100644 --- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs +++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Helpers/Search/SearchOptionsBuilder.cs @@ -171,7 +171,10 @@ public static SearchOptions BuildSearchOptions( { foreach (var facet in config.FacetFields) { - searchOptions.Facets.Add(facet); + var facetValue = facet.Contains("count:") + ? facet: $"{facet},count:20"; + + searchOptions.Facets.Add(facetValue); } } From 29aad9afd98d1940a898e30712141d00f63f3bf6 Mon Sep 17 00:00:00 2001 From: Binon Date: Tue, 17 Mar 2026 08:55:08 +0000 Subject: [PATCH 106/106] Handle inconsistenrt resource type --- .../LearningHub.Nhs.Database/Views/SearchResourcesView.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WebAPI/LearningHub.Nhs.Database/Views/SearchResourcesView.sql b/WebAPI/LearningHub.Nhs.Database/Views/SearchResourcesView.sql index d8eb0a019..bcacb902e 100644 --- a/WebAPI/LearningHub.Nhs.Database/Views/SearchResourcesView.sql +++ b/WebAPI/LearningHub.Nhs.Database/Views/SearchResourcesView.sql @@ -6,6 +6,7 @@ -- Modification History -- TD-6212 - https://hee-tis.atlassian.net/browse/TD-6212 -- 06-02-2026 Binon Yesudhas Initial Revision +-- 13-02-2026 Binon Yesudhas Handle inconsistenrt resource type ------------------------------------------------------------------------------- CREATE VIEW [dbo].[SearchResourcesView] AS @@ -14,7 +15,10 @@ WITH BaseResource AS ( r.Id, r.ResourceTypeId, -- r.ResourceTypeId as ContentType, - LOWER(rt.Name) AS ContentType, + CASE + WHEN LOWER(rt.Name) LIKE '%scorm%' THEN 'scorm' + ELSE LOWER(rt.Name) + END AS ContentType, rv.Id AS ResourceVersionId, rv.Title, rv.Description,