diff --git a/.gitignore b/.gitignore
index eecc9c86a..04baa838f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,17 +33,14 @@ obj
/.cache_ggshield
/AdminUI/LearningHub.Nhs.AdminUI/appsettings.Development.json
/AdminUI/LearningHub.Nhs.AdminUI/node_modules
-/AdminUI/LearningHub.Nhs.AdminUI/wwwroot/js/bundle
-/AdminUI/LearningHub.Nhs.AdminUI/wwwroot/css
/AdminUI/LearningHub.Nhs.AdminUI/web.config
/AdminUI/LearningHub.Nhs.AdminUI/internal-nlog-lhadminui.txt
+/WebAPI/LearningHub.Nhs.API/internal-nlog-lhapi.txt
+/WebAPI/LearningHub.Nhs.API/web.config
/AdminUI/LearningHub.Nhs.AdminUI/yarn.lock
/AdminUI/LearningHub.Nhs.AdminUI/Properties/launchSettings.json
/AdminUI/LearningHub.Nhs.AdminUI/web.config
/AdminUI/LearningHub.Nhs.AdminUI/Properties/launchSettings.json
-/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj.user
-/WebAPI/LearningHub.Nhs.API/internal-nlog-lhapi.txt
-/WebAPI/LearningHub.Nhs.API/web.config
/WebAPI/LearningHub.Nhs.API/Properties/launchSettings.json
/WebAPI/LearningHub.Nhs.API/appsettings.Development.json
/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.dbmdl
@@ -51,5 +48,5 @@ obj
/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Database/LearningHub.Nhs.Migration.Staging.Database.dbmdl
/WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Database/LearningHub.Nhs.Migration.Staging.Database.jfm
/LearningHub.Nhs.WebUI.AutomatedUiTests/appsettings.Development.json
-/WebAPI/LearningHub.Nhs.API/LearningHub.Nhs.Api.csproj.user
-/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj.user
+/OpenAPI/LearningHub.Nhs.OpenApi/appsettings.Development.json
+/OpenAPI/LearningHub.Nhs.OpenApi/web.config
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj
index 0047f4483..2c87ba2cb 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/LearningHub.Nhs.OpenApi.Models.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs
index 3675959a6..a4c1fbf4c 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceMetadataViewModel.cs
@@ -1,7 +1,9 @@
namespace LearningHub.Nhs.OpenApi.Models.ViewModels
{
+ using LearningHub.Nhs.Models.Entities.Activity;
using System.Collections.Generic;
+
///
/// Class.
///
@@ -23,20 +25,25 @@ public ResourceMetadataViewModel()
/// .
/// .
/// .
+ /// .
public ResourceMetadataViewModel(
int resourceId,
string title,
string description,
List references,
string resourceType,
- decimal rating)
+ int? majorVersion,
+ decimal rating,
+ List userSummaryActivityStatuses)
{
this.ResourceId = resourceId;
this.Title = title;
this.Description = description;
this.References = references;
this.ResourceType = resourceType;
+ this.MajorVersion = majorVersion;
this.Rating = rating;
+ this.UserSummaryActivityStatuses = userSummaryActivityStatuses;
}
///
@@ -64,9 +71,20 @@ public ResourceMetadataViewModel(
///
public string ResourceType { get; set; }
+ ///
+ /// Gets or sets .
+ ///
+ public int? MajorVersion { get; set; }
+
+
///
/// Gets or sets .
///
public decimal Rating { get; set; }
+
+ ///
+ /// Gets or sets .
+ ///
+ public List UserSummaryActivityStatuses { get; set; }
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs
index cf31bdf54..41d1b197b 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Models/ViewModels/ResourceReferenceWithResourceDetailsViewModel.cs
@@ -1,3 +1,6 @@
+using LearningHub.Nhs.Models.Entities.Activity;
+using System.Collections.Generic;
+
namespace LearningHub.Nhs.OpenApi.Models.ViewModels
{
///
@@ -14,8 +17,10 @@ public class ResourceReferenceWithResourceDetailsViewModel
/// .
/// .
/// .
+ ///
/// .
/// .
+ ///
public ResourceReferenceWithResourceDetailsViewModel(
int resourceId,
int refId,
@@ -23,17 +28,21 @@ public ResourceReferenceWithResourceDetailsViewModel(
string description,
CatalogueViewModel catalogueViewModel,
string resourceType,
+ int? majorVersion,
decimal rating,
- string link)
+ string link,
+ List userSummaryActivityStatuses)
{
this.ResourceId = resourceId;
this.RefId = refId;
this.Title = title;
this.Description = description;
this.Catalogue = catalogueViewModel;
+ this.MajorVersion = majorVersion;
this.ResourceType = resourceType;
this.Rating = rating;
this.Link = link;
+ this.UserSummaryActivityStatuses = userSummaryActivityStatuses;
}
///
@@ -66,14 +75,27 @@ public ResourceReferenceWithResourceDetailsViewModel(
///
public string ResourceType { get; }
+
+ ///
+ /// Gets .
+ ///
+ public int? MajorVersion { get; }
+
///
/// Gets .
///
+ ///
+
public decimal Rating { get; }
///
/// Gets .
///
public string Link { get; }
+
+ ///
+ /// Gets .
+ ///
+ public List UserSummaryActivityStatuses { get; }
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs
index 50eff71f6..d3a1856e4 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories.Interface/Repositories/IResourceRepository.cs
@@ -2,6 +2,7 @@ namespace LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories
{
using System.Collections.Generic;
using System.Threading.Tasks;
+ using LearningHub.Nhs.Models.Entities.Activity;
using LearningHub.Nhs.Models.Entities.Resource;
///
@@ -23,5 +24,13 @@ public interface IResourceRepository
/// Resource references.
public Task> GetResourceReferencesByOriginalResourceReferenceIds(
IEnumerable originalResourceReferenceIds);
+
+ ///
+ /// Gets resource activity for resourceReferenceIds and userIds.
+ ///
+ /// .
+ ///
+ /// ResourceActivityDTO.
+ Task> GetResourceActivityPerResourceMajorVersion(IEnumerable? resourceReferenceIds, IEnumerable? userIds);
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs
index 020a4f1f5..c1ebf721b 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/LearningHubDbContext.cs
@@ -295,6 +295,11 @@ public LearningHubDbContext(LearningHubDbContextOptions options)
///
public virtual DbSet FileChunkDetail { get; set; }
+ ///
+ /// Gets or sets the ResourceActivityDto. These are not entities. They are returned from the [activity].[GetResourceActivityPerResourceMajorVersion] stored proc..
+ ///
+ public virtual DbSet ResourceActivityDTO { get; set; }
+
///
/// Gets or sets the RecentlyAddedResources. These are not entities. They are returned from the [resources].[GetRecentlyAddedResources] stored proc..
///
@@ -312,14 +317,14 @@ public LearningHubDbContext(LearningHubDbContextOptions options)
public virtual DbSet DashboardResourceDto { get; set; }
///
- /// Gets or sets the ScormContentDetailsViewModel.
+ /// Gets or sets the ExternalContentDetailsViewModel.
///
- public virtual DbSet ScormContentDetailsViewModel { get; set; }
+ public virtual DbSet ExternalContentDetailsViewModel { get; set; }
///
- /// Gets or sets the ScormContentServerViewModel.
+ /// Gets or sets the ContentServerViewModel.
///
- public virtual DbSet ScormContentServerViewModel { get; set; }
+ public virtual DbSet ContentServerViewModel { get; set; }
///
/// Gets or sets the DashboardCatalogueDto
@@ -520,12 +525,12 @@ public LearningHubDbContext(LearningHubDbContextOptions options)
///
/// Gets or sets the whole slide image annotation.
///
- public virtual DbSet WholeSlideImageAnnotation { get; set; }
+ public virtual DbSet ImageAnnotation { get; set; }
///
/// Gets or sets the whole slide image annotation mark.
///
- public virtual DbSet WholeSlideImageAnnotationMark { get; set; }
+ public virtual DbSet ImageAnnotationMark { get; set; }
///
/// Gets or sets the media block.
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs
index 668d318b1..8db1cd876 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/EntityFramework/ServiceMappings.cs
@@ -110,8 +110,8 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs
index 1bdbb69c8..42d2e309f 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Content/PageSectionDetailMap.cs
@@ -17,8 +17,6 @@ protected override void InternalMap(EntityTypeBuilder entity)
{
entity.ToTable("PageSectionDetail", "content");
- entity.Property(e => e.AssetPositionId).HasDefaultValueSql("((2))");
-
entity.Property(e => e.BackgroundColour).HasMaxLength(20);
entity.Property(e => e.Description).HasMaxLength(512);
@@ -31,7 +29,7 @@ protected override void InternalMap(EntityTypeBuilder entity)
entity.Property(e => e.TextColour).HasMaxLength(20);
- entity.Property(e => e.Title).HasMaxLength(128);
+ entity.Property(e => e.SectionTitle).HasMaxLength(128);
entity.Property(e => e.DeletePending).IsRequired(false);
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs
index 62ebffd0c..0b8bb7428 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/LogMap.cs
@@ -68,10 +68,6 @@ protected void InternalMap(EntityTypeBuilder modelBuilder)
modelBuilder.Property(e => e.UserId)
.HasColumnName("UserId");
- modelBuilder.HasOne(d => d.User)
- .WithMany(p => p.Logs)
- .HasForeignKey(d => d.UserId)
- .OnDelete(DeleteBehavior.ClientSetNull);
}
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs
index 18a94369a..00f7f46cb 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMap.cs
@@ -7,7 +7,7 @@
///
/// The whole slide image annotation map.
///
- public class WholeSlideImageAnnotationMap : BaseEntityMap
+ public class ImageAnnotationMap : BaseEntityMap
{
///
/// The internal map.
@@ -15,15 +15,15 @@ public class WholeSlideImageAnnotationMap : BaseEntityMap
/// The model builder.
///
- protected override void InternalMap(EntityTypeBuilder modelBuilder)
+ protected override void InternalMap(EntityTypeBuilder modelBuilder)
{
- modelBuilder.ToTable("WholeSlideImageAnnotation", "resources");
+ modelBuilder.ToTable("ImageAnnotation", "resources");
modelBuilder.HasOne(a => a.WholeSlideImage)
- .WithMany(i => i.WholeSlideImageAnnotations)
+ .WithMany(i => i.ImageAnnotations)
.HasForeignKey(a => a.WholeSlideImageId)
.OnDelete(DeleteBehavior.Cascade)
- .HasConstraintName("FK_WholeSlideImageAnnotation_WholeSlideImageId");
+ .HasConstraintName("FK_ImageAnnotation_WholeSlideImageId");
}
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs
index 2720b23db..9db0a6db6 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/Blocks/WholeSlideImageAnnotationMarkMap.cs
@@ -7,7 +7,7 @@
///
/// The whole slide image annotation map.
///
- public class WholeSlideImageAnnotationMarkMap : BaseEntityMap
+ public class ImageAnnotationMarkMap : BaseEntityMap
{
///
/// The internal map.
@@ -15,15 +15,15 @@ public class WholeSlideImageAnnotationMarkMap : BaseEntityMap
/// The model builder.
///
- protected override void InternalMap(EntityTypeBuilder modelBuilder)
+ protected override void InternalMap(EntityTypeBuilder modelBuilder)
{
- modelBuilder.ToTable("WholeSlideImageAnnotationMark", "resources");
+ modelBuilder.ToTable("ImageAnnotationMark", "resources");
- modelBuilder.HasOne(a => a.WholeSlideImageAnnotation)
- .WithMany(i => i.WholeSlideImageAnnotationMarks)
- .HasForeignKey(a => a.WholeSlideImageAnnotationId)
+ modelBuilder.HasOne(a => a.ImageAnnotation)
+ .WithMany(i => i.ImageAnnotationMarks)
+ .HasForeignKey(a => a.ImageAnnotationId)
.OnDelete(DeleteBehavior.Cascade)
- .HasConstraintName("FK_WholeSlideImageAnnotationMark_WholeSlideImageAnnotationId");
+ .HasConstraintName("FK_ImageAnnotationMark_ImageAnnotationId");
}
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs
index a87bee808..d96c3b4fe 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Map/Resources/ResourceVersionMap.cs
@@ -39,6 +39,9 @@ protected override void InternalMap(EntityTypeBuilder modelBuil
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_ResourceVersion_Resource");
+ modelBuilder.Property(e => e.ResourceAccessibilityEnum).HasColumnName("ResourceAccessibilityId")
+ .HasConversion();
+
modelBuilder.Property(e => e.VersionStatusEnum).HasColumnName("VersionStatusId")
.HasConversion();
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs
index a826c627e..7ffb3c6fa 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Repositories/Repositories/ResourceRepository.cs
@@ -1,11 +1,14 @@
namespace LearningHub.Nhs.OpenApi.Repositories.Repositories
{
+ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+ using LearningHub.Nhs.Models.Entities.Activity;
using LearningHub.Nhs.Models.Entities.Resource;
using LearningHub.Nhs.OpenApi.Repositories.EntityFramework;
using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories;
+ using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
///
@@ -69,5 +72,35 @@ public async Task> GetResourceReferencesByOrigina
.ThenInclude(r => r.ResourceVersionRatingSummary)
.ToListAsync();
}
+
+ ///
+ ///
+ ///
+ /// .
+ /// A representing the result of the asynchronous operation.
+ public async Task> GetResourceActivityPerResourceMajorVersion(
+ IEnumerable? resourceReferenceIds, IEnumerable? userIds)
+ {
+ var resourceIdsParam = resourceReferenceIds != null
+ ? string.Join(",", resourceReferenceIds)
+ : null;
+
+ var userIdsParam = userIds != null
+ ? string.Join(",", userIds)
+ : null;
+
+ var resourceIdsParameter = new SqlParameter("@p0", resourceIdsParam ?? (object)DBNull.Value);
+ var userIdsParameter = new SqlParameter("@p1", userIdsParam ?? (object)DBNull.Value);
+
+ List resourceActivityDTOs = await dbContext.ResourceActivityDTO
+ .FromSqlRaw(
+ "[activity].[GetResourceActivityPerResourceMajorVersion] @p0, @p1",
+ resourceIdsParameter,
+ userIdsParameter)
+ .AsNoTracking()
+ .ToListAsync();
+
+ return resourceActivityDTOs;
+ }
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj
index 6372c89b6..108c74355 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/LearningHub.Nhs.OpenApi.Services.Interface.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs
index 8b2e46dee..795ddbf42 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/IResourceService.cs
@@ -15,13 +15,13 @@ public interface IResourceService
///
/// The original resource reference id.
/// The the resourceMetaDataViewModel corresponding to the resource reference.
- Task GetResourceReferenceByOriginalId(int originalResourceReferenceId);
+ Task GetResourceReferenceByOriginalId(int originalResourceReferenceId, int? currentUserId);
///
/// The get resources by Ids endpoint.
///
/// The original resource reference Ids.
/// The resourceReferenceMetaDataViewModel.
- Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds);
+ Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds, int? currentUserId);
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs
index 63155c5eb..c9b6e3031 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services.Interface/Services/ISearchService.cs
@@ -14,6 +14,6 @@ public interface ISearchService
///
/// .
/// .
- Task Search(ResourceSearchRequest query);
+ Task Search(ResourceSearchRequest query, int? currentUserId);
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj
index 2207dfdd4..ce9034d03 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/LearningHub.Nhs.OpenApi.Services.csproj
@@ -1,4 +1,4 @@
-
+
net6.0
@@ -24,6 +24,7 @@
+
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs
index 1e3f4e563..3212b0b37 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/ResourceService.cs
@@ -2,10 +2,13 @@ namespace LearningHub.Nhs.OpenApi.Services.Services
{
using System;
using System.Collections.Generic;
+ using System.Data;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
+ using LearningHub.Nhs.Models.Entities.Activity;
using LearningHub.Nhs.Models.Entities.Resource;
+ using LearningHub.Nhs.Models.ViewModels.Helpers;
using LearningHub.Nhs.OpenApi.Models.Exceptions;
using LearningHub.Nhs.OpenApi.Models.ViewModels;
using LearningHub.Nhs.OpenApi.Repositories.Interface.Repositories;
@@ -47,9 +50,11 @@ public ResourceService(ILearningHubService learningHubService, IResourceReposito
/// the get by id async.
///
/// the id.
+ ///
/// the resource.
- public async Task GetResourceReferenceByOriginalId(int originalResourceReferenceId)
+ public async Task GetResourceReferenceByOriginalId(int originalResourceReferenceId, int? currentUserId)
{
+ List resourceActivities = new List() { };
var list = new List() { originalResourceReferenceId };
var resourceReferences = await this.resourceRepository.GetResourceReferencesByOriginalResourceReferenceIds(list);
@@ -64,7 +69,15 @@ public async Task GetResourceRefe
throw new HttpResponseException("No matching resource reference", HttpStatusCode.NotFound);
}
- return this.GetResourceReferenceWithResourceDetailsViewModel(resourceReference);
+ if (currentUserId.HasValue)
+ {
+ List resourceIds = new List() { resourceReference.ResourceId };
+ List userIds = new List() { currentUserId.Value };
+
+ resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(resourceIds, userIds))?.ToList() ?? new List() { };
+ }
+
+ return this.GetResourceReferenceWithResourceDetailsViewModel(resourceReference, resourceActivities);
}
catch (InvalidOperationException exception)
{
@@ -78,8 +91,11 @@ public async Task GetResourceRefe
///
/// the resource reference ids.
/// the resource.
- public async Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds)
+ public async Task GetResourceReferencesByOriginalIds(List originalResourceReferenceIds, int? currentUserId)
{
+ List resourceActivities = new List() { };
+ List majorVersionIdActivityStatusDescription = new List() { };
+
var resourceReferences = await this.resourceRepository.GetResourceReferencesByOriginalResourceReferenceIds(originalResourceReferenceIds);
var resourceReferencesList = resourceReferences.ToList();
var matchedIds = resourceReferencesList.Select(r => r.OriginalResourceReferenceId).ToList();
@@ -95,18 +111,33 @@ public async Task GetResourceReferencesByOrigina
this.logger.LogWarning($"Multiple resource references found with OriginalResourceReferenceId {duplicateIds.First()}");
}
- var matchedResources = resourceReferencesList
- .Select(this.GetResourceReferenceWithResourceDetailsViewModel)
- .ToList();
+ if (currentUserId.HasValue)
+ {
+ List resourceIds = resourceReferencesList.Select(rrl => rrl.ResourceId).ToList();
+ List userIds = new List() { currentUserId.Value };
+
+ resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(resourceIds, userIds))?.ToList() ?? new List() { };
+ }
+
+ List matchedResources = resourceReferencesList
+ .Select(rr => this.GetResourceReferenceWithResourceDetailsViewModel(rr, resourceActivities.Where(ra => ra.ResourceId == rr.ResourceId).ToList()))
+ .ToList();
return new BulkResourceReferenceViewModel(matchedResources, unmatchedIds);
}
- private ResourceReferenceWithResourceDetailsViewModel GetResourceReferenceWithResourceDetailsViewModel(ResourceReference resourceReference)
+ private ResourceReferenceWithResourceDetailsViewModel GetResourceReferenceWithResourceDetailsViewModel(ResourceReference resourceReference, List resourceActivities)
{
var hasCurrentResourceVersion = resourceReference.Resource.CurrentResourceVersion != null;
var hasRating = resourceReference.Resource.CurrentResourceVersion?.ResourceVersionRatingSummary != null;
+ List majorVersionIdActivityStatusDescription = new List() { };
+
+ if (resourceActivities != null && resourceActivities.Count != 0)
+ {
+ majorVersionIdActivityStatusDescription = ActivityStatusHelper.GetMajorVersionIdActivityStatusDescriptionLSPerResource(resourceReference.Resource, resourceActivities).ToList();
+ }
+
if (resourceReference.Resource == null)
{
throw new Exception("No matching resource");
@@ -135,8 +166,10 @@ private ResourceReferenceWithResourceDetailsViewModel GetResourceReferenceWithRe
resourceReference.Resource.CurrentResourceVersion?.Description ?? string.Empty,
resourceReference.GetCatalogue(),
resourceTypeNameOrEmpty,
+ resourceReference.Resource?.CurrentResourceVersion?.MajorVersion ?? 0,
resourceReference.Resource?.CurrentResourceVersion?.ResourceVersionRatingSummary?.AverageRating ?? 0,
- this.learningHubService.GetResourceLaunchUrl(resourceReference.OriginalResourceReferenceId));
+ this.learningHubService.GetResourceLaunchUrl(resourceReference.OriginalResourceReferenceId),
+ majorVersionIdActivityStatusDescription);
}
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs
index 656a8cb14..c5ede76fb 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Services/Services/SearchService.cs
@@ -3,8 +3,11 @@ namespace LearningHub.Nhs.OpenApi.Services.Services
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+ using LearningHub.Nhs.Models.Entities.Activity;
using LearningHub.Nhs.Models.Entities.Resource;
+ using LearningHub.Nhs.Models.Resource;
using LearningHub.Nhs.Models.Search;
+ using LearningHub.Nhs.Models.ViewModels.Helpers;
using LearningHub.Nhs.OpenApi.Models.ServiceModels.Findwise;
using LearningHub.Nhs.OpenApi.Models.ServiceModels.Resource;
using LearningHub.Nhs.OpenApi.Models.ViewModels;
@@ -57,7 +60,7 @@ public SearchService(
}
///
- public async Task Search(ResourceSearchRequest query)
+ public async Task Search(ResourceSearchRequest query, int? currentUserId)
{
var findwiseResultModel = await this.findwiseClient.Search(query);
@@ -66,7 +69,7 @@ public async Task Search(ResourceSearchRequest query)
return ResourceSearchResultModel.FailedWithStatus(findwiseResultModel.FindwiseRequestStatus);
}
- var resourceMetadataViewModels = await this.GetResourceMetadataViewModels(findwiseResultModel);
+ var resourceMetadataViewModels = await this.GetResourceMetadataViewModels(findwiseResultModel, currentUserId);
var totalHits = findwiseResultModel.SearchResults?.Stats.TotalHits;
@@ -77,8 +80,9 @@ public async Task Search(ResourceSearchRequest query)
}
private async Task> GetResourceMetadataViewModels(
- FindwiseResultModel findwiseResultModel)
+ FindwiseResultModel findwiseResultModel, int? currentUserId)
{
+ List resourceActivities = new List() { };
var documentsFound = findwiseResultModel.SearchResults?.DocumentList.Documents?.ToList() ??
new List();
var findwiseResourceIds = documentsFound.Select(d => int.Parse(d.Id)).ToList();
@@ -90,7 +94,7 @@ private async Task> GetResourceMetadataViewModel
var resourcesFound = await this.resourceRepository.GetResourcesFromIds(findwiseResourceIds);
- var resourceMetadataViewModels = resourcesFound.Select(this.MapToViewModel)
+ List resourceMetadataViewModels = resourcesFound.Select(resource => MapToViewModel(resource, resourceActivities.Where(x => x.ResourceId == resource.Id).ToList()))
.OrderBySequence(findwiseResourceIds)
.ToList();
@@ -105,14 +109,29 @@ private async Task> GetResourceMetadataViewModel
unmatchedResourcesIdsString);
}
+ if (currentUserId.HasValue)
+ {
+ List resourceIds = resourcesFound.Select(x => x.Id).ToList();
+ List userIds = new List() { currentUserId.Value };
+
+ resourceActivities = (await this.resourceRepository.GetResourceActivityPerResourceMajorVersion(resourceIds, userIds))?.ToList() ?? new List() { };
+ }
return resourceMetadataViewModels;
}
- private ResourceMetadataViewModel MapToViewModel(Resource resource)
+ private ResourceMetadataViewModel MapToViewModel(Resource resource, List resourceActivities)
{
var hasCurrentResourceVersion = resource.CurrentResourceVersion != null;
var hasRating = resource.CurrentResourceVersion?.ResourceVersionRatingSummary != null;
+ List majorVersionIdActivityStatusDescription = new List() { };
+
+ if (resourceActivities != null && resourceActivities.Count != 0)
+ {
+ majorVersionIdActivityStatusDescription = ActivityStatusHelper.GetMajorVersionIdActivityStatusDescriptionLSPerResource(resource, resourceActivities)
+ .ToList();
+ }
+
if (!hasCurrentResourceVersion)
{
this.logger.LogInformation(
@@ -131,13 +150,17 @@ private ResourceMetadataViewModel MapToViewModel(Resource resource)
this.logger.LogError($"Resource has unrecognised type: {resource.ResourceTypeEnum}");
}
+
return new ResourceMetadataViewModel(
resource.Id,
resource.CurrentResourceVersion?.Title ?? ResourceHelpers.NoResourceVersionText,
resource.CurrentResourceVersion?.Description ?? string.Empty,
resource.ResourceReference.Select(this.GetResourceReferenceViewModel).ToList(),
resourceTypeNameOrEmpty,
- resource.CurrentResourceVersion?.ResourceVersionRatingSummary?.AverageRating ?? 0.0m);
+ resource.CurrentResourceVersion?.MajorVersion ?? 0,
+ resource.CurrentResourceVersion?.ResourceVersionRatingSummary?.AverageRating ?? 0.0m,
+ majorVersionIdActivityStatusDescription
+ );
}
private ResourceReferenceViewModel GetResourceReferenceViewModel(
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs
index 49f901c72..acde1d3ef 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Controllers/ResourceControllerTests.cs
@@ -18,8 +18,11 @@ namespace LearningHub.Nhs.OpenApi.Tests.Controllers
using Moq;
using Newtonsoft.Json;
using Xunit;
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Mvc;
+ using System.Security.Claims;
- public sealed class ResourceControllerTests : IDisposable
+ public sealed class ResourceControllerTests
{
private readonly Mock searchService;
private readonly Mock resourceService;
@@ -87,6 +90,7 @@ await Assert.ThrowsAsync(
public async Task SearchEndpointUsesDefaultLimitGivenInConfig()
{
// Given
+ int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth
this.GivenSearchServiceSucceedsButFindsNoItems();
this.GivenDefaultLimitForFindwiseSearchIs(12);
this.resourceController = new ResourceController(
@@ -99,7 +103,7 @@ public async Task SearchEndpointUsesDefaultLimitGivenInConfig()
// Then
this.searchService.Verify(
- service => service.Search(It.Is(request => request.Limit == 12)));
+ service => service.Search(It.Is(request => request.Limit == 12), currentUserId));
}
[Fact]
@@ -177,6 +181,41 @@ await Assert.ThrowsAsync(
exception.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
+ [Fact]
+ public void CurrentUserIdSetByAuth()
+ {
+ // Arrange
+ ResourceController resourceController = new ResourceController(
+ this.searchService.Object,
+ this.resourceService.Object,
+ this.findwiseConfigOptions.Object
+ );
+
+
+ // This Id is the development accountId
+ int currentUserId = 57541;
+
+ // Create claims identity with the specified user id
+ var claims = new List
+ {
+ new Claim(ClaimTypes.NameIdentifier, currentUserId.ToString()),
+ };
+ var identity = new ClaimsIdentity(claims, "AuthenticationTypes.Federation"); // Set the authentication type to "Federation"
+
+ // Create claims principal with the claims identity
+ var claimsPrincipal = new ClaimsPrincipal(identity);
+
+ // Create a mock HttpContext and set it to the ControllerContext
+ var httpContext = new DefaultHttpContext { User = claimsPrincipal };
+ var controllerContext = new ControllerContext { HttpContext = httpContext };
+ resourceController.ControllerContext = controllerContext;
+
+ // Act
+
+ // Assert that the CurrentUserId property of the resourceController matches the currentUserId
+ Assert.Equal(currentUserId, resourceController.CurrentUserId);
+ }
+
[Theory]
[InlineData(1)]
[InlineData(20)]
@@ -184,6 +223,7 @@ await Assert.ThrowsAsync(
public async Task SearchEndpointUsesPassedInLimitIfGiven(int limit)
{
// Given
+ int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth
this.GivenSearchServiceSucceedsButFindsNoItems();
this.GivenDefaultLimitForFindwiseSearchIs(20);
this.resourceController = new ResourceController(
@@ -196,12 +236,7 @@ public async Task SearchEndpointUsesPassedInLimitIfGiven(int limit)
// Then
this.searchService.Verify(
- service => service.Search(It.Is(request => request.Limit == limit)));
- }
-
- public void Dispose()
- {
- this.resourceController?.Dispose();
+ service => service.Search(It.Is(request => request.Limit == limit), currentUserId));
}
private void GivenDefaultLimitForFindwiseSearchIs(int limit)
@@ -212,14 +247,17 @@ private void GivenDefaultLimitForFindwiseSearchIs(int limit)
private void GivenSearchServiceFailsWithStatus(FindwiseRequestStatus status)
{
- this.searchService.Setup(ss => ss.Search(It.IsAny())).ReturnsAsync(
+ int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth
+ this.searchService.Setup(ss => ss.Search(It.IsAny(), currentUserId)).ReturnsAsync(
new ResourceSearchResultModel(new List(), status, 0));
}
private void GivenSearchServiceSucceedsButFindsNoItems()
{
- this.searchService.Setup(ss => ss.Search(It.IsAny())).ReturnsAsync(
+ int? currentUserId = null; //E.g if hitting endpoint with ApiKey auth
+ this.searchService.Setup(ss => ss.Search(It.IsAny(), currentUserId)).ReturnsAsync(
new ResourceSearchResultModel(new List(), FindwiseRequestStatus.Success, 0));
}
+
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs
index d6ae8f3db..1f791a353 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/ResourceServiceTests.cs
@@ -2,10 +2,12 @@ namespace LearningHub.Nhs.OpenApi.Tests.Services.Services
{
using System;
using System.Collections.Generic;
+ using System.Linq;
using System.Net;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
+ using LearningHub.Nhs.Models.Entities.Activity;
using LearningHub.Nhs.Models.Entities.Resource;
using LearningHub.Nhs.Models.Enums;
using LearningHub.Nhs.OpenApi.Models.Exceptions;
@@ -22,14 +24,25 @@ public class ResourceServiceTests
private readonly Mock learningHubService;
private readonly ResourceService resourceService;
private readonly Mock resourceRepository;
+ private readonly int currentUserId;
public ResourceServiceTests()
{
+ //This Id is the development accountId
+ this.currentUserId = 57541;
+
this.learningHubService = new Mock();
this.resourceRepository = new Mock();
this.resourceService = new ResourceService(this.learningHubService.Object, this.resourceRepository.Object, new NullLogger());
}
-
+ private List ResourceActivityDTOList => new List()
+ {
+ new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 5, MajorVersion = 5 },
+ new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 7, MajorVersion = 4 },
+ new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 3, MajorVersion = 3 },
+ new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 7, MajorVersion = 2 },
+ new ResourceActivityDTO{ ResourceId = 1, ActivityStatusId = 3, MajorVersion = 1 },
+ };
private List ResourceList => new List()
{
ResourceTestHelper.CreateResourceWithDetails(id: 1, title: "title1", description: "description1", rating: 3m, resourceType: ResourceTypeEnum.Article),
@@ -63,7 +76,7 @@ public async Task SingleResourceEndpointReturnsTheCorrectInformationIfThereIsAMa
.ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1));
// When
- var x = await this.resourceService.GetResourceReferenceByOriginalId(1);
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(1, null);
// Then
x.Rating.Should().Be(3);
@@ -80,7 +93,7 @@ public async Task SingleResourceReturnsA404IfTheresNoResourceReferenceWithAMatch
.ReturnsAsync(new List());
// When / Then
- var exception = await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(999));
+ var exception = await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(999, null));
exception.StatusCode.Should().Be(HttpStatusCode.NotFound);
exception.ResponseBody.Should().Be("No matching resource reference");
}
@@ -93,7 +106,7 @@ public async Task SingleResourceEndpointReturnsAResourceMetadataViewModelObjectW
.ReturnsAsync(this.ResourceReferenceList.GetRange(1, 1));
// When
- var x = await this.resourceService.GetResourceReferenceByOriginalId(2);
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(2, null);
// Then
x.Title.Should().Be("No current resource version");
@@ -108,7 +121,7 @@ public async Task SingleResourceEndpointReturnsAMessageSayingNoCatalogueIfThereI
.ReturnsAsync(this.ResourceReferenceList.GetRange(2, 1));
// When
- var x = await this.resourceService.GetResourceReferenceByOriginalId(3);
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(3, null);
// Then
x.Catalogue.Name.Should().Be("No catalogue for resource reference");
@@ -122,7 +135,7 @@ public async Task SingleResourceEndpointReturnsAMessageSayingNoCatalogueIfThereI
.ReturnsAsync(this.ResourceReferenceList.GetRange(3, 1));
// When
- var x = await this.resourceService.GetResourceReferenceByOriginalId(4);
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(4, null);
// Then
x.Catalogue.Name.Should().Be("No catalogue for resource reference");
@@ -136,7 +149,7 @@ public async Task SingleResourceEndpointReturnsAMessageSayingNoCatalogueIfThereI
.ReturnsAsync(this.ResourceReferenceList.GetRange(5, 1));
// When
- var x = await this.resourceService.GetResourceReferenceByOriginalId(6);
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(6, null);
// Then
x.Catalogue.Name.Should().Be("No catalogue for resource reference");
@@ -150,7 +163,7 @@ public async Task SingleResourceEndpointReturnsAZeroForRatingIfTheresNoRatingSum
.ReturnsAsync(this.ResourceReferenceList.GetRange(7, 1));
// When
- var x = await this.resourceService.GetResourceReferenceByOriginalId(8);
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(8, null);
// Then
x.Catalogue.Name.Should().Be("catalogue3");
@@ -165,7 +178,7 @@ public async Task SingleResourceEndpointThrowsAnErrorAndReturnsABlankStringIfThe
.ReturnsAsync(this.ResourceReferenceList.GetRange(8, 1));
// When
- var x = await this.resourceService.GetResourceReferenceByOriginalId(9);
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(9, null);
// Then
x.ResourceType.Should().Be(string.Empty);
@@ -179,7 +192,7 @@ public async Task SingleResourceEndpointThrowsAnErrorIfThereIsMoreThanOneResourc
.ReturnsAsync(this.ResourceReferenceList.GetRange(9, 2));
// When / Then
- await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(10));
+ await Assert.ThrowsAsync(async () => await this.resourceService.GetResourceReferenceByOriginalId(10, null));
}
/*[Fact]
@@ -198,7 +211,7 @@ public async Task BulkEndpointReturnsAllMatchingResources()
.ReturnsAsync(this.ResourceReferenceList.GetRange(0, 2));
// When
- var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp);
+ var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null);
// Then
x.ResourceReferences.Count.Should().Be(2);
@@ -220,7 +233,7 @@ public async Task BulkEndpointReturnsA404IfThereAreNoMatchingResources()
.ReturnsAsync(new List());
// When
- var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp);
+ var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null);
// Then
x.UnmatchedResourceReferenceIds.Count.Should().Be(2);
@@ -237,7 +250,7 @@ public async Task BulkEndpointReturnsResourcesWithIncompleteInformation()
.ReturnsAsync(this.ResourceReferenceList.GetRange(0, 4));
// When
- var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp);
+ var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null);
// Then
x.ResourceReferences.Count.Should().Be(4);
@@ -257,7 +270,7 @@ public async Task BulkEndpointReturnsUnmatchedResourcesWithMatchedResources()
.ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1));
// When
- var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp);
+ var x = await this.resourceService.GetResourceReferencesByOriginalIds(idsToLookUp, null);
// Then
x.ResourceReferences.Count.Should().Be(1);
@@ -277,7 +290,7 @@ public async Task ResourceServiceReturnsTheOriginalResourceReferenceIdAsTheRefId
.ReturnsAsync(this.ResourceReferenceList.GetRange(5, 2));
// When
- var x = await this.resourceService.GetResourceReferencesByOriginalIds(list);
+ var x = await this.resourceService.GetResourceReferencesByOriginalIds(list, null);
// Then
x.ResourceReferences[0].RefId.Should().Be(6);
@@ -293,7 +306,7 @@ public async Task ResourceServiceReturnsTheOriginalResourceReferenceIdAsTheRefId
.ReturnsAsync(this.ResourceReferenceList.GetRange(5, 1));
// When
- var x = await this.resourceService.GetResourceReferenceByOriginalId(6);
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(6, null);
// Then
x.RefId.Should().Be(6);
@@ -308,7 +321,7 @@ public async Task ResourceServiceReturnsThatARestrictedCatalogueIsRestricted()
.ReturnsAsync(this.ResourceReferenceList.GetRange(8, 1));
// When
- var x = await this.resourceService.GetResourceReferenceByOriginalId(9);
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(9, null);
// Then
x.Catalogue.IsRestricted.Should().BeTrue();
@@ -323,10 +336,58 @@ public async Task ResourceServiceReturnsThatAnUnrestrictedCatalogueIsUnrestricte
.ReturnsAsync(this.ResourceReferenceList.GetRange(7, 1));
// When
- var x = await this.resourceService.GetResourceReferenceByOriginalId(8);
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(8, null);
// Then
x.Catalogue.IsRestricted.Should().BeFalse();
}
+
+ [Fact]
+ public async Task SingleResourceEndpointReturnsActivitySummaryWhenCurrentUserIdProvided()
+ {
+ // Given
+ this.resourceRepository.Setup(rr => rr.GetResourceReferencesByOriginalResourceReferenceIds(new List() { 1 }))
+ .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1));
+
+ this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(new List() { 1 }, new List() { currentUserId }))
+ .ReturnsAsync(this.ResourceActivityDTOList.ToList());
+
+ // When
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(1, currentUserId);
+
+ // Then
+ x.UserSummaryActivityStatuses.Should().NotBeNull();
+ x.UserSummaryActivityStatuses[0].MajorVersionId.Should().Be(5);
+ x.UserSummaryActivityStatuses[1].MajorVersionId.Should().Be(4);
+ x.UserSummaryActivityStatuses[2].MajorVersionId.Should().Be(3);
+ x.UserSummaryActivityStatuses[3].MajorVersionId.Should().Be(2);
+ x.UserSummaryActivityStatuses[4].MajorVersionId.Should().Be(1);
+
+ x.UserSummaryActivityStatuses[0].ActivityStatusDescription.Should().Be("Passed");
+ x.UserSummaryActivityStatuses[1].ActivityStatusDescription.Should().Be("In progress");
+ x.UserSummaryActivityStatuses[2].ActivityStatusDescription.Should().Be("Viewed");
+ x.UserSummaryActivityStatuses[3].ActivityStatusDescription.Should().Be("In progress");
+ x.UserSummaryActivityStatuses[4].ActivityStatusDescription.Should().Be("Viewed");
+
+ }
+
+ [Fact]
+ public async Task SingleResourceEndpointReturnsEmptyActivitySummaryWhenNoCurrentUserIdProvided()
+ {
+ // Given
+ this.resourceRepository.Setup(rr => rr.GetResourceReferencesByOriginalResourceReferenceIds(new List() { 1 }))
+ .ReturnsAsync(this.ResourceReferenceList.GetRange(0, 1));
+
+ // This should not be hit
+ this.resourceRepository.Setup(rr => rr.GetResourceActivityPerResourceMajorVersion(new List() { 1 }, new List() { currentUserId }))
+ .ReturnsAsync(this.ResourceActivityDTOList.ToList());
+
+ // When
+ var x = await this.resourceService.GetResourceReferenceByOriginalId(1, null);
+
+ // Then
+ x.UserSummaryActivityStatuses.Should().BeEmpty();
+
+ }
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs
index d393ce59d..ee84c1c51 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi.Tests/Services/Services/SearchServiceTests.cs
@@ -6,6 +6,7 @@ namespace LearningHub.Nhs.OpenApi.Tests.Services.Services
using FizzWare.NBuilder;
using FluentAssertions;
using FluentAssertions.Execution;
+ using LearningHub.Nhs.Models.Entities.Activity;
using LearningHub.Nhs.Models.Entities.Resource;
using LearningHub.Nhs.Models.Enums;
using LearningHub.Nhs.Models.Search;
@@ -73,7 +74,7 @@ public async Task SearchPassesQueryOnToFindwise()
.ReturnsAsync(FindwiseResultModel.Failure(FindwiseRequestStatus.Timeout));
// When
- await this.searchService.Search(searchRequest);
+ await this.searchService.Search(searchRequest, null);
// Then
this.findwiseClient.Verify(fc => fc.Search(searchRequest));
@@ -90,7 +91,7 @@ public async Task SearchReturnsTotalHitsAndSearchResult()
this.GivenFindwiseReturnsSuccessfulResponse(74, Enumerable.Range(1, 34));
// When
- var searchResult = await this.searchService.Search(searchRequest);
+ var searchResult = await this.searchService.Search(searchRequest, null);
// Then
searchResult.Resources.Count.Should().Be(34);
@@ -137,7 +138,7 @@ public async Task SearchResultsReturnExpectedValues()
this.GivenFindwiseReturnsSuccessfulResponse(2, new[] { 1, 2, 3 });
// When
- var searchResult = await this.searchService.Search(searchRequest);
+ var searchResult = await this.searchService.Search(searchRequest, null);
// Then
searchResult.Resources.Count.Should().Be(2);
@@ -180,7 +181,7 @@ public async Task SearchReturnsResourcesInOrderMatchingFindwise()
.ReturnsAsync(resources);
// When
- var searchResultModel = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10));
+ var searchResultModel = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10), null);
// Then
searchResultModel.Resources.Select(r => r.ResourceId).Should().ContainInOrder(new[] { 1, 3, 2 });
@@ -194,7 +195,6 @@ public async Task SearchReplacesNullPropertiesOfResourceWithDefaultValues()
{
Builder.CreateNew()
.With(r => r.Id = 1)
- .With(r => r.CurrentResourceVersion = null)
.With(
r => r.ResourceReference = new[]
{
@@ -212,7 +212,7 @@ public async Task SearchReplacesNullPropertiesOfResourceWithDefaultValues()
this.GivenFindwiseReturnsSuccessfulResponse(1, new[] { 1 });
// When
- var searchResult = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10));
+ var searchResult = await this.searchService.Search(new ResourceSearchRequest("text", 0, 10), null);
// Then
using var scope = new AssertionScope();
@@ -233,7 +233,9 @@ public async Task SearchReplacesNullPropertiesOfResourceWithDefaultValues()
string.Empty,
expectedResourceReferences,
"Article",
- 0));
+ 0,
+ 0,
+ new List(){ }));
}
private void GivenFindwiseReturnsSuccessfulResponse(int totalHits, IEnumerable resourceIds)
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/OpenApiControllerBase.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/OpenApiControllerBase.cs
new file mode 100644
index 000000000..863a8bb22
--- /dev/null
+++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/OpenApiControllerBase.cs
@@ -0,0 +1,41 @@
+namespace LearningHub.NHS.OpenAPI.Controllers
+{
+ using System.Net;
+ using System.Security.Claims;
+ using LearningHub.Nhs.OpenApi.Models.Exceptions;
+ using Microsoft.AspNetCore.Mvc;
+
+ ///
+ /// The base class for API controllers.
+ ///
+ public abstract class OpenApiControllerBase : ControllerBase
+ {
+ ///
+ /// Gets the current user's ID.
+ ///
+ public int? CurrentUserId
+ {
+ get
+ {
+ if ((this.User?.Identity?.AuthenticationType ?? null) == "AuthenticationTypes.Federation")
+ {
+ int userId;
+ if (int.TryParse(User.FindFirst(ClaimTypes.NameIdentifier).Value, out userId))
+ {
+ return userId;
+ }
+ else
+ {
+ // If parsing fails, return null - for apikey this will be the name
+ return null;
+ }
+ }
+ else
+ {
+ // When authorizing by ApiKey we do not have a user for example
+ return null;
+ }
+ }
+ }
+ }
+}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs
index 95591961f..a92463d20 100644
--- a/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs
+++ b/OpenAPI/LearningHub.Nhs.OpenApi/Controllers/ResourceController.cs
@@ -21,7 +21,7 @@ namespace LearningHub.NHS.OpenAPI.Controllers
///
[Route("Resource")]
[Authorize]
- public class ResourceController : Controller
+ public class ResourceController : OpenApiControllerBase
{
private const int MaxNumberOfReferenceIds = 1000;
private readonly ISearchService searchService;
@@ -75,7 +75,7 @@ await this.searchService.Search(
offset,
limit ?? this.findwiseConfig.DefaultItemLimitForSearch,
catalogueId,
- resourceTypes));
+ resourceTypes), this.CurrentUserId);
switch (resourceSearchResult.FindwiseRequestStatus)
{
@@ -109,7 +109,7 @@ await this.searchService.Search(
[HttpGet("{originalResourceReferenceId}")]
public async Task GetResourceReferenceByOriginalId(int originalResourceReferenceId)
{
- return await this.resourceService.GetResourceReferenceByOriginalId(originalResourceReferenceId);
+ return await this.resourceService.GetResourceReferenceByOriginalId(originalResourceReferenceId, this.CurrentUserId);
}
///
@@ -125,7 +125,7 @@ public async Task GetResourceReferencesByOrigina
throw new HttpResponseException($"Too many resources requested. The maximum is {MaxNumberOfReferenceIds}", HttpStatusCode.BadRequest);
}
- return await this.resourceService.GetResourceReferencesByOriginalIds(resourceReferenceIds.ToList());
+ return await this.resourceService.GetResourceReferencesByOriginalIds(resourceReferenceIds.ToList(), this.CurrentUserId);
}
///
@@ -148,7 +148,7 @@ public async Task GetResourceReferencesByOrigina
throw new HttpResponseException($"Too many resources requested. The maximum is {MaxNumberOfReferenceIds}", HttpStatusCode.BadRequest);
}
- return await this.resourceService.GetResourceReferencesByOriginalIds(bulkResourceReferences.ResourceReferenceIds);
+ return await this.resourceService.GetResourceReferencesByOriginalIds(bulkResourceReferences.ResourceReferenceIds, this.CurrentUserId);
}
}
}
diff --git a/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user
new file mode 100644
index 000000000..b17387f00
--- /dev/null
+++ b/OpenAPI/LearningHub.Nhs.OpenApi/LearningHub.NHS.OpenAPI.csproj.user
@@ -0,0 +1,9 @@
+
+
+
+ ProjectDebugger
+
+
+ IIS Local
+
+
\ No newline at end of file
diff --git a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj
index 5670201e0..ee9f4c0ad 100644
--- a/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj
+++ b/WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.sqlproj
@@ -59,6 +59,7 @@
+
@@ -199,10 +200,6 @@
-
-
-
-
@@ -518,22 +515,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetResourceActivityPerResourceMajorVersion.sql b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetResourceActivityPerResourceMajorVersion.sql
new file mode 100644
index 000000000..063110343
--- /dev/null
+++ b/WebAPI/LearningHub.Nhs.Database/Stored Procedures/Activity/GetResourceActivityPerResourceMajorVersion.sql
@@ -0,0 +1,152 @@
+
+-------------------------------------------------------------------------------
+-- Author Phil T
+-- Created 04-07-24
+-- Purpose Return resource activity for each major version for user
+
+
+-- Description
+/*
+ This procedure returns a single entry per resource Id, selecting the most important one for that major version.
+ This is so users can still have a resourceActivity history following a majorVersion change
+
+ UserIds is nullable so that general resource activity can be searched for
+ ResourceIds is nullable so that all a users history can be searched for
+
+ When determining the resourceActivity statusDescription in the front end resourceTypeId is also required for changing completed statuses to resourceType specific ones
+
+ Currently if multiple rows meet the case criteria we retrieve the one with the highest Id which is also expected to be the ActivityEnd part of the activityStatus pair.
+
+*/
+-- Future Considerations
+/*
+ Because the activityResource should come in pairs one with ActivityStart populated and one with ActivityEnd populated
+ it could be desireable to join via LaunchResourceActivityId and coalesce the data in future.
+ Or/And coalesce where the case returns multiple rows.
+
+*/
+-- Notes
+ -- resourceId is used not originalResourceId
+
+
+-------------------------------------------------------------------------------
+
+-- Create the new stored procedure
+CREATE PROCEDURE [activity].[GetResourceActivityPerResourceMajorVersion]
+ @ResourceIds VARCHAR(MAX) = NULL,
+ @UserIds VARCHAR(MAX) = NULL
+AS
+BEGIN
+
+ -- Split the comma-separated list into a table of integers
+ DECLARE @ResourceIdTable TABLE (ResourceId INT);
+
+ IF @ResourceIds IS NOT NULL AND @ResourceIds <> ''
+ BEGIN
+ INSERT INTO @ResourceIdTable (ResourceId)
+ SELECT CAST(value AS INT)
+ FROM STRING_SPLIT(@ResourceIds, ',');
+ END;
+
+ -- Split the comma-separated list of UserIds into a table
+ DECLARE @UserIdTable TABLE (UserId INT);
+
+ IF @UserIds IS NOT NULL AND @UserIds <> ''
+ BEGIN
+ INSERT INTO @UserIdTable (UserId)
+ SELECT CAST(value AS INT)
+ FROM STRING_SPLIT(@UserIds, ',');
+ END;
+
+ WITH FilteredResourceActivities AS (
+ SELECT
+ ars.[Id],
+ ars.[UserId],
+ ars.[LaunchResourceActivityId],
+ ars.[ResourceId],
+ ars.[ResourceVersionId],
+ ars.[MajorVersion],
+ ars.[MinorVersion],
+ ars.[NodePathId],
+ ars.[ActivityStatusId],
+ ars.[ActivityStart],
+ ars.[ActivityEnd],
+ ars.[DurationSeconds],
+ ars.[Score],
+ ars.[Deleted],
+ ars.[CreateUserID],
+ ars.[CreateDate],
+ ars.[AmendUserID],
+ ars.[AmendDate]
+ FROM
+ [activity].[resourceactivity] ars
+ WHERE
+ (@UserIds IS NULL OR ars.userId IN (SELECT UserId FROM @UserIdTable) OR NOT EXISTS (SELECT 1 FROM @UserIdTable))
+ AND (@ResourceIds IS NULL OR @ResourceIds = '' OR ars.resourceId IN (SELECT ResourceId FROM @ResourceIdTable) OR NOT EXISTS (SELECT 1 FROM @ResourceIdTable))
+ AND ars.Deleted = 0
+ AND ars.ActivityStatusId NOT IN (1, 6, 2) -- These Ids are not in use - Launched, Downloaded, In Progress (stored as completed and incomplete then renamed in the application)
+ ),
+ RankedActivities AS (
+ SELECT
+ ra.[Id],
+ ra.[UserId],
+ ra.[LaunchResourceActivityId],
+ ra.[ResourceId],
+ ra.[ResourceVersionId],
+ ra.[MajorVersion],
+ ra.[MinorVersion],
+ ra.[NodePathId],
+ ra.[ActivityStatusId],
+ ra.[ActivityStart],
+ ra.[ActivityEnd],
+ ra.[DurationSeconds],
+ ra.[Score],
+ ra.[Deleted],
+ ra.[CreateUserID],
+ ra.[CreateDate],
+ ra.[AmendUserID],
+ ra.[AmendDate],
+ ROW_NUMBER() OVER (
+ PARTITION BY resourceId, userId, MajorVersion
+ ORDER BY
+ CASE
+ WHEN ActivityStatusId = 5 THEN 1 -- Passed
+ WHEN ActivityStatusId = 3 THEN 2 -- Completed
+ WHEN ActivityStatusId = 4 THEN 3 -- Failed
+ WHEN ActivityStatusId = 7 THEN 4 -- Incomplete
+ ELSE 5 -- shouldn't be any
+ END,
+ Id DESC -- we have two entries per interacting with a resource the start and the end, we are just returning the last entry made
+ -- there is the option of instead coalescing LaunchResourceActivityId, ActivityStart,ActivityEnd potentially via joining LaunchResourceActivityId and UserId
+ ) AS RowNum
+ FROM
+ FilteredResourceActivities ra
+ )
+ SELECT
+ ra.[Id],
+ ra.[UserId],
+ ra.[LaunchResourceActivityId],
+ ra.[ResourceId],
+ ra.[ResourceVersionId],
+ ra.[MajorVersion],
+ ra.[MinorVersion],
+ ra.[NodePathId],
+ ra.[ActivityStatusId],
+ ra.[ActivityStart],
+ ra.[ActivityEnd],
+ ra.[DurationSeconds],
+ ra.[Score],
+ ra.[Deleted],
+ ra.[CreateUserID],
+ ra.[CreateDate],
+ ra.[AmendUserID],
+ ra.[AmendDate]
+ FROM
+ RankedActivities ra
+ WHERE
+ RowNum = 1
+ order by MajorVersion desc;
+END;
+GO
+
+