diff --git a/Childrens-Social-Care-CPD-Tests/Contentful/EntityResolverTests.cs b/Childrens-Social-Care-CPD-Tests/Contentful/EntityResolverTests.cs index f50f1ab4..eddd142f 100644 --- a/Childrens-Social-Care-CPD-Tests/Contentful/EntityResolverTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Contentful/EntityResolverTests.cs @@ -23,14 +23,12 @@ public class EntityResolverTests [TestCase("detailedRole", typeof(DetailedRole))] [TestCase("heroBanner", typeof(HeroBanner))] [TestCase("imageCard", typeof(ImageCard))] - [TestCase("imageResource", typeof(ImageResource))] [TestCase("linkCard", typeof(LinkCard))] [TestCase("linkListCard", typeof(LinkListCard))] [TestCase("pdfFileResource", typeof(PdfFileResource))] - [TestCase("resource", typeof(Resource))] [TestCase("richTextBlock", typeof(RichTextBlock))] [TestCase("roleList", typeof(RoleList))] - [TestCase("sideMenu", typeof(SideMenu))] + [TestCase("navigationMenu", typeof(NavigationMenu))] [TestCase("textBlock", typeof(TextBlock))] [TestCase("videoResource", typeof(VideoResource))] public void Resolves_Correctly(string contentTypeId, Type expectedType) diff --git a/Childrens-Social-Care-CPD-Tests/Contentful/PartialsFactoryTests.cs b/Childrens-Social-Care-CPD-Tests/Contentful/PartialsFactoryTests.cs index 22ea4d51..5ccc67bc 100644 --- a/Childrens-Social-Care-CPD-Tests/Contentful/PartialsFactoryTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Contentful/PartialsFactoryTests.cs @@ -9,7 +9,7 @@ namespace Childrens_Social_Care_CPD_Tests.Contentful; [TestFixture] public partial class PartialsFactoryTests { - public static object[] Successful_Resolves = + private static readonly object[] Successful_Resolves = { new object[] { new AreaOfPractice(), "_AreaOfPractice" }, new object[] { new AreaOfPracticeList(), "_AreaOfPracticeList" }, @@ -23,13 +23,11 @@ public partial class PartialsFactoryTests new object[] { new HeroBanner(), string.Empty }, new object[] { new LinkCard(), "_LinkCard" }, new object[] { new ImageCard(), "_ImageCard" }, - new object[] { new ImageResource(), "_ImageResource" }, + new object[] { new NavigationMenu(), "_NavigationMenu" }, new object[] { new LinkListCard(), "_LinkListCard" }, new object[] { new PdfFileResource(), "_PdfFileResource" }, - new object[] { new Resource(), "_Resource" }, new object[] { new RichTextBlock(), "_RichTextBlock" }, new object[] { new RoleList(), "_RoleList" }, - new object[] { new SideMenu(), "_SideMenu" }, new object[] { new TextBlock(), "_TextBlock" }, new object[] { new VideoResource(), "_VideoResource" }, }; @@ -42,7 +40,7 @@ public void Resolves_Correctly(IContent item, string expectedPartialName) actual.Should().Be(expectedPartialName); } - public static IContent[] Unsuccessful_Resolves = + private static readonly IContent[] Unsuccessful_Resolves = { new TestingContentItem(), null diff --git a/Childrens-Social-Care-CPD-Tests/Contentful/Renderers/AssetStructureRendererTests.cs b/Childrens-Social-Care-CPD-Tests/Contentful/Renderers/AssetStructureRendererTests.cs new file mode 100644 index 00000000..4dc9bcdd --- /dev/null +++ b/Childrens-Social-Care-CPD-Tests/Contentful/Renderers/AssetStructureRendererTests.cs @@ -0,0 +1,126 @@ + +using Childrens_Social_Care_CPD.Contentful.Renderers; +using Contentful.Core.Models; +using FluentAssertions; +using Microsoft.Extensions.WebEncoders.Testing; +using NUnit.Framework; +using StringWriter = System.IO.StringWriter; + + +namespace Childrens_Social_Care_CPD_Tests.Contentful.Renderers; + +public class AssetStructureRendererTests +{ + private readonly IRenderer _sut = new AssetStructureRenderer(); + + [Test] + public void Ignores_Empty_Structure() + { + // arrange + var assetStructure = new AssetStructure(); + + // act + var htmlContent = _sut.Render(assetStructure); + + // assert + htmlContent.Should().BeNull(); + } + + [Test] + public void Ignores_Empty_StructureData() + { + // arrange + var assetStructure = new AssetStructure + { + Data = new AssetStructureData() + }; + + // act + var htmlContent = _sut.Render(assetStructure); + + // assert + htmlContent.Should().BeNull(); + } + + [Test] + public void Ignores_Empty_Asset() + { + // arrange + var assetStructure = new AssetStructure + { + Data = new AssetStructureData + { + Target = new Asset() + } + }; + + // act + var htmlContent = _sut.Render(assetStructure); + + // assert + htmlContent.Should().BeNull(); + } + + [TestCase("text/html")] + [TestCase("application/pdf")] + public void Ignores_ContentTypes(string contentType) + { + // arrange + var assetStructure = new AssetStructure + { + Data = new AssetStructureData + { + Target = new Asset + { + File = new File + { + ContentType = contentType, + Url = "/foo" + }, + Description = "foo" + } + } + }; + + // act + var htmlContent = _sut.Render(assetStructure); + + // assert + htmlContent.Should().BeNull(); + } + + [TestCase("image/png")] + [TestCase("Image/PNG")] + [TestCase("image/gif")] + [TestCase("image/xxx")] + public void Renders_Image_Asset(string contentType) + { + // arrange + var stringWriter = new StringWriter(); + var assetStructure = new AssetStructure + { + Data = new AssetStructureData + { + Target = new Asset + { + File = new File + { + ContentType = contentType, + Url = "/foo" + }, + Description = "foo" + } + } + }; + + // act + var htmlContent = _sut.Render(assetStructure); + htmlContent.WriteTo(stringWriter, new HtmlTestEncoder()); + var actual = stringWriter.ToString(); + + // assert + actual.Should().StartWith(" _sut = new ContentLinkRenderer(); [TestCase("http://foo", "http://foo")] [TestCase("https://foo", "https://foo")] diff --git a/Childrens-Social-Care-CPD-Tests/Controllers/ContentControllerTests.cs b/Childrens-Social-Care-CPD-Tests/Controllers/ContentControllerTests.cs index a3c89caa..8f62f9b6 100644 --- a/Childrens-Social-Care-CPD-Tests/Controllers/ContentControllerTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Controllers/ContentControllerTests.cs @@ -126,21 +126,21 @@ public async Task Index_Sets_The_ContextModel_Preferences_Set_Value_Correctly(bo actual.PreferenceSet.Should().Be(preferenceSet); } - private static readonly object[] _sideMenuContent = + private static readonly object[] _navigationMenuContent = { - new object[] { new SideMenu() }, + new object[] { new NavigationMenu() }, new object[] { null }, }; - [TestCaseSource(nameof(_sideMenuContent))] - public async Task Index_Sets_The_ContextModel_UseContainers_From_SideMenu_Value_Correctly(SideMenu sideMenu) + [TestCaseSource(nameof(_navigationMenuContent))] + public async Task Index_Sets_The_ContextModel_UseContainers_From_SideMenu_Value_Correctly(NavigationMenu navigationMenu) { // arrange var rootContent = new Content() { - SideMenu = sideMenu + Navigation = navigationMenu }; - var expected = sideMenu == null; + var expected = navigationMenu == null; SetContent(rootContent); // act diff --git a/Childrens-Social-Care-CPD-Tests/Controllers/CookieControllerTests.cs b/Childrens-Social-Care-CPD-Tests/Controllers/CookieControllerTests.cs index 1acd856c..ac6408cb 100644 --- a/Childrens-Social-Care-CPD-Tests/Controllers/CookieControllerTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Controllers/CookieControllerTests.cs @@ -122,19 +122,19 @@ public async Task Cookies_Sets_The_ContextModel_Preferences_Set_Value_Correctly( actual.PreferenceSet.Should().Be(preferenceSet); } - private static readonly object[] _sideMenuContent = + private static readonly object[] _navigationMenuContent = { - new object[] { new SideMenu() }, + new object[] { new NavigationMenu() }, new object[] { null }, }; - [TestCaseSource(nameof(_sideMenuContent))] - public async Task Cookies_Sets_The_ContextModel_UseContainers_Ignoring_The_SideMenu_Value(SideMenu sideMenu) + [TestCaseSource(nameof(_navigationMenuContent))] + public async Task Cookies_Sets_The_ContextModel_UseContainers_Ignoring_The_SideMenu_Value(NavigationMenu navigationMenu) { // arrange var rootContent = new Content() { - SideMenu = sideMenu + Navigation = navigationMenu }; SetContent(rootContent); diff --git a/Childrens-Social-Care-CPD-Tests/Controllers/ResourcesControllerTests.cs b/Childrens-Social-Care-CPD-Tests/Controllers/ResourcesControllerTests.cs index cc39a970..fe5e464c 100644 --- a/Childrens-Social-Care-CPD-Tests/Controllers/ResourcesControllerTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Controllers/ResourcesControllerTests.cs @@ -1,14 +1,19 @@ using Childrens_Social_Care_CPD.Configuration; +using Childrens_Social_Care_CPD.Contentful.Models; using Childrens_Social_Care_CPD.Controllers; using Childrens_Social_Care_CPD.Core.Resources; +using Childrens_Social_Care_CPD.DataAccess; +using Childrens_Social_Care_CPD.GraphQL.Queries; using Childrens_Social_Care_CPD.Models; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewFeatures; using NSubstitute; -using NSubstitute.Core; using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -18,6 +23,7 @@ public class ResourcesControllerTests { private IFeaturesConfig _featuresConfig; private IResourcesSearchStrategy _searchStrategy; + private IResourcesRepository _resourcesRepository; private ResourcesController _resourcesController; private IRequestCookieCollection _cookies; private HttpContext _httpContext; @@ -26,6 +32,7 @@ public class ResourcesControllerTests [SetUp] public void SetUp() { + _resourcesRepository = Substitute.For(); _cookies = Substitute.For(); _httpContext = Substitute.For(); _httpRequest = Substitute.For(); @@ -38,7 +45,7 @@ public void SetUp() _featuresConfig = Substitute.For(); _featuresConfig.IsEnabled(Features.ResourcesAndLearning).Returns(true); _searchStrategy = Substitute.For(); - _resourcesController = new ResourcesController(_featuresConfig, _searchStrategy) + _resourcesController = new ResourcesController(_featuresConfig, _searchStrategy, _resourcesRepository) { ControllerContext = controllerContext, TempData = Substitute.For() @@ -65,9 +72,9 @@ public async Task Search_Returns_Strategy_Model_When_Order_Value_Returned() // arrange var model = new ResourcesListViewModel(null, null, null, null); _searchStrategy.SearchAsync(Arg.Any(), Arg.Any()).Returns(model); - ResourcesQuery query = new ResourcesQuery(); + var query = new ResourcesQuery(); - // act + // act var actual = await _resourcesController.Search(query: query) as ViewResult; // assert @@ -88,4 +95,151 @@ public async Task Disabling_Resources_Feature_Returns_NotFoundResult() // assert actual.Should().BeOfType(); } + + [Test] + public async Task Index_Returns_404_When_Resource_Feature_Disabled() + { + // arrange + _featuresConfig.IsEnabled(Features.ResourcesAndLearning).Returns(false); + + // act + var actual = await _resourcesController.Index(); + + // assert + actual.Should().BeOfType(); + } + + [Test] + public async Task Index_Searches_For_Page_Under_Resources_Area() + { + // arrange + var actual = string.Empty; + _resourcesRepository.GetByIdAsync(Arg.Do(x => actual = x), cancellationToken: Arg.Any()) + .Returns(Task.FromResult(Tuple.Create((Content)null, (GetContentTags.ResponseType)null))); + + // act + await _resourcesController.Index("foo"); + + // assert + actual.Should().Be("resources-learning/foo"); + } + + [Test] + public async Task Index_Returns_Not_Found_When_Content_Does_Not_Exist() + { + // arrange + _resourcesRepository.GetByIdAsync(Arg.Any(), cancellationToken: Arg.Any()) + .Returns(Task.FromResult(Tuple.Create((Content)null, (GetContentTags.ResponseType)null))); + + // act + var actual = await _resourcesController.Index("foo"); + + // assert + actual.Should().BeOfType(); + } + + private static GetContentTags.ResponseType CreateTagsResponse(List tags) + { + return new GetContentTags.ResponseType + { + ContentCollection = new() + { + Items = new[] + { + new GetContentTags.ContentItem + { + ContentfulMetaData = new() + { + Tags = tags + } + } + } + } + }; + } + + [TestCase("Published")] + [TestCase("Last updated")] + public async Task Index_Passes_Default_Properties(string propertyName) + { + // arrange + var createdAt = DateTime.UtcNow.AddMinutes(-10); + var updatedAt = DateTime.UtcNow; + var content = new Content + { + Sys = new() + { + CreatedAt = createdAt, + UpdatedAt = updatedAt, + } + }; + var tags = CreateTagsResponse(new()); + + _resourcesRepository.GetByIdAsync(Arg.Any(), cancellationToken: Arg.Any()) + .Returns(Task.FromResult(Tuple.Create(content, tags))); + + // act + var result = await _resourcesController.Index("foo") as ViewResult; + var properties = result.ViewData["Properties"] as IDictionary; + + // assert + properties.Should().NotBeNull(); + properties.ContainsKey(propertyName).Should().BeTrue(); + } + + [Test] + public async Task Index_Passes_Correctly_Tagged_Properties() + { + // arrange + var createdAt = DateTime.UtcNow.AddMinutes(-10); + var updatedAt = DateTime.UtcNow; + var content = new Content + { + Sys = new () + { + CreatedAt = createdAt, + UpdatedAt = updatedAt, + } + }; + var tags = CreateTagsResponse(new() { new () { Id = "foo", Name = "Resource:Foo=foo" }, }); + + _resourcesRepository.GetByIdAsync(Arg.Any(), cancellationToken: Arg.Any()) + .Returns(Task.FromResult(Tuple.Create(content, tags))); + + // act + var result = await _resourcesController.Index("foo") as ViewResult; + var properties = result.ViewData["Properties"] as IDictionary; + + // assert + properties.Should().NotBeNull(); + properties["Foo"].Should().Be("foo"); + } + + [Test] + public async Task Index_Ignores_Unrelated_Tags_For_Properties() + { + // arrange + var createdAt = DateTime.UtcNow.AddMinutes(-10); + var updatedAt = DateTime.UtcNow; + var content = new Content + { + Sys = new() + { + CreatedAt = createdAt, + UpdatedAt = updatedAt, + } + }; + var tags = CreateTagsResponse(new() { new() { Id = "foo", Name = "Topic:Foo=foo" }, }); + + _resourcesRepository.GetByIdAsync(Arg.Any(), cancellationToken: Arg.Any()) + .Returns(Task.FromResult(Tuple.Create(content, tags))); + + // act + var result = await _resourcesController.Index("foo") as ViewResult; + var properties = result.ViewData["Properties"] as IDictionary; + + // assert + properties.Should().NotBeNull(); + properties.ContainsKey("Foo").Should().BeFalse(); + } } \ No newline at end of file diff --git a/Childrens-Social-Care-CPD-Tests/Core/Resources/ResourcesDynamicTagsSearchStategyTests.cs b/Childrens-Social-Care-CPD-Tests/Core/Resources/ResourcesDynamicTagsSearchStategyTests.cs index 9db77a3b..5530c3bd 100644 --- a/Childrens-Social-Care-CPD-Tests/Core/Resources/ResourcesDynamicTagsSearchStategyTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Core/Resources/ResourcesDynamicTagsSearchStategyTests.cs @@ -109,7 +109,7 @@ public async Task Search_Page_Set_To_Be_In_Bounds() // arrange var results = new ResponseType() { - ResourceCollection = new ResourceCollection() + ContentCollection = new ContentCollection() { Total = 3, Items = new Collection() diff --git a/Childrens-Social-Care-CPD-Tests/Core/Resources/ResourcesFixedTagsSearchStrategyTests.cs b/Childrens-Social-Care-CPD-Tests/Core/Resources/ResourcesFixedTagsSearchStrategyTests.cs index 28793eb3..80a62c27 100644 --- a/Childrens-Social-Care-CPD-Tests/Core/Resources/ResourcesFixedTagsSearchStrategyTests.cs +++ b/Childrens-Social-Care-CPD-Tests/Core/Resources/ResourcesFixedTagsSearchStrategyTests.cs @@ -103,7 +103,7 @@ public async Task Search_Page_Set_To_Be_In_Bounds() // arrange var results = new ResponseType() { - ResourceCollection = new ResourceCollection() + ContentCollection = new ContentCollection() { Total = 3, Items = new Collection() diff --git a/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs b/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs index a213c107..be8a5178 100644 --- a/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs +++ b/Childrens-Social-Care-CPD-Tests/DataAccess/ResourcesRepositoryTests.cs @@ -12,12 +12,12 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using static Childrens_Social_Care_CPD.GraphQL.Queries.SearchResourcesByTags; using System.Collections.ObjectModel; using System; using Contentful.Core.Models.Management; using System.Linq; using Childrens_Social_Care_CPD.Controllers; +using Childrens_Social_Care_CPD.GraphQL.Queries; namespace Childrens_Social_Care_CPD_Tests.DataAccess; @@ -28,11 +28,11 @@ public class ResourcesRepositoryTests private ICpdContentfulClient _contentfulClient; private IGraphQLWebSocketClient _gqlClient; - private void SetSearchResults(ResponseType responseType) + private void SetSearchResults(SearchResourcesByTags.ResponseType responseType) { - var response = Substitute.For>(); + var response = Substitute.For>(); response.Data = responseType; - _gqlClient.SendQueryAsync(Arg.Any(), Arg.Any()).Returns(response); + _gqlClient.SendQueryAsync(Arg.Any(), Arg.Any()).Returns(response); } [SetUp] @@ -92,14 +92,14 @@ public async Task FetchRootPageAsync_Returns_Null_When_Root_Page_Not_Found() public async Task FindByTagsAsync_Returns_Results() { // arrange - var results = new ResponseType + var results = new SearchResourcesByTags.ResponseType { - ResourceCollection = new ResourceCollection + ContentCollection = new SearchResourcesByTags.ContentCollection { Total = 1, - Items = new Collection + Items = new Collection { - new SearchResult() + new SearchResourcesByTags.SearchResult() } } }; @@ -117,11 +117,11 @@ public async Task FindByTagsAsync_Returns_Results() public async Task FindByTagsAsync_Limits_Results() { // arrange - var results = new ResponseType(); - var response = Substitute.For>(); + var results = new SearchResourcesByTags.ResponseType(); + var response = Substitute.For>(); response.Data = results; GraphQLRequest request = null; - _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); @@ -137,11 +137,11 @@ public async Task FindByTagsAsync_Limits_Results() public async Task FindByTagsAsync_Skips_Results() { // arrange - var results = new ResponseType(); - var response = Substitute.For>(); + var results = new SearchResourcesByTags.ResponseType(); + var response = Substitute.For>(); response.Data = results; GraphQLRequest request = null; - _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); @@ -157,10 +157,10 @@ public async Task FindByTagsAsync_Skips_Results() public async Task FindByTagsAsync_Preview_Flag_Is_False_By_Default() { // arrange - var response = Substitute.For>(); - response.Data = new ResponseType(); + var response = Substitute.For>(); + response.Data = new SearchResourcesByTags.ResponseType(); GraphQLRequest request = null; - _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); @@ -178,10 +178,10 @@ public async Task FindByTagsAsync_Sets_Preview_Flag() // arrange _applicationConfiguration.ContentfulEnvironment.Returns(new StringConfigSetting(() => ApplicationEnvironment.PreProduction)); - var response = Substitute.For>(); - response.Data = new ResponseType(); + var response = Substitute.For>(); + response.Data = new SearchResourcesByTags.ResponseType(); GraphQLRequest request = null; - _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); @@ -249,4 +249,114 @@ public async Task GetSearchTagsAsync_Strips_Unwanted_Grouped_Tags() // assert actual.Any(x => x.TagName == "fooFoo").Should().BeFalse(); } + + [Test] + public async Task GetByIdAsync_Queries_Tags_By_Id() + { + // arrange + var collection = new ContentfulCollection + { + Items = new List() + }; + _contentfulClient.GetEntries(Arg.Any>(), Arg.Any()).Returns(collection); + + var results = new GetContentTags.ResponseType(); + var response = Substitute.For>(); + response.Data = results; + GraphQLRequest request = null; + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + await sut.GetByIdAsync("foo", cancellationToken: _cancellationTokenSource.Token); + + // assert + dynamic variables = request.Variables; + (variables.id as object).Should().Be("foo"); + } + + [Test] + public async Task GetByIdAsync_Preview_Flag_Is_False_By_Default() + { + // arrange + var collection = new ContentfulCollection + { + Items = new List() + }; + _contentfulClient.GetEntries(Arg.Any>(), Arg.Any()).Returns(collection); + + var results = new GetContentTags.ResponseType(); + var response = Substitute.For>(); + response.Data = results; + GraphQLRequest request = null; + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + await sut.GetByIdAsync("foo", cancellationToken: _cancellationTokenSource.Token); + + // assert + dynamic variables = request.Variables; + (variables.preview as object).Should().Be(false); + } + + [Test] + public async Task GetByIdAsync_Sets_Preview_Flag() + { + // arrange + _applicationConfiguration.ContentfulEnvironment.Returns(new StringConfigSetting(() => ApplicationEnvironment.PreProduction)); + + var collection = new ContentfulCollection + { + Items = new List() + }; + _contentfulClient.GetEntries(Arg.Any>(), Arg.Any()).Returns(collection); + + var results = new GetContentTags.ResponseType(); + var response = Substitute.For>(); + response.Data = results; + GraphQLRequest request = null; + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + await sut.GetByIdAsync("foo", cancellationToken: _cancellationTokenSource.Token); + + // assert + dynamic variables = request.Variables; + (variables.preview as object).Should().Be(true); + } + + [Test] + public async Task GetByIdAsync_Returns_Data() + { + // arrange + _applicationConfiguration.ContentfulEnvironment.Returns(new StringConfigSetting(() => ApplicationEnvironment.PreProduction)); + + var content = new Content(); + var collection = new ContentfulCollection + { + Items = new List { content } + }; + _contentfulClient.GetEntries(Arg.Any>(), Arg.Any()).Returns(collection); + + var results = new GetContentTags.ResponseType(); + var response = Substitute.For>(); + response.Data = results; + GraphQLRequest request = null; + _gqlClient.SendQueryAsync(Arg.Do(value => request = value), Arg.Any()).Returns(response); + + var sut = new ResourcesRepository(_applicationConfiguration, _contentfulClient, _gqlClient); + + // act + (var actualContent, var actualTags) = await sut.GetByIdAsync("foo", cancellationToken: _cancellationTokenSource.Token); + + // assert + actualContent.Should().Be(content); + actualTags.Should().Be(results); + } + } diff --git a/Childrens-Social-Care-CPD-Tests/GlobalSuppressions.cs b/Childrens-Social-Care-CPD-Tests/GlobalSuppressions.cs new file mode 100644 index 00000000..ca032dcd --- /dev/null +++ b/Childrens-Social-Care-CPD-Tests/GlobalSuppressions.cs @@ -0,0 +1,10 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("CodeQuality", "IDE0077:Avoid legacy format target in 'SuppressMessageAttribute'", Justification = "")] +[assembly: SuppressMessage("GeneratedRegex", "SYSLIB1045:Convert to 'GeneratedRegexAttribute'.", Justification = "Test project", Scope = "namespaceanddescendants", Target = "Childrens_Social_Care_CPD_Tests")] +[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Test project", Scope = "namespaceanddescendants", Target = "Childrens_Social_Care_CPD_Tests")] diff --git a/Childrens-Social-Care-CPD-Tests/TagHelperOutputExtensions.cs b/Childrens-Social-Care-CPD-Tests/TagHelperOutputExtensions.cs new file mode 100644 index 00000000..3994fc32 --- /dev/null +++ b/Childrens-Social-Care-CPD-Tests/TagHelperOutputExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.WebEncoders.Testing; +using System.IO; + +namespace Childrens_Social_Care_CPD_Tests; + +public static class TagHelperOutputExtensions +{ + public static string AsString(this TagHelperOutput tagHelperOutput) + { + var stringWriter = new StringWriter(); + tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); + return stringWriter.ToString(); + } +} diff --git a/Childrens-Social-Care-CPD-Tests/TagHelpers/CpdPropertiesListTests.cs b/Childrens-Social-Care-CPD-Tests/TagHelpers/CpdPropertiesListTests.cs new file mode 100644 index 00000000..0503a299 --- /dev/null +++ b/Childrens-Social-Care-CPD-Tests/TagHelpers/CpdPropertiesListTests.cs @@ -0,0 +1,118 @@ +using Childrens_Social_Care_CPD.TagHelpers; +using FluentAssertions; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.WebEncoders.Testing; +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace Childrens_Social_Care_CPD_Tests.TagHelpers; + +public class CpdPropertiesListTests +{ + private TagHelperContext _tagHelperContext; + private TagHelperOutput _tagHelperOutput; + + [SetUp] + public void SetUp() + { + static Task func(bool result, HtmlEncoder encoder) + { + var tagHelperContent = new DefaultTagHelperContent(); + tagHelperContent.SetHtmlContent(string.Empty); + return Task.FromResult(tagHelperContent); + } + + _tagHelperContext = new TagHelperContext(new TagHelperAttributeList(), new Dictionary(), "id"); + _tagHelperOutput = new TagHelperOutput(CpdPropertiesList.TagName, new TagHelperAttributeList(), func); + } + + [Test] + public async Task Output_Is_Suppressed_When_Items_Not_Set() + { + // arrange + var sut = new CpdPropertiesList(); + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.AsString().Should().BeEmpty(); + } + + [Test] + public async Task Output_Is_Suppressed_When_No_Items() + { + // arrange + var sut = new CpdPropertiesList + { + Items = new Dictionary() + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.AsString().Should().BeEmpty(); + } + + [Test] + public async Task Output_Is_A_UL_Element() + { + // arrange + var sut = new CpdPropertiesList + { + Items = new Dictionary() { { "Foo", "foo" } } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.TagName.Should().Be("ul"); + } + + [Test] + public async Task Items_Are_Rendered() + { + // arrange + var sut = new CpdPropertiesList + { + Items = new Dictionary() + { + { "Foo", "foo" }, + { "Foo2", "foo2" } + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + var actual = _tagHelperOutput.AsString(); + + // assert + actual.Should().Contain("Foo"); + actual.Should().Contain("Foo2"); + } + + [Test] + public async Task Items_Are_Rendered_As_ListItem() + { + // arrange + var sut = new CpdPropertiesList + { + Items = new Dictionary() + { + { "Foo", "foo" } + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + var actual = _tagHelperOutput.AsString(); + + // assert + actual.Should().Contain("
  • HtmlEncode[[Foo"); + } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD-Tests/TagHelpers/CpdResourceNavTests.cs b/Childrens-Social-Care-CPD-Tests/TagHelpers/CpdResourceNavTests.cs new file mode 100644 index 00000000..f086549c --- /dev/null +++ b/Childrens-Social-Care-CPD-Tests/TagHelpers/CpdResourceNavTests.cs @@ -0,0 +1,148 @@ +using Childrens_Social_Care_CPD.Contentful.Models; +using Childrens_Social_Care_CPD.TagHelpers; +using FluentAssertions; +using Microsoft.AspNetCore.Razor.TagHelpers; +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace Childrens_Social_Care_CPD_Tests.TagHelpers; + +public class CpdResourceNavTests +{ + private TagHelperContext _tagHelperContext; + private TagHelperOutput _tagHelperOutput; + + [SetUp] + public void SetUp() + { + static Task func(bool result, HtmlEncoder encoder) + { + var tagHelperContent = new DefaultTagHelperContent(); + tagHelperContent.SetHtmlContent(string.Empty); + return Task.FromResult(tagHelperContent); + } + + _tagHelperContext = new TagHelperContext(new TagHelperAttributeList(), new Dictionary(), "id"); + _tagHelperOutput = new TagHelperOutput(CpdResourceNav.TagName, new TagHelperAttributeList(), func); + } + + [Test] + public async Task Output_Is_Suppressed_When_Navigation_Not_Set() + { + // arrange + var sut = new CpdResourceNav(); + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.AsString().Should().BeEmpty(); + } + + [Test] + public async Task Output_Is_Suppressed_When_No_Navigation_Items() + { + // arrange + var sut = new CpdResourceNav + { + Navigation = new List() + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.AsString().Should().BeEmpty(); + } + + [Test] + public async Task Output_Is_A_Nav_Element() + { + // arrange + var sut = new CpdResourceNav + { + Navigation = new List + { + new ContentLink { Uri = "/foo" } + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.TagName.Should().Be("nav"); + } + + [TestCase("role", "navigation")] + [TestCase("aria-label", "Resource pages")] + public async Task Nav_Has_Attributes(string name, string value) + { + // arrange + var sut = new CpdResourceNav + { + Navigation = new List + { + new ContentLink { Uri = "/foo" } + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.Attributes.ContainsName(name); + _tagHelperOutput.Attributes[name].Value.Should().Be(value); + } + + [Test] + public async Task Selected_Item_Is_Rendered() + { + // arrange + var sut = new CpdResourceNav + { + Selected = "foo1", + Navigation = new List + { + new ContentLink { Name = "Foo1", Uri = "/foo1" }, + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + var actual = _tagHelperOutput.AsString(); + + // assert + actual.Should().NotContain(""); + } + + [Test] + public async Task UnSelected_Item_Is_Rendered() + { + // arrange + var sut = new CpdResourceNav + { + Selected = "foo2", + Navigation = new List + { + new ContentLink { Name = "Foo1", Uri = "/foo1" }, + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + var actual = _tagHelperOutput.AsString(); + + // assert + actual.Should().Contain(" func(bool result, HtmlEncoder encoder) + { + var tagHelperContent = new DefaultTagHelperContent(); + tagHelperContent.SetHtmlContent(string.Empty); + return Task.FromResult(tagHelperContent); + } + + _tagHelperContext = new TagHelperContext(new TagHelperAttributeList(), new Dictionary(), "id"); + _tagHelperOutput = new TagHelperOutput(CpdResourcePageNav.TagName, new TagHelperAttributeList(), func); + } + + [Test] + public async Task Current_Is_Required() + { + // arrange + var sut = new CpdResourcePageNav(); + Func act = () => sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // act/assert + await act.Should().ThrowAsync(); + } + + [Test] + public async Task Output_Is_Suppressed_When_Navigation_Not_Set() + { + // arrange + var sut = new CpdResourcePageNav { Current = "foo" }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.AsString().Should().BeEmpty(); + } + + [Test] + public async Task Output_Is_Suppressed_When_No_Navigation_Items() + { + // arrange + var sut = new CpdResourcePageNav + { + Current = "foo", + Navigation = new () + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.AsString().Should().BeEmpty(); + } + + [Test] + public async Task Output_Is_A_Nav_Element() + { + // arrange + var sut = new CpdResourcePageNav + { + Current = "foo", + Navigation = new () + { + new ContentLink { Uri = "/foo" } + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.TagName.Should().Be("nav"); + } + + [TestCase("role", "navigation")] + [TestCase("aria-label", "pagination")] + public async Task Nav_Has_Attributes(string name, string value) + { + // arrange + var sut = new CpdResourcePageNav + { + Current = "foo", + Navigation = new () + { + new ContentLink { Uri = "/foo" } + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + + // assert + _tagHelperOutput.Attributes.ContainsName(name); + _tagHelperOutput.Attributes[name].Value.Should().Be(value); + } + + [Test] + public async Task Renders_Only_Next_When_At_Start_Of_Navigation() + { + // arrange + var sut = new CpdResourcePageNav + { + Current = "foo1", + Navigation = new () + { + new ContentLink { Uri = "/foo1" }, + new ContentLink { Uri = "/foo2" }, + new ContentLink { Uri = "/foo3" }, + new ContentLink { Uri = "/foo4" }, + new ContentLink { Uri = "/foo5" }, + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + var actual = _tagHelperOutput.AsString(); + + // assert + actual.Should().NotContain("prev"); + actual.Should().Contain("next"); + } + + [TestCase("foo2")] + [TestCase("foo3")] + [TestCase("foo4")] + public async Task Renders_Only_Prev_And_Next_When_In_Middle_Of_Navigation(string current) + { + // arrange + var sut = new CpdResourcePageNav + { + Current = current, + Navigation = new () + { + new ContentLink { Uri = "/foo1" }, + new ContentLink { Uri = "/foo2" }, + new ContentLink { Uri = "/foo3" }, + new ContentLink { Uri = "/foo4" }, + new ContentLink { Uri = "/foo5" }, + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + var actual = _tagHelperOutput.AsString(); + + // assert + actual.Should().Contain("prev"); + actual.Should().Contain("next"); + } + + [Test] + public async Task Renders_Only_Prev_When_At_End_Of_Navigation() + { + // arrange + var sut = new CpdResourcePageNav + { + Current = "foo5", + Navigation = new () + { + new ContentLink { Uri = "/foo1" }, + new ContentLink { Uri = "/foo2" }, + new ContentLink { Uri = "/foo3" }, + new ContentLink { Uri = "/foo4" }, + new ContentLink { Uri = "/foo5" }, + } + }; + + // act + await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); + var actual = _tagHelperOutput.AsString(); + + // assert + actual.Should().Contain("prev"); + actual.Should().NotContain("next"); + } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterCategoryTagHelperTests.cs b/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterCategoryTagHelperTests.cs index 26b0b73e..c9bf5d4c 100644 --- a/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterCategoryTagHelperTests.cs +++ b/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterCategoryTagHelperTests.cs @@ -47,14 +47,14 @@ public async Task Output_Is_An_Div_Element() public async Task Index_Should_Be_Used() { // arrange - var stringWriter = new StringWriter(); - var sut = new GdsFilterCategoryTagHelper(); - sut.Index = 3; + var sut = new GdsFilterCategoryTagHelper + { + Index = 3 + }; // act await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().Contain("id=\"HtmlEncode[[accordion-default-heading-3]]\""); @@ -65,14 +65,14 @@ public async Task Index_Should_Be_Used() public async Task Title_Should_Be_Used() { // arrange - var stringWriter = new StringWriter(); - var sut = new GdsFilterCategoryTagHelper(); - sut.Title = "Foo"; + var sut = new GdsFilterCategoryTagHelper + { + Title = "Foo" + }; // act await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().Contain(">Foo<"); @@ -82,7 +82,6 @@ public async Task Title_Should_Be_Used() public async Task ChildContent_Should_Be_Rendered() { // arrange - var stringWriter = new StringWriter(); var sut = new GdsFilterCategoryTagHelper(); var content = ""; @@ -99,8 +98,7 @@ Task func(bool result, HtmlEncoder encoder) // act await sut.ProcessAsync(_tagHelperContext, tagHelperOutput); - tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = tagHelperOutput.AsString(); // assert actual.Should().Contain(""); diff --git a/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterCheckboxTagHelperTests.cs b/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterCheckboxTagHelperTests.cs index 1fbffd47..12652dc9 100644 --- a/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterCheckboxTagHelperTests.cs +++ b/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterCheckboxTagHelperTests.cs @@ -47,14 +47,14 @@ public async Task Output_Is_An_Div_Element() public async Task Id_Should_Be_Used() { // arrange - var stringWriter = new StringWriter(); - var sut = new GdsFilterCheckboxTagHelper(); - sut.Id = "foo"; + var sut = new GdsFilterCheckboxTagHelper + { + Id = "foo" + }; // act await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().Contain("id=\"HtmlEncode[[foo]]\""); @@ -64,14 +64,14 @@ public async Task Id_Should_Be_Used() public async Task Name_Should_Be_Used() { // arrange - var stringWriter = new StringWriter(); - var sut = new GdsFilterCheckboxTagHelper(); - sut.Name = "foo"; + var sut = new GdsFilterCheckboxTagHelper + { + Name = "foo" + }; // act await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().Contain("name=\"HtmlEncode[[foo]]\""); @@ -81,14 +81,14 @@ public async Task Name_Should_Be_Used() public async Task Value_Should_Be_Used() { // arrange - var stringWriter = new StringWriter(); - var sut = new GdsFilterCheckboxTagHelper(); - sut.Value = "foo"; + var sut = new GdsFilterCheckboxTagHelper + { + Value = "foo" + }; // act await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().Contain("value=\"HtmlEncode[[foo]]\""); @@ -98,14 +98,14 @@ public async Task Value_Should_Be_Used() public async Task Checked_Should_Be_Used() { // arrange - var stringWriter = new StringWriter(); - var sut = new GdsFilterCheckboxTagHelper(); - sut.Checked = true; + var sut = new GdsFilterCheckboxTagHelper + { + Checked = true + }; // act await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().Contain("checked"); @@ -115,14 +115,14 @@ public async Task Checked_Should_Be_Used() public async Task Checked_Should_Not_Be_Used() { // arrange - var stringWriter = new StringWriter(); - var sut = new GdsFilterCheckboxTagHelper(); - sut.Checked = false; + var sut = new GdsFilterCheckboxTagHelper + { + Checked = false + }; // act await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().NotContain("checked"); @@ -132,14 +132,14 @@ public async Task Checked_Should_Not_Be_Used() public async Task Output_Is_A_Checkbox() { // arrange - var stringWriter = new StringWriter(); - var sut = new GdsFilterCheckboxTagHelper(); - sut.Checked = false; + var sut = new GdsFilterCheckboxTagHelper + { + Checked = false + }; // act await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().Contain("type=\"HtmlEncode[[checkbox]]\""); @@ -149,7 +149,6 @@ public async Task Output_Is_A_Checkbox() public async Task ChildContent_Should_Be_Rendered() { // arrange - var stringWriter = new StringWriter(); var sut = new GdsFilterCheckboxTagHelper(); var content = ""; @@ -166,8 +165,7 @@ Task func(bool result, HtmlEncoder encoder) // act await sut.ProcessAsync(_tagHelperContext, tagHelperOutput); - tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = tagHelperOutput.AsString(); // assert actual.Should().Contain(""); diff --git a/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterTagHelperTests.cs b/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterTagHelperTests.cs index bb37ad7f..a1a0df11 100644 --- a/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterTagHelperTests.cs +++ b/Childrens-Social-Care-CPD-Tests/TagHelpers/GdsFilterTagHelperTests.cs @@ -48,8 +48,10 @@ public async Task ClearFiltersUri_Should_Be_Used() { // arrange var stringWriter = new StringWriter(); - var sut = new GdsFilterTagHelper(); - sut.ClearFiltersUri = "foo"; + var sut = new GdsFilterTagHelper + { + ClearFiltersUri = "foo" + }; // act await sut.ProcessAsync(_tagHelperContext, _tagHelperOutput); diff --git a/Childrens-Social-Care-CPD-Tests/TagHelpers/GovUkPaginationTagHelperTests.cs b/Childrens-Social-Care-CPD-Tests/TagHelpers/GovUkPaginationTagHelperTests.cs index db31db8a..f8e89e64 100644 --- a/Childrens-Social-Care-CPD-Tests/TagHelpers/GovUkPaginationTagHelperTests.cs +++ b/Childrens-Social-Care-CPD-Tests/TagHelpers/GovUkPaginationTagHelperTests.cs @@ -71,7 +71,6 @@ public void Should_Be_A_Nav() public void Should_Highlight_Current_Page(int currentPage) { // arrange - var stringWriter = new StringWriter(); var tagHelper = new GovUkPaginationTagHelper { PageCount = 2, @@ -82,8 +81,7 @@ public void Should_Highlight_Current_Page(int currentPage) // act tagHelper.Process(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); var match = regex.Match(actual); // assert @@ -94,7 +92,6 @@ public void Should_Highlight_Current_Page(int currentPage) public void Should_Not_Highlight_Next_Page() { // arrange - var stringWriter = new StringWriter(); var tagHelper = new GovUkPaginationTagHelper { PageCount = 2, @@ -105,8 +102,7 @@ public void Should_Not_Highlight_Next_Page() // act tagHelper.Process(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); var match = regex.Match(actual); // assert @@ -117,7 +113,6 @@ public void Should_Not_Highlight_Next_Page() public void Shows_Next_Link() { // arrange - var stringWriter = new StringWriter(); var tagHelper = new GovUkPaginationTagHelper { PageCount = 2, @@ -127,8 +122,7 @@ public void Shows_Next_Link() // act tagHelper.Process(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().Contain("HtmlEncode[[Next]]"); @@ -138,7 +132,6 @@ public void Shows_Next_Link() public void Does_Not_Show_Previous_Link_On_First_Page() { // arrange - var stringWriter = new StringWriter(); var tagHelper = new GovUkPaginationTagHelper { PageCount = 2, @@ -148,8 +141,7 @@ public void Does_Not_Show_Previous_Link_On_First_Page() // act tagHelper.Process(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().NotContain("HtmlEncode[[Previous]]"); @@ -159,7 +151,6 @@ public void Does_Not_Show_Previous_Link_On_First_Page() public void Shows_Previous_Link() { // arrange - var stringWriter = new StringWriter(); var tagHelper = new GovUkPaginationTagHelper { PageCount = 2, @@ -169,8 +160,7 @@ public void Shows_Previous_Link() // act tagHelper.Process(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().Contain("HtmlEncode[[Previous]]"); @@ -180,7 +170,6 @@ public void Shows_Previous_Link() public void Does_Not_Show_Next_Link_On_Last_Page() { // arrange - var stringWriter = new StringWriter(); var tagHelper = new GovUkPaginationTagHelper { PageCount = 2, @@ -190,8 +179,7 @@ public void Does_Not_Show_Next_Link_On_Last_Page() // act tagHelper.Process(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().NotContain("HtmlEncode[[Next]]"); @@ -201,7 +189,6 @@ public void Does_Not_Show_Next_Link_On_Last_Page() public void Ellipses_Are_Generated() { // arrange - var stringWriter = new StringWriter(); var tagHelper = new GovUkPaginationTagHelper { PageCount = 4, @@ -211,8 +198,7 @@ public void Ellipses_Are_Generated() // act tagHelper.Process(_tagHelperContext, _tagHelperOutput); - _tagHelperOutput.WriteTo(stringWriter, new HtmlTestEncoder()); - var actual = stringWriter.ToString(); + var actual = _tagHelperOutput.AsString(); // assert actual.Should().Contain("⋯"); diff --git a/Childrens-Social-Care-CPD/Contentful/EntityResolver.cs b/Childrens-Social-Care-CPD/Contentful/EntityResolver.cs index 7d947dd2..708d7e81 100644 --- a/Childrens-Social-Care-CPD/Contentful/EntityResolver.cs +++ b/Childrens-Social-Care-CPD/Contentful/EntityResolver.cs @@ -26,14 +26,12 @@ public Type Resolve(string contentTypeId) "detailedRole" => typeof(DetailedRole), "heroBanner" => typeof(HeroBanner), "imageCard" => typeof(ImageCard), - "imageResource" => typeof(ImageResource), "linkCard" => typeof(LinkCard), "linkListCard" => typeof(LinkListCard), "pdfFileResource" => typeof(PdfFileResource), - "resource" => typeof(Resource), "richTextBlock" => typeof(RichTextBlock), "roleList" => typeof(RoleList), - "sideMenu" => typeof(SideMenu), + "navigationMenu" => typeof(NavigationMenu), "textBlock" => typeof(TextBlock), "videoResource" => typeof(VideoResource), _ => null diff --git a/Childrens-Social-Care-CPD/Contentful/Models/Content.cs b/Childrens-Social-Care-CPD/Contentful/Models/Content.cs index 87e486c2..06e48d4f 100644 --- a/Childrens-Social-Care-CPD/Contentful/Models/Content.cs +++ b/Childrens-Social-Care-CPD/Contentful/Models/Content.cs @@ -1,17 +1,28 @@ using Contentful.Core.Models; +using Newtonsoft.Json; namespace Childrens_Social_Care_CPD.Contentful.Models; +public static class ContentTypes +{ + public const string Resource = "Resource"; +} + public class Content : IContent { public string Id { get; set; } + public string ContentType { get; set; } public string Title { get; set; } public string ContentTitle { get; set; } public string ContentSubtitle { get; set; } public bool ShowContentHeader { get; set; } public string Category { get; set; } public ContentLink BackLink { get; set; } - public SideMenu SideMenu { get; set; } public List Items { get; set; } + public NavigationMenu Navigation { get; set; } public RelatedContent RelatedContent { get; set; } + + [JsonProperty("$metadata")] + public ContentfulMetadata Metadata { get; set; } + public SystemProperties Sys { get; set; } } \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Contentful/Models/ImageResource.cs b/Childrens-Social-Care-CPD/Contentful/Models/ImageResource.cs deleted file mode 100644 index 0ef13286..00000000 --- a/Childrens-Social-Care-CPD/Contentful/Models/ImageResource.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Contentful.Core.Models; - -namespace Childrens_Social_Care_CPD.Contentful.Models; - -public class ImageResource : IContent -{ - public string Id { get; set; } - public Asset Image { get; set; } -} diff --git a/Childrens-Social-Care-CPD/Contentful/Models/SideMenu.cs b/Childrens-Social-Care-CPD/Contentful/Models/NavigationMenu.cs similarity index 82% rename from Childrens-Social-Care-CPD/Contentful/Models/SideMenu.cs rename to Childrens-Social-Care-CPD/Contentful/Models/NavigationMenu.cs index 4cbde799..08c520ef 100644 --- a/Childrens-Social-Care-CPD/Contentful/Models/SideMenu.cs +++ b/Childrens-Social-Care-CPD/Contentful/Models/NavigationMenu.cs @@ -2,7 +2,7 @@ namespace Childrens_Social_Care_CPD.Contentful.Models; -public class SideMenu: IContent +public class NavigationMenu: IContent { public string Name { get; set; } public List Items { get; set; } diff --git a/Childrens-Social-Care-CPD/Contentful/Models/Resource.cs b/Childrens-Social-Care-CPD/Contentful/Models/Resource.cs deleted file mode 100644 index ed7b1e5a..00000000 --- a/Childrens-Social-Care-CPD/Contentful/Models/Resource.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Contentful.Core.Models; -using Newtonsoft.Json; - -namespace Childrens_Social_Care_CPD.Contentful.Models; - -public class Resource : IContent -{ - public string Id { get; set; } - public string Title { get; set; } - public string From { get; set; } - public string Summary { get; set; } - public string Label { get; set; } - public string SearchSummary { get; set; } - public List Items { get; set; } - - // need these for queries - [JsonProperty("$metadata")] - public ContentfulMetadata Metadata { get; set; } - public SystemProperties Sys { get; set; } -} diff --git a/Childrens-Social-Care-CPD/Contentful/PartialsFactory.cs b/Childrens-Social-Care-CPD/Contentful/PartialsFactory.cs index 44fba8b8..fde2be51 100644 --- a/Childrens-Social-Care-CPD/Contentful/PartialsFactory.cs +++ b/Childrens-Social-Care-CPD/Contentful/PartialsFactory.cs @@ -25,14 +25,12 @@ public static string GetPartialFor(IContent item) DetailedPathway => "_DetailedPathway", HeroBanner => string.Empty,// skip - handled in specific layout section ImageCard => "_ImageCard", - ImageResource => "_ImageResource", LinkCard => "_LinkCard", LinkListCard => "_LinkListCard", PdfFileResource => "_PdfFileResource", - Resource => "_Resource", RichTextBlock => "_RichTextBlock", RoleList => "_RoleList", - SideMenu => "_SideMenu", + NavigationMenu => "_NavigationMenu", TextBlock => "_TextBlock", VideoResource => "_VideoResource", _ => "_UnknownContentWarning", diff --git a/Childrens-Social-Care-CPD/Contentful/Renderers/AssetStructureRenderer.cs b/Childrens-Social-Care-CPD/Contentful/Renderers/AssetStructureRenderer.cs new file mode 100644 index 00000000..53e28113 --- /dev/null +++ b/Childrens-Social-Care-CPD/Contentful/Renderers/AssetStructureRenderer.cs @@ -0,0 +1,41 @@ +using Contentful.Core.Models; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Childrens_Social_Care_CPD.Contentful.Renderers; + +public class AssetStructureRenderer : IRenderer +{ + public IHtmlContent Render(AssetStructure item) + { + switch (item.Data) + { + case AssetStructureData assetStructureData: + { + switch (assetStructureData.Target) + { + case Asset asset: + { + var contentType = asset.File?.ContentType.ToLower(); + + if (contentType?.StartsWith("image/") ?? false) + { + var img = new TagBuilder("img"); + img.Attributes.Add("src", asset.File.Url); + if (!string.IsNullOrEmpty(asset.Description)) + { + img.Attributes.Add("alt", asset.Description); + } + + return img; + } + break; + } + } + break; + } + } + + return null; + } +} diff --git a/Childrens-Social-Care-CPD/Controllers/ContentController.cs b/Childrens-Social-Care-CPD/Controllers/ContentController.cs index 328b200e..b1e0c622 100644 --- a/Childrens-Social-Care-CPD/Controllers/ContentController.cs +++ b/Childrens-Social-Care-CPD/Controllers/ContentController.cs @@ -45,23 +45,24 @@ private async Task FetchPageContentAsync(string contentId, Cancellation public async Task Index(CancellationToken cancellationToken, string pageName = "home", bool preferenceSet = false) { pageName = pageName?.TrimEnd('/'); - var pageContent = await FetchPageContentAsync(pageName, cancellationToken); - if (pageContent == null) + var content = await FetchPageContentAsync(pageName, cancellationToken); + if (content == null) { return NotFound(); } var contextModel = new ContextModel( - Id: pageContent.Id, - Title: pageContent.Title, + Id: content.Id, + Title: content.Title, PageName: pageName, - Category: pageContent.Category, - UseContainers: pageContent.SideMenu == null, + Category: content.Category, + UseContainers: content.Navigation == null, PreferenceSet: preferenceSet, - BackLink: pageContent.BackLink); + BackLink: content.BackLink); ViewData["ContextModel"] = contextModel; ViewData["StateModel"] = new StateModel(); - return View(pageContent); + + return View(content); } } \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs b/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs index 6b4b5740..353642c5 100644 --- a/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs +++ b/Childrens-Social-Care-CPD/Controllers/ResourcesController.cs @@ -1,8 +1,8 @@ using Childrens_Social_Care_CPD.Configuration; using Childrens_Social_Care_CPD.Core.Resources; +using Childrens_Social_Care_CPD.DataAccess; using Childrens_Social_Care_CPD.Models; using Microsoft.AspNetCore.Mvc; -using Childrens_Social_Care_CPD.Extensions; namespace Childrens_Social_Care_CPD.Controllers; @@ -18,7 +18,7 @@ public class ResourcesQuery public int Page { get; set; } = 1; public ResourceSortOrder SortOrder { get; set; } -public ResourcesQuery() + public ResourcesQuery() { Tags = Array.Empty(); } @@ -28,12 +28,14 @@ public class ResourcesController : Controller { private readonly IFeaturesConfig _featuresConfig; private readonly IResourcesSearchStrategy _strategy; + private readonly IResourcesRepository _resourcesRepository; - public ResourcesController(IFeaturesConfig featuresConfig, IResourcesSearchStrategy strategy) + public ResourcesController(IFeaturesConfig featuresConfig, IResourcesSearchStrategy strategy, IResourcesRepository resourcesRepository) { ArgumentNullException.ThrowIfNull(strategy); _featuresConfig = featuresConfig; _strategy = strategy; + _resourcesRepository = resourcesRepository; } [Route("resources-learning")] @@ -51,4 +53,48 @@ public async Task Search([FromQuery] ResourcesQuery query, bool p var viewModel = await _strategy.SearchAsync(query, cancellationToken); return View(viewModel); } + + [Route("resources-learning/{*pagename:regex(^[[0-9a-z]](\\/?[[0-9a-z\\-]])*\\/?$)}")] + public async Task Index(string pageName = "home", bool preferenceSet = false, CancellationToken cancellationToken = default) + { + if (!_featuresConfig.IsEnabled(Features.ResourcesAndLearning)) + { + return NotFound(); + } + + pageName = $"resources-learning/{pageName?.TrimEnd('/')}"; + (var content, var tags) = await _resourcesRepository.GetByIdAsync(pageName, cancellationToken: cancellationToken); + if (content == null) + { + return NotFound(); + } + + var properties = new Dictionary(tags.ContentCollection.Items.First().ContentfulMetaData.Tags.Where(x => x.Name.StartsWith("Resource:")).Select(x => + { + var property = x.Name[9..]; + var tokens = property.Split('='); + return tokens.Length > 1 + ? KeyValuePair.Create(tokens[0].Trim(' '), tokens[1].Trim(' ')) + : KeyValuePair.Create(property, string.Empty); + })) + { + { "Published", content.Sys.CreatedAt?.ToString("dd MMMM yyyy") }, + { "Last updated", content.Sys.UpdatedAt?.ToString("dd MMMM yyyy") } + }; + + var contextModel = new ContextModel( + Id: content.Id, + Title: content.Title, + PageName: pageName, + Category: content.Category, + UseContainers: content.Navigation == null, + PreferenceSet: preferenceSet, + BackLink: content.BackLink); + + ViewData["ContextModel"] = contextModel; + ViewData["StateModel"] = new StateModel(); + ViewData["Properties"] = properties; + + return View("Resource", content); + } } diff --git a/Childrens-Social-Care-CPD/Core/Resources/ResourcesDynamicTagsSearchStategy.cs b/Childrens-Social-Care-CPD/Core/Resources/ResourcesDynamicTagsSearchStategy.cs index 98b708ac..70bb49e3 100644 --- a/Childrens-Social-Care-CPD/Core/Resources/ResourcesDynamicTagsSearchStategy.cs +++ b/Childrens-Social-Care-CPD/Core/Resources/ResourcesDynamicTagsSearchStategy.cs @@ -1,6 +1,5 @@ using Childrens_Social_Care_CPD.Controllers; using Childrens_Social_Care_CPD.DataAccess; -using Childrens_Social_Care_CPD.Extensions; using Childrens_Social_Care_CPD.GraphQL.Queries; using Childrens_Social_Care_CPD.Models; @@ -33,7 +32,7 @@ private static IEnumerable SanitiseTags(IEnumerable tags, HashSe private static Tuple CalculatePageStats(SearchResourcesByTags.ResponseType searchResults, int page) { - var totalResults = searchResults?.ResourceCollection?.Total ?? 0; + var totalResults = searchResults?.ContentCollection?.Total ?? 0; var totalPages = (int)Math.Ceiling((decimal)totalResults / PAGE_SIZE); return Tuple.Create(totalResults, totalPages, Math.Min(page, totalPages)); @@ -75,7 +74,7 @@ public async Task SearchAsync(ResourcesQuery query, Canc return new ResourcesListViewModel( pageContent, - searchResults?.ResourceCollection, + searchResults?.ContentCollection, tagInfos, query.Tags, (int)query.SortOrder, diff --git a/Childrens-Social-Care-CPD/Core/Resources/ResourcesFixedTagsSearchStrategy.cs b/Childrens-Social-Care-CPD/Core/Resources/ResourcesFixedTagsSearchStrategy.cs index c285ae24..e6680f8b 100644 --- a/Childrens-Social-Care-CPD/Core/Resources/ResourcesFixedTagsSearchStrategy.cs +++ b/Childrens-Social-Care-CPD/Core/Resources/ResourcesFixedTagsSearchStrategy.cs @@ -1,6 +1,5 @@ using Childrens_Social_Care_CPD.Controllers; using Childrens_Social_Care_CPD.DataAccess; -using Childrens_Social_Care_CPD.Extensions; using Childrens_Social_Care_CPD.GraphQL.Queries; using Childrens_Social_Care_CPD.Models; @@ -46,7 +45,7 @@ private IEnumerable GetQueryTags(int[] tags) private static Tuple CalculatePageStats(SearchResourcesByTags.ResponseType searchResults, int page) { - var totalResults = searchResults?.ResourceCollection?.Total ?? 0; + var totalResults = searchResults?.ContentCollection?.Total ?? 0; var totalPages = (int)Math.Ceiling((decimal)totalResults / PAGE_SIZE); return Tuple.Create(totalResults, totalPages, Math.Min(page, totalPages)); @@ -84,7 +83,7 @@ public async Task SearchAsync(ResourcesQuery query, Canc return new ResourcesListViewModel( pageContent, - searchResults?.ResourceCollection, + searchResults?.ContentCollection, _tagInfos, queryTags.Select(x => x.ToString()), (int)query.SortOrder, diff --git a/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs b/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs index 660dc84b..fbb26d93 100644 --- a/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs +++ b/Childrens-Social-Care-CPD/DataAccess/ResourcesRepository.cs @@ -6,7 +6,6 @@ using Childrens_Social_Care_CPD.GraphQL.Queries; using Contentful.Core.Search; using GraphQL.Client.Abstractions.Websocket; -using System.Diagnostics; namespace Childrens_Social_Care_CPD.DataAccess; @@ -15,6 +14,7 @@ public interface IResourcesRepository Task FetchRootPageAsync(CancellationToken cancellationToken = default); Task FindByTagsAsync(IEnumerable tags, int skip, int take, ResourceSortOrder resourceSortOrder, CancellationToken cancellationToken = default); Task> GetSearchTagsAsync(); + Task> GetByIdAsync(string id, int depth = 10, CancellationToken cancellationToken = default); } public class ResourcesRepository : IResourcesRepository @@ -52,6 +52,25 @@ public Task FetchRootPageAsync(CancellationToken cancellationToken = de .ContinueWith(x => x.Result.Data); } + public async Task> GetByIdAsync(string id, int depth = 10, CancellationToken cancellationToken = default) + { + var queryBuilder = QueryBuilder.New + .ContentTypeIs("content") + .Include(depth) + .FieldEquals("fields.id", id); + + var tagsTask = _gqlClient + .SendQueryAsync(GetContentTags.Query(id, _isPreview), cancellationToken) + .ContinueWith(x => x.Result.Data); + + var contentTask = _cpdClient + .GetEntries(queryBuilder, cancellationToken) + .ContinueWith(x => x.Result.FirstOrDefault()); + + await Task.WhenAll(contentTask, tagsTask); + return Tuple.Create(contentTask.Result, tagsTask.Result); + } + public async Task> GetSearchTagsAsync() { var allTags = await _cpdClient.GetTags(); diff --git a/Childrens-Social-Care-CPD/Extensions/EnumExtension.cs b/Childrens-Social-Care-CPD/Extensions/EnumExtension.cs deleted file mode 100644 index 6687b5c5..00000000 --- a/Childrens-Social-Care-CPD/Extensions/EnumExtension.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Childrens_Social_Care_CPD.Extensions -{ - public static class EnumExtension - { - public static T ToEnum(this string enumString) - { - return (T)Enum.Parse(typeof(T), enumString); - } - } -} diff --git a/Childrens-Social-Care-CPD/GraphQL/Queries/GetContentTags.cs b/Childrens-Social-Care-CPD/GraphQL/Queries/GetContentTags.cs new file mode 100644 index 00000000..110d95d5 --- /dev/null +++ b/Childrens-Social-Care-CPD/GraphQL/Queries/GetContentTags.cs @@ -0,0 +1,78 @@ +using GraphQL; +using System.Text.Json.Serialization; + +namespace Childrens_Social_Care_CPD.GraphQL.Queries; + +public class GetContentTags +{ + public static GraphQLRequest Query(string id, bool preview = false) + { + return new GraphQLRequest + { + Query = @" + query GetContentTags($id: String!, $preview: Boolean) { + contentCollection(where: { + id : $id, + }, preview: $preview) { + total + items { + id + sys { + publishedAt + firstPublishedAt + } + contentfulMetadata { + tags { + id + name + } + } + } + } + }", + OperationName = "GetContentTags", + Variables = new + { + id, + preview, + } + }; + } + + public class ResponseType + { + [JsonPropertyName("contentCollection")] + public ContentCollection ContentCollection { get; set; } + } + + public class ContentCollection + { + [JsonPropertyName("items")] + public ICollection Items { get; set; } + public int Total { get; set; } + } + + public class ContentItem + { + public string Id { get; set; } + public PublishedInfo Sys { get; set; } + public MetaData ContentfulMetaData { get; set; } + } + + public class PublishedInfo + { + public DateTime? PublishedAt { get; set; } + public DateTime? FirstPublishedAt { get; set; } + } + + public class MetaData + { + public List Tags { get; set; } + } + + public class Tag + { + public string Id { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/GraphQL/Queries/SearchResourcesByTags.cs b/Childrens-Social-Care-CPD/GraphQL/Queries/SearchResourcesByTags.cs index 8371962a..8486b711 100644 --- a/Childrens-Social-Care-CPD/GraphQL/Queries/SearchResourcesByTags.cs +++ b/Childrens-Social-Care-CPD/GraphQL/Queries/SearchResourcesByTags.cs @@ -10,8 +10,9 @@ public static GraphQLRequest Query(IEnumerable tags, int limit, int skip return new GraphQLRequest { Query = @" - query SearchResourcesByTags($searchTags: [String!], $limit: Int, $skip: Int, $order: [ResourceOrder], $preview: Boolean) { - resourceCollection(where: { + query SearchResourcesByTags($searchTags: [String!], $limit: Int, $skip: Int, $order: [ContentOrder], $preview: Boolean) { + contentCollection(where: { + contentType: ""Resource"" contentfulMetadata: { tags_exists: true tags: { @@ -21,19 +22,17 @@ query SearchResourcesByTags($searchTags: [String!], $limit: Int, $skip: Int, $or }, limit: $limit, skip: $skip, order: $order, preview: $preview) { total items { - title - from + id + contentTitle searchSummary - label sys { publishedAt firstPublishedAt } - linkedFrom { - contentCollection { - items { - id - } + contentfulMetadata { + tags { + id + name } } } @@ -53,11 +52,11 @@ query SearchResourcesByTags($searchTags: [String!], $limit: Int, $skip: Int, $or public class ResponseType { - [JsonPropertyName("resourceCollection")] - public ResourceCollection ResourceCollection { get; set; } + [JsonPropertyName("contentCollection")] + public ContentCollection ContentCollection { get; set; } } - public class ResourceCollection + public class ContentCollection { [JsonPropertyName("items")] public ICollection Items { get; set; } @@ -66,12 +65,11 @@ public class ResourceCollection public class SearchResult { - public string Title { get; set; } - public string From { get; set; } + public string Id { get; set; } + public string ContentTitle { get; set; } public string SearchSummary { get; set; } - public string Label { get; set; } public PublishedInfo Sys { get; set; } - public LinkedFromContentCollection LinkedFrom { get; set; } + public MetaData ContentfulMetaData { get; set; } } public class PublishedInfo @@ -80,21 +78,14 @@ public class PublishedInfo public DateTime? FirstPublishedAt { get; set; } } - public class LinkedFromContentCollection - { - public LinkedFrom ContentCollection { get; set; } - } - - public class LinkedFrom + public class MetaData { - public ICollection Items { get; set; } + public List Tags { get; set; } } - public class LinkedItem + public class Tag { public string Id { get; set; } + public string Name { get; set; } } -} - - - +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Models/ResourcesListViewModel.cs b/Childrens-Social-Care-CPD/Models/ResourcesListViewModel.cs index 785069f2..0f8382f7 100644 --- a/Childrens-Social-Care-CPD/Models/ResourcesListViewModel.cs +++ b/Childrens-Social-Care-CPD/Models/ResourcesListViewModel.cs @@ -1,5 +1,4 @@ using Childrens_Social_Care_CPD.Contentful.Models; -using Childrens_Social_Care_CPD.Controllers; using Childrens_Social_Care_CPD.Core.Resources; using Childrens_Social_Care_CPD.GraphQL.Queries; @@ -12,10 +11,9 @@ public static class ResourceSort public const string MostViewed = "Most viewed"; } - public record ResourcesListViewModel( Content Content, - SearchResourcesByTags.ResourceCollection Results, + SearchResourcesByTags.ContentCollection Results, IEnumerable TagInfos, IEnumerable SelectedTags, int SortOrder = 0, diff --git a/Childrens-Social-Care-CPD/TagHelpers/CpdPropertiesList.cs b/Childrens-Social-Care-CPD/TagHelpers/CpdPropertiesList.cs new file mode 100644 index 00000000..70723029 --- /dev/null +++ b/Childrens-Social-Care-CPD/TagHelpers/CpdPropertiesList.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Text.Encodings.Web; + +namespace Childrens_Social_Care_CPD.TagHelpers; + +[HtmlTargetElement(TagName)] +[OutputElementHint("ul")] +public class CpdPropertiesList : TagHelper +{ + internal const string TagName = "cpd-properties-list"; + + [HtmlAttributeName("items")] + public IReadOnlyDictionary Items { get; set; } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (Items == null || Items.Count == 0 ) + { + output.SuppressOutput(); + return; + } + + output.TagName = "ul"; + output.TagMode = TagMode.StartTagAndEndTag; + output.AddClass("course-details", HtmlEncoder.Default); + output.AddClass("govuk-list", HtmlEncoder.Default); + + foreach (var item in Items) + { + var span = new TagBuilder("span"); + span.AddCssClass("govuk-!-font-size-16"); + span.InnerHtml.Append($"{item.Key}: {item.Value}"); + + var li = new TagBuilder("li"); + li.InnerHtml.AppendHtml(span); + + output.Content.AppendHtml(li); + } + } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/TagHelpers/CpdResourceNav.cs b/Childrens-Social-Care-CPD/TagHelpers/CpdResourceNav.cs new file mode 100644 index 00000000..8b7ef6e0 --- /dev/null +++ b/Childrens-Social-Care-CPD/TagHelpers/CpdResourceNav.cs @@ -0,0 +1,75 @@ +using Childrens_Social_Care_CPD.Contentful.Models; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Text.Encodings.Web; + +namespace Childrens_Social_Care_CPD.TagHelpers; + +[HtmlTargetElement(TagName)] +[OutputElementHint("nav")] +public class CpdResourceNav : TagHelper +{ + internal const string TagName = "cpd-resource-nav"; + + [HtmlAttributeName("navigation")] + public IList Navigation { get; set; } + + [HtmlAttributeName("selected")] + public string Selected { get; set; } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (Navigation == null || Navigation.Count == 0) + { + output.SuppressOutput(); + return; + } + + output.TagName = "nav"; + output.TagMode = TagMode.StartTagAndEndTag; + output.AddClass("gem-c-contents-list", HtmlEncoder.Default); + output.Attributes.Add("role", "navigation"); + output.Attributes.Add("aria-label", "Resource pages"); + + var h2 = new TagBuilder("h2"); + h2.AddCssClass("gem-c-contents-list__title"); + h2.InnerHtml.Append("Content"); + + output.Content.AppendHtml(h2); + output.Content.AppendHtml(RenderNavigation(Navigation, Selected)); + } + + private static IHtmlContent RenderNavigation(IList navigation, string selected) + { + var ol = new TagBuilder("ol"); + ol.AddCssClass("gem-c-contents-list__list"); + + foreach (var contentLink in navigation) + { + var li = new TagBuilder("li"); + li.AddCssClass("gem-c-contents-list__list-item gem-c-contents-list__list-item--dashed"); + + var uri = contentLink.Uri.TrimStart('/'); + if (uri == selected) + { + li.AddCssClass("gem-c-contents-list__list-item--active"); + li.Attributes.Add("aria-current", "true"); + li.InnerHtml.Append(contentLink.Name); + } + else + { + var a = new TagBuilder("a"); + a.AddCssClass("gem-c-contents-list__link govuk-link"); + a.Attributes.Add("href", $"/{uri}"); + a.InnerHtml.Append(contentLink.Name); + li.InnerHtml.AppendHtml(a); + } + + ol.InnerHtml.AppendHtml(li); + } + + return ol; + } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/TagHelpers/CpdResourcePageNav.cs b/Childrens-Social-Care-CPD/TagHelpers/CpdResourcePageNav.cs new file mode 100644 index 00000000..0580f1e8 --- /dev/null +++ b/Childrens-Social-Care-CPD/TagHelpers/CpdResourcePageNav.cs @@ -0,0 +1,153 @@ +using Childrens_Social_Care_CPD.Contentful.Models; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Text.Encodings.Web; + +namespace Childrens_Social_Care_CPD.TagHelpers; + +[HtmlTargetElement(TagName)] +[OutputElementHint("nav")] +public class CpdResourcePageNav : TagHelper +{ + internal const string TagName = "cpd-resource-page-nav"; + + [HtmlAttributeName("navigation")] + public List Navigation { get; set; } + + [HtmlAttributeName("current")] + public string Current { get; set; } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(Current); + + if (Navigation == null || Navigation.Count == 0) + { + output.SuppressOutput(); + return; + } + + output.TagName = "nav"; + output.TagMode = TagMode.StartTagAndEndTag; + output.AddClass("govuk-pagination", HtmlEncoder.Default); + output.AddClass("govuk-pagination--block", HtmlEncoder.Default); + output.Attributes.Add("role", "navigation"); + output.Attributes.Add("aria-label", "pagination"); + + var index = Navigation.FindIndex(x => x.Uri.TrimStart('/') == Current); + + if (index > 0) + { + output.Content.AppendHtml(RenderPrevious(Navigation[index - 1])); + } + + if (index < Navigation.Count-1) + { + output.Content.AppendHtml(RenderNext(Navigation[index + 1])); + } + } + + private static IHtmlContent RenderNext(ContentLink contentLink) + { + var span1 = new TagBuilder("span"); + span1.AddCssClass("govuk-pagination__link-title"); + span1.InnerHtml.AppendHtml("Next"); + + var span2 = new TagBuilder("span"); + span2.AddCssClass("govuk-visually-hidden"); + span2.InnerHtml.AppendHtml(":"); + + var span3 = new TagBuilder("span"); + span3.AddCssClass("govuk-pagination__link-label"); + span3.InnerHtml.AppendHtml(contentLink.Name); + + var uri = contentLink.Uri.TrimStart('/'); + var anchor = new TagBuilder("a"); + anchor.AddCssClass("govuk-link govuk-pagination__link"); + anchor.Attributes.Add("href", $"/{uri}"); + anchor.Attributes.Add("rel", "next"); + anchor.InnerHtml.AppendHtml(ArrowRight()); + anchor.InnerHtml.AppendLine(); // This is here to fix a CSS rendering bug in Chrome + anchor.InnerHtml.AppendHtml(span1); + anchor.InnerHtml.AppendHtml(span2); + anchor.InnerHtml.AppendHtml(span3); + + var div = new TagBuilder("div"); + div.AddCssClass("govuk-pagination__next"); + div.InnerHtml.AppendHtml(anchor); + + return div; + } + + private static IHtmlContent RenderPrevious(ContentLink contentLink) + { + var span1 = new TagBuilder("span"); + span1.AddCssClass("govuk-pagination__link-title"); + span1.InnerHtml.AppendHtml("Previous"); + + var span2 = new TagBuilder("span"); + span2.AddCssClass("govuk-visually-hidden"); + span2.InnerHtml.AppendHtml(":"); + + var span3 = new TagBuilder("span"); + span3.AddCssClass("govuk-pagination__link-label"); + span3.InnerHtml.AppendHtml(contentLink.Name); + + var uri = contentLink.Uri.TrimStart('/'); + var anchor = new TagBuilder("a"); + anchor.AddCssClass("govuk-link govuk-pagination__link"); + anchor.Attributes.Add("href", $"/{uri}"); + anchor.Attributes.Add("rel", "prev"); + anchor.InnerHtml.AppendHtml(ArrowLeft()); + anchor.InnerHtml.AppendLine(); // This is here to fix a CSS rendering bug in Chrome + anchor.InnerHtml.AppendHtml(span1); + anchor.InnerHtml.AppendHtml(span2); + anchor.InnerHtml.AppendHtml(span3); + + var div = new TagBuilder("div"); + div.AddCssClass("govuk-pagination__prev"); + div.InnerHtml.AppendHtml(anchor); + + return div; + } + + private static IHtmlContent ArrowRight() + { + var path = new TagBuilder("path"); + path.Attributes.Add("d", "m8.107-0.0078125-1.4136 1.414 4.2926 4.293h-12.986v2h12.896l-4.1855 3.9766 1.377 1.4492 6.7441-6.4062-6.7246-6.7266z"); + + var svg = new TagBuilder("svg"); + svg.AddCssClass("govuk-pagination__icon govuk-pagination__icon--next"); + svg.Attributes.Add("xmlns", "http://www.w3.org/2000/svg"); + svg.Attributes.Add("height", "13"); + svg.Attributes.Add("width", "15"); + svg.Attributes.Add("aria-hidden", "true"); + svg.Attributes.Add("focusable", "false"); + svg.Attributes.Add("viewBox", "0 0 15 13"); + + svg.InnerHtml.AppendHtml(path); + svg.InnerHtml.AppendLine(); + return svg; + } + + private static IHtmlContent ArrowLeft() + { + var path = new TagBuilder("path"); + path.Attributes.Add("d", "m6.5938-0.0078125-6.7266 6.7266 6.7441 6.4062 1.377-1.449-4.1856-3.9768h12.896v-2h-12.984l4.2931-4.293-1.414-1.414z"); + + var svg = new TagBuilder("svg"); + svg.AddCssClass("govuk-pagination__icon govuk-pagination__icon--prev"); + svg.Attributes.Add("xmlns", "http://www.w3.org/2000/svg"); + svg.Attributes.Add("height", "13"); + svg.Attributes.Add("width", "15"); + svg.Attributes.Add("aria-hidden", "true"); + svg.Attributes.Add("focusable", "false"); + svg.Attributes.Add("viewBox", "0 0 15 13"); + + svg.InnerHtml.AppendHtml(path); + return svg; + } + +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Views/Content/Index.cshtml b/Childrens-Social-Care-CPD/Views/Content/Index.cshtml index 64646091..4ca8b6e9 100644 --- a/Childrens-Social-Care-CPD/Views/Content/Index.cshtml +++ b/Childrens-Social-Care-CPD/Views/Content/Index.cshtml @@ -8,7 +8,7 @@ @inject IRenderer _relatedContentRenderer @{ - Layout = Model.SideMenu == null + Layout = Model.Navigation == null ? "_DefaultPageLayout" : "_SubPageLayout"; } @@ -31,9 +31,9 @@ } @section SideMenu { - @if (Model.SideMenu != null) + @if (Model.Navigation != null) { - + } } diff --git a/Childrens-Social-Care-CPD/Views/Resources/Resource.cshtml b/Childrens-Social-Care-CPD/Views/Resources/Resource.cshtml new file mode 100644 index 00000000..27a4cdad --- /dev/null +++ b/Childrens-Social-Care-CPD/Views/Resources/Resource.cshtml @@ -0,0 +1,54 @@ +@using Childrens_Social_Care_CPD.Contentful.Models; +@using Childrens_Social_Care_CPD.Contentful.Renderers; +@using Contentful.Core.Models; + +@model Content; + +@inject IRenderer _relatedContentRenderer + +@{ + Layout = "_DefaultPageLayout"; +} + +@section Head { + @if (ViewData.TryGetValue("StateModel", out var stateModel) && ((StateModel)stateModel).IncludeMediaPlayer) + { + + } +} + +
    +
    + Resources +

    @Model.ContentTitle

    + + @if (Model.ContentSubtitle != null) + { +

    @Model.ContentSubtitle

    + } + + @if (Model.Navigation != null) + { + + } +
    +
    +
    + + + + + + + +@_relatedContentRenderer.Render(Model.RelatedContent) + +@section Scripts { + @if (ViewData.TryGetValue("StateModel", out var stateModel) && ((StateModel)stateModel).IncludeMediaPlayer) + { + + + } +} \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Views/Resources/Search.cshtml b/Childrens-Social-Care-CPD/Views/Resources/Search.cshtml index 126e8172..6bae3058 100644 --- a/Childrens-Social-Care-CPD/Views/Resources/Search.cshtml +++ b/Childrens-Social-Care-CPD/Views/Resources/Search.cshtml @@ -125,9 +125,6 @@
      @foreach (var contentItem in Model.Results.Items) { - var linkedFrom = contentItem.LinkedFrom.ContentCollection.Items.FirstOrDefault(); - if (linkedFrom == null) continue; - }
    diff --git a/Childrens-Social-Care-CPD/Views/Resources/_SearchResult.cshtml b/Childrens-Social-Care-CPD/Views/Resources/_SearchResult.cshtml index cd7db6bb..f4572134 100644 --- a/Childrens-Social-Care-CPD/Views/Resources/_SearchResult.cshtml +++ b/Childrens-Social-Care-CPD/Views/Resources/_SearchResult.cshtml @@ -3,24 +3,37 @@ @model SearchResourcesByTags.SearchResult +@{ + var properties = new Dictionary(Model.ContentfulMetaData.Tags.Where(x => x.Name.StartsWith("Resource:")).Select(x => + { + var property = x.Name[9..]; + var tokens = property.Split('='); + return tokens.Length > 1 + ? KeyValuePair.Create(tokens[0].Trim(' '), tokens[1].Trim(' ')) + : KeyValuePair.Create(property, string.Empty); + })); +} +
  • -

    @Model.Title  - @if (!string.IsNullOrEmpty(@Model.Label)) +

    @Model.ContentTitle  + @if (properties.ContainsKey("Label")) { - @Model.Label + @properties["Label"] }

    @Model.SearchSummary

  • diff --git a/Childrens-Social-Care-CPD/Views/Shared/_ImageResource.cshtml b/Childrens-Social-Care-CPD/Views/Shared/_ImageResource.cshtml deleted file mode 100644 index a5555d0d..00000000 --- a/Childrens-Social-Care-CPD/Views/Shared/_ImageResource.cshtml +++ /dev/null @@ -1,13 +0,0 @@ -@using Childrens_Social_Care_CPD.Contentful.Models; - -@model ImageResource - -
    -
    -
    - - @Model.Image.Description - -
    -
    -
    \ No newline at end of file diff --git a/Childrens-Social-Care-CPD/Views/Shared/_SideMenu.cshtml b/Childrens-Social-Care-CPD/Views/Shared/_NavigationMenu.cshtml similarity index 96% rename from Childrens-Social-Care-CPD/Views/Shared/_SideMenu.cshtml rename to Childrens-Social-Care-CPD/Views/Shared/_NavigationMenu.cshtml index afc1a032..a963b7a4 100644 --- a/Childrens-Social-Care-CPD/Views/Shared/_SideMenu.cshtml +++ b/Childrens-Social-Care-CPD/Views/Shared/_NavigationMenu.cshtml @@ -1,6 +1,6 @@ @using Childrens_Social_Care_CPD.Contentful.Models; -@model SideMenu +@model NavigationMenu