From 5802839003eebf0d0726e1494886b6431ea02103 Mon Sep 17 00:00:00 2001 From: Marnix van Valen Date: Fri, 20 Nov 2020 06:23:47 +0100 Subject: [PATCH 01/10] First ideas --- .../Tools/KontentSetupHelpers.cs | 10 +++ .../When_working_with_taxonomy.cs | 74 +++++++++++++++++++ Kontent.Statiq/KontentTaxonomy.cs | 36 +++++++++ Kontent.Statiq/KontentTaxonomyHelpers.cs | 74 +++++++++++++++++++ Kontent.Statiq/TypedContentExtensions.cs | 2 + 5 files changed, 196 insertions(+) create mode 100644 Kontent.Statiq.Tests/When_working_with_taxonomy.cs create mode 100644 Kontent.Statiq/KontentTaxonomy.cs create mode 100644 Kontent.Statiq/KontentTaxonomyHelpers.cs diff --git a/Kontent.Statiq.Tests/Tools/KontentSetupHelpers.cs b/Kontent.Statiq.Tests/Tools/KontentSetupHelpers.cs index c0c3bd4..4344dd8 100644 --- a/Kontent.Statiq.Tests/Tools/KontentSetupHelpers.cs +++ b/Kontent.Statiq.Tests/Tools/KontentSetupHelpers.cs @@ -15,5 +15,15 @@ public static IDeliveryClient WithFakeContent(this IDeliveryClient cli return client; } + + public static IDeliveryClient WithFakeTaxonomy(this IDeliveryClient client, params ITaxonomyGroup[] taxonomies) + { + var response = A.Fake(); + A.CallTo(() => response.Taxonomies).Returns(taxonomies); + A.CallTo(() => client.GetTaxonomiesAsync(A>._)) + .Returns(response); + + return client; + } } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs new file mode 100644 index 0000000..8c9f625 --- /dev/null +++ b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs @@ -0,0 +1,74 @@ +using FakeItEasy; +using FluentAssertions; +using Kentico.Kontent.Delivery.Abstractions; +using Kontent.Statiq.Tests.Models; +using Kontent.Statiq.Tests.Tools; +using Statiq.Common; +using Statiq.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Kontent.Statiq.Tests +{ + public class When_working_with_taxonomy + { + [Fact] + public async Task It_should_load_taxonomy() + { + // Arrange + var group = setupTaxonomyGroup("Test", "Test1", "Test2"); + var deliveryClient = A.Fake().WithFakeTaxonomy( group ); + + var sut = new KontentTaxonomy(deliveryClient); + + // Act + var engine = SetupExecution(sut, + new IModule[]{ + + }, + // Assert + async docs => docs.FirstOrDefault().GetChildren().Should().HaveCount(2) + ); + await engine.ExecuteAsync(); + } + + private ITaxonomyGroup setupTaxonomyGroup(string name, params string[] terms) + { + return new TestTaxonomyGroup + { + System = A.Fake(), + Terms = terms.Select( t => new TestTaxonomyTerm{ Codename = t, Name = t }).Cast().ToList() + }; + } + + private static Engine SetupExecution(KontentTaxonomy kontentModule, IModule[] processModules, Func, Task> test) + { + var engine = new Engine(); + var pipeline = new Pipeline() + { + InputModules = { kontentModule }, + OutputModules = { new TestModule(test) } + }; + pipeline.ProcessModules.AddRange(processModules); + + engine.Pipelines.Add("test", pipeline); + return engine; + } + } + + internal class TestTaxonomyGroup : ITaxonomyGroup + { + public ITaxonomyGroupSystemAttributes System { get; set; } + public IList Terms { get; set; } + } + + internal class TestTaxonomyTerm : ITaxonomyTermDetails + { + public string Codename { get; set; } + public string Name { get; set; } + public IList Terms { get; set; } + } +} diff --git a/Kontent.Statiq/KontentTaxonomy.cs b/Kontent.Statiq/KontentTaxonomy.cs new file mode 100644 index 0000000..0c4eb94 --- /dev/null +++ b/Kontent.Statiq/KontentTaxonomy.cs @@ -0,0 +1,36 @@ +using Kentico.Kontent.Delivery.Abstractions; +using Statiq.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Kontent.Statiq +{ + public sealed class KontentTaxonomy : Module + { + private readonly IDeliveryClient _client; + + internal List QueryParameters { get; } = new List(); + + /// + /// Create a new instance of the KontentTaxonomy module for Statiq using an existing Kontent client. + /// + /// The Kontent client to use. + /// Thrown if is null. + public KontentTaxonomy(IDeliveryClient client) + { + _client = client ?? throw new ArgumentNullException(nameof(client), $"{nameof(client)} must not be null"); + } + + /// + protected override async Task> ExecuteContextAsync(IExecutionContext context) + { + var items = await _client.GetTaxonomiesAsync(QueryParameters); + + var documentTasks = items.Taxonomies.Select(item => KontentTaxonomyHelpers.CreateDocument(context, item)).ToArray(); + + return await Task.WhenAll(documentTasks); + } + } +} \ No newline at end of file diff --git a/Kontent.Statiq/KontentTaxonomyHelpers.cs b/Kontent.Statiq/KontentTaxonomyHelpers.cs new file mode 100644 index 0000000..2a02064 --- /dev/null +++ b/Kontent.Statiq/KontentTaxonomyHelpers.cs @@ -0,0 +1,74 @@ +using Kentico.Kontent.Delivery.Abstractions; +using Statiq.Common; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Kontent.Statiq +{ + internal class KontentTaxonomyHelpers + { + internal static async Task CreateDocument(IExecutionContext context, ITaxonomyGroup item) + { + var treePath = $"/{item.System.Codename}"; + + var metadata = new List> + { + new KeyValuePair(Keys.Title, item.System.Name), + new KeyValuePair(Keys.GroupKey, item.System.Codename), + new KeyValuePair(Keys.TreePath, treePath), + new KeyValuePair(TypedContentExtensions.KontentTaxonomyGroupKey, item) + }; + + MapSystemMetadata(item, metadata); + + if (item.Terms != null) + { + var terms = await item.Terms.ParallelSelectAsync(async t => await CreateDocument(context, t, treePath)); + metadata.Add(new KeyValuePair(Keys.Children, terms)); + } + + return await context.CreateDocumentAsync(metadata, "", "text/html"); + } + + internal static async Task CreateDocument(IExecutionContext context, ITaxonomyTermDetails item, string parentPath) + { + var treePath = $"{parentPath}/{item.Codename}"; + var metadata = new List> + { + new KeyValuePair(Keys.Title, item.Name), + new KeyValuePair(Keys.GroupKey, item.Codename), + new KeyValuePair(Keys.TreePath, treePath), + new KeyValuePair(KontentKeys.System.Name, item.Name), + new KeyValuePair(KontentKeys.System.CodeName, item.Codename), + }; + + if (item.Terms != null) + { + var terms = await item.Terms.ParallelSelectAsync(async t => await CreateDocument(context, t, treePath)); + metadata.Add(new KeyValuePair(Keys.Children, terms)); + } + + return await context.CreateDocumentAsync(metadata, "", "text/html"); + } + + /// + /// Map Kontent system properties directly into document metadata. + /// + /// The Kontent item. + /// + /// + private static void MapSystemMetadata(ITaxonomyGroup item, List> metadata) + { + if( item.System != null ) + { + metadata.AddRange(new[] + { + new KeyValuePair(KontentKeys.System.Name, item.System.Name), + new KeyValuePair(KontentKeys.System.CodeName, item.System.Codename), + new KeyValuePair(KontentKeys.System.Id, item.System.Id), + new KeyValuePair(KontentKeys.System.LastModified, item.System.LastModified), + }); + } + } + } +} \ No newline at end of file diff --git a/Kontent.Statiq/TypedContentExtensions.cs b/Kontent.Statiq/TypedContentExtensions.cs index 9d738c7..370c83d 100644 --- a/Kontent.Statiq/TypedContentExtensions.cs +++ b/Kontent.Statiq/TypedContentExtensions.cs @@ -12,6 +12,8 @@ public static class TypedContentExtensions /// The key used in Statiq documents to store the Kontent item. /// public const string KontentItemKey = "KONTENT"; + public const string KontentTaxonomyGroupKey = "KONTENT-TAXONOMY-GROUP"; + public const string KontentTaxonomyTermsKey = "KONTENT-TAXONOMY-TERMS"; /// /// Return the strong typed model for a Statiq document. From 64cd626ee00e8dc6e113d8e40ceade7b4eb8ab99 Mon Sep 17 00:00:00 2001 From: Marnix van Valen Date: Sun, 22 Nov 2020 21:36:54 +0100 Subject: [PATCH 02/10] Additional testing --- .../When_working_with_taxonomy.cs | 172 +++++++++++++++--- Kontent.Statiq.sln | 1 + 2 files changed, 152 insertions(+), 21 deletions(-) diff --git a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs index 8c9f625..791bc9c 100644 --- a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs +++ b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs @@ -5,6 +5,7 @@ using Kontent.Statiq.Tests.Tools; using Statiq.Common; using Statiq.Core; +using Statiq.Testing; using System; using System.Collections.Generic; using System.Linq; @@ -19,56 +20,185 @@ public class When_working_with_taxonomy public async Task It_should_load_taxonomy() { // Arrange - var group = setupTaxonomyGroup("Test", "Test1", "Test2"); + var group = SetupTaxonomyGroup("Test", "Test1", "Test2"); var deliveryClient = A.Fake().WithFakeTaxonomy( group ); - var sut = new KontentTaxonomy(deliveryClient); + // Act + var pipeline = new Pipeline + { + InputModules = + { + new KontentTaxonomy(deliveryClient) + } + }; + + var results = await Execute(pipeline); + + // Assert + results.Should().HaveCount(1); + results.FirstOrDefault().GetChildren().Should().HaveCount(2); + } + + [Fact] + public async Task It_should_flatten_taxonomy() + { + // Arrange + var group = SetupTaxonomyGroup("Test", "Test1", "Test2"); + var deliveryClient = A.Fake().WithFakeTaxonomy(group); // Act - var engine = SetupExecution(sut, - new IModule[]{ - + var pipeline = new Pipeline + { + InputModules = + { + new KontentTaxonomy(deliveryClient), + new FlattenTree(), + new SetDestination(Config.FromDocument((doc,ctx)=> + new NormalizedPath( doc.Get(KontentKeys.System.Name)))), + } + }; + + var results = await Execute(pipeline); + + // Assert + results.Should().HaveCount(3); + results.Get(new NormalizedPath("Test1")).Get(Keys.TreePath).Should().Be("/Test/Test1"); + results.Get(new NormalizedPath("Test2")).Get(Keys.TreePath).Should().Be("/Test/Test2"); + } + + [Fact] + public async Task It_should_work_with_lookup() + { + // Arrange + var group = SetupTaxonomyGroup("Sitemap", "Recipe", "Product"); + var articles = new[] + { + new Article + { + System = new TestContentItemSystemAttributes(){ Name = "make-cookies" }, + Title = "How to make cookies", + Sitemap = new ITaxonomyTerm[] + { + new TestTaxonomyTerm {Codename = "Recipe", Name = "Recipe"}, + new TestTaxonomyTerm {Codename = "Featured", Name = "Featured"} + } + }, + new Article + { + System = new TestContentItemSystemAttributes(){ Name = "make-a-cake" }, + Title = "How to make a cake", + Sitemap = new ITaxonomyTerm[] + { + new TestTaxonomyTerm {Codename = "Recipe", Name = "Recipe"} + } }, - // Assert - async docs => docs.FirstOrDefault().GetChildren().Should().HaveCount(2) - ); - await engine.ExecuteAsync(); + new Article + { + System = new TestContentItemSystemAttributes(){ Name = "buy-cookies"}, + Title = "Chocolate chip cookies", + Sitemap = new ITaxonomyTerm[] + { + new TestTaxonomyTerm {Codename = "Product", Name = "Product"}, + new TestTaxonomyTerm {Codename = "Featured", Name = "Featured"} + } + } + }; + + var deliveryClient = A.Fake() + .WithFakeTaxonomy(group) + .WithFakeContent(articles); + + // Act + var pipeline = new Pipeline + { + InputModules = + { + new Kontent
(deliveryClient), + new SetMetadata("Tags", KontentConfig.Get((Article art) => art.Sitemap.Select( t => t.Codename ).ToArray())), + } + }; + + var results = await Execute(pipeline); + + // Assert + var documentsByTag = results.ToLookupMany("Tags"); + documentsByTag["Recipe"].Should().HaveCount(2); + documentsByTag["Product"].Should().HaveCount(1); + documentsByTag["Featured"].Should().HaveCount(2); // this fails } - private ITaxonomyGroup setupTaxonomyGroup(string name, params string[] terms) + [Fact] + public void Test() { - return new TestTaxonomyGroup + // Arrange + var docs = new[] { - System = A.Fake(), - Terms = terms.Select( t => new TestTaxonomyTerm{ Codename = t, Name = t }).Cast().ToList() + new TestDocument() + { + {"Tags", new[] {"Tag1", "Tag2"}} + }, + new TestDocument() + { + {"Tags", new[] {"Tag1"}} + } }; + + // Act + var docsByTag = docs.ToLookupMany("Tags"); + + // Assert + docsByTag["Tag1"].Should().HaveCount(2); + docsByTag["Tag2"].Should().HaveCount(1); + } - private static Engine SetupExecution(KontentTaxonomy kontentModule, IModule[] processModules, Func, Task> test) + private ITaxonomyGroup SetupTaxonomyGroup(string name, params string[] terms) { - var engine = new Engine(); - var pipeline = new Pipeline() + return new TestTaxonomyGroup { - InputModules = { kontentModule }, - OutputModules = { new TestModule(test) } + System = new TestTaxonomyGroupSystemAttributes + { + Name = name, + Codename = name, + Id = Guid.NewGuid().ToString("N"), + LastModified = DateTime.Now + }, + Terms = terms.Select( t => new TestTaxonomyTermDetails{ Codename = t, Name = t }).Cast().ToList() }; - pipeline.ProcessModules.AddRange(processModules); + } + private static Task Execute(Pipeline pipeline) + { + var engine = new Engine(); engine.Pipelines.Add("test", pipeline); - return engine; + return engine.ExecuteAsync(); } } + internal class TestTaxonomyGroupSystemAttributes : ITaxonomyGroupSystemAttributes + { + public string Codename { get; set; } + public string Id { get; set; } + public DateTime LastModified { get; set; } + public string Name { get; set; } + } + internal class TestTaxonomyGroup : ITaxonomyGroup { public ITaxonomyGroupSystemAttributes System { get; set; } public IList Terms { get; set; } } - internal class TestTaxonomyTerm : ITaxonomyTermDetails + internal class TestTaxonomyTermDetails : ITaxonomyTermDetails { public string Codename { get; set; } public string Name { get; set; } public IList Terms { get; set; } } + + internal class TestTaxonomyTerm : ITaxonomyTerm + { + public string Codename { get; set; } + public string Name { get; set; } + } } diff --git a/Kontent.Statiq.sln b/Kontent.Statiq.sln index 8a8c3fc..63959e7 100644 --- a/Kontent.Statiq.sln +++ b/Kontent.Statiq.sln @@ -70,6 +70,7 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {B34C1AB3-6D7F-4AFA-8706-FBFDE1F942DC} = {E6921A26-7A5F-48E1-A4BD-9D251B0ABE65} {2F7DACF5-7D47-4683-B7C9-A08726F3A8A8} = {EAFC4D0E-920E-4D34-98BE-D18C25E8A4F1} {1167DDD2-C6FC-4E2D-BF02-62EB42A212B0} = {EAFC4D0E-920E-4D34-98BE-D18C25E8A4F1} EndGlobalSection From e9465db03b1c38fa920e1528df713d3b622854f6 Mon Sep 17 00:00:00 2001 From: Marnix van Valen Date: Tue, 15 Dec 2020 20:49:58 +0100 Subject: [PATCH 03/10] Added helpers created for working with taxonomy from themes --- Kontent.Statiq/IDocumentToLookupExtensions.cs | 31 +++++++++++++++++++ Kontent.Statiq/TaxonomyTermComparer.cs | 28 +++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 Kontent.Statiq/IDocumentToLookupExtensions.cs create mode 100644 Kontent.Statiq/TaxonomyTermComparer.cs diff --git a/Kontent.Statiq/IDocumentToLookupExtensions.cs b/Kontent.Statiq/IDocumentToLookupExtensions.cs new file mode 100644 index 0000000..d902021 --- /dev/null +++ b/Kontent.Statiq/IDocumentToLookupExtensions.cs @@ -0,0 +1,31 @@ +using Kentico.Kontent.Delivery.Abstractions; +using Statiq.Common; +using System.Collections.Generic; +using System.Linq; + +namespace Kontent.Statiq +{ + /// + /// Extensions for creating lookups from document sequences. + /// + public static class IDocumentToLookupExtensions + { + /// + /// Creates a lookup from a sequence of documents according to a specified metadata key + /// that contains a sequence of Kontent taxonomy terms. + /// + /// The type of the key. + /// The documents. + /// The key metadata key. + /// A lookup. + public static ILookup ToLookupManyByTaxonomy( + this IEnumerable documents, + string keyMetadataKey) + { + documents.ThrowIfNull(nameof(documents)); + keyMetadataKey.ThrowIfNull(nameof(keyMetadataKey)); + + return documents.ToLookupMany(keyMetadataKey, new TaxonomyTermComparer()); + } + } +} \ No newline at end of file diff --git a/Kontent.Statiq/TaxonomyTermComparer.cs b/Kontent.Statiq/TaxonomyTermComparer.cs new file mode 100644 index 0000000..40f531a --- /dev/null +++ b/Kontent.Statiq/TaxonomyTermComparer.cs @@ -0,0 +1,28 @@ +using Kentico.Kontent.Delivery.Abstractions; +using System.Collections.Generic; + +namespace Kontent.Statiq +{ + /// + /// Comparer for Kontent taxonomy. + /// Use this with + /// + public class TaxonomyTermComparer : IEqualityComparer + { + /// + public bool Equals(ITaxonomyTerm? x, ITaxonomyTerm? y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + if (x.GetType() != y.GetType()) return false; + return x.Codename == y.Codename; + } + + /// + public int GetHashCode(ITaxonomyTerm obj) + { + return (obj.Codename != null ? obj.Codename.GetHashCode() : 0); + } + } +} From 8b5ac390dd14861984af473fb75f6b08871d819c Mon Sep 17 00:00:00 2001 From: Marnix van Valen Date: Tue, 15 Dec 2020 20:50:23 +0100 Subject: [PATCH 04/10] Update to latest nuget dependencies --- Kontent.Statiq.Tests/Kontent.Statiq.Tests.csproj | 6 +++--- Kontent.Statiq/Kontent.Statiq.csproj | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Kontent.Statiq.Tests/Kontent.Statiq.Tests.csproj b/Kontent.Statiq.Tests/Kontent.Statiq.Tests.csproj index 7809c01..d8fd39c 100644 --- a/Kontent.Statiq.Tests/Kontent.Statiq.Tests.csproj +++ b/Kontent.Statiq.Tests/Kontent.Statiq.Tests.csproj @@ -12,9 +12,9 @@ - - - + + + all diff --git a/Kontent.Statiq/Kontent.Statiq.csproj b/Kontent.Statiq/Kontent.Statiq.csproj index f8325e4..402c759 100644 --- a/Kontent.Statiq/Kontent.Statiq.csproj +++ b/Kontent.Statiq/Kontent.Statiq.csproj @@ -28,14 +28,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 7b36e73081d65c72a5813b7c0f097b2089c22517 Mon Sep 17 00:00:00 2001 From: Marnix van Valen Date: Wed, 16 Dec 2020 09:15:17 +0100 Subject: [PATCH 05/10] Extending functionality for grouping content by taxonomy --- .../Tools/TestContentItemSystemAttributes.cs | 1 + Kontent.Statiq.Tests/Tools/XUnitLogger.cs | 69 +++++++ .../When_working_with_taxonomy.cs | 178 ++++++++++++++---- Kontent.Statiq/IDocumentToLookupExtensions.cs | 19 +- Kontent.Statiq/KontentDocumentHelpers.cs | 2 +- Kontent.Statiq/KontentTaxonomyHelpers.cs | 39 +++- .../KontentTaxonomyModuleConfiguration.cs | 22 +++ Kontent.Statiq/TaxonomyTermComparer.cs | 1 - Kontent.Statiq/TypedContentExtensions.cs | 69 ++++++- 9 files changed, 351 insertions(+), 49 deletions(-) create mode 100644 Kontent.Statiq.Tests/Tools/XUnitLogger.cs create mode 100644 Kontent.Statiq/KontentTaxonomyModuleConfiguration.cs diff --git a/Kontent.Statiq.Tests/Tools/TestContentItemSystemAttributes.cs b/Kontent.Statiq.Tests/Tools/TestContentItemSystemAttributes.cs index 57f9c1b..e843ff5 100644 --- a/Kontent.Statiq.Tests/Tools/TestContentItemSystemAttributes.cs +++ b/Kontent.Statiq.Tests/Tools/TestContentItemSystemAttributes.cs @@ -13,6 +13,7 @@ internal sealed class TestContentItemSystemAttributes : IContentItemSystemAttrib public string Name { get; internal set; } public string Codename { get; internal set; } public string Type { get; internal set; } + public string Collection { get; } public IList SitemapLocation { get; internal set; } public DateTime LastModified { get; internal set; } public string Language { get; internal set; } diff --git a/Kontent.Statiq.Tests/Tools/XUnitLogger.cs b/Kontent.Statiq.Tests/Tools/XUnitLogger.cs new file mode 100644 index 0000000..e5bc990 --- /dev/null +++ b/Kontent.Statiq.Tests/Tools/XUnitLogger.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.Logging; +using System; +using Xunit.Abstractions; + +namespace Kontent.Statiq.Tests.Tools +{ + public class XUnitLoggerFactory : ILoggerFactory + { + private readonly ITestOutputHelper _output; + + public XUnitLoggerFactory(ITestOutputHelper output) + { + _output = output; + } + public void Dispose() + { + throw new NotImplementedException(); + } + + public ILogger CreateLogger(string categoryName) + { + return new XunitLogger(_output, categoryName); + } + + public void AddProvider(ILoggerProvider provider) + { + + } + } + public class XunitLogger : ILogger + { + private readonly ITestOutputHelper _testOutputHelper; + private readonly string _categoryName; + + public XunitLogger(ITestOutputHelper testOutputHelper, string categoryName) + { + _testOutputHelper = testOutputHelper; + _categoryName = categoryName; + } + + public IDisposable BeginScope(TState state) + { + return NoopDisposable.Instance; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _testOutputHelper.WriteLine($"{_categoryName} [{eventId}] {formatter(state, exception)}"); + + if (exception != null) + _testOutputHelper.WriteLine(exception.ToString()); + } + + private class NoopDisposable : IDisposable + { + public static NoopDisposable Instance = new NoopDisposable(); + + public void Dispose() + { + + } + } + } +} diff --git a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs index 791bc9c..2d997b8 100644 --- a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs +++ b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs @@ -1,8 +1,11 @@ using FakeItEasy; using FluentAssertions; using Kentico.Kontent.Delivery.Abstractions; +using Kentico.Kontent.Delivery.Urls.QueryParameters.Filters; using Kontent.Statiq.Tests.Models; using Kontent.Statiq.Tests.Tools; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Statiq.Common; using Statiq.Core; using Statiq.Testing; @@ -11,11 +14,19 @@ using System.Linq; using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; namespace Kontent.Statiq.Tests { public class When_working_with_taxonomy { + private readonly ITestOutputHelper _output; + + public When_working_with_taxonomy(ITestOutputHelper output) + { + _output = output; + } + [Fact] public async Task It_should_load_taxonomy() { @@ -36,7 +47,52 @@ public async Task It_should_load_taxonomy() // Assert results.Should().HaveCount(1); - results.FirstOrDefault().GetChildren().Should().HaveCount(2); + results.First().GetChildren().Should().HaveCount(2); + } + + [Fact] + public async Task It_should_load_taxonomy_and_retrieve_it() + { + // Arrange + var group = SetupTaxonomyGroup("Test", "Test1", "Test2"); + var deliveryClient = A.Fake().WithFakeTaxonomy(group); + + // Act + var pipeline = new Pipeline + { + InputModules = + { + new KontentTaxonomy(deliveryClient) + } + }; + + var results = await Execute(pipeline); + + // Assert + results.First().AsKontentTaxonomy().Should().BeEquivalentTo(group); + } + + [Fact] + public async Task It_should_load_taxonomy_and_retrieve_its_terms() + { + // Arrange + var group = SetupTaxonomyGroup("Test", "Test1", "Test2"); + var deliveryClient = A.Fake().WithFakeTaxonomy(group); + + // Act + var pipeline = new Pipeline + { + InputModules = + { + new KontentTaxonomy(deliveryClient) + } + }; + + var results = await Execute(pipeline); + + // Assert + results.First().AsKontentTaxonomyTerms().Should().HaveCount(2); + } [Fact] @@ -62,50 +118,41 @@ public async Task It_should_flatten_taxonomy() // Assert results.Should().HaveCount(3); - results.Get(new NormalizedPath("Test1")).Get(Keys.TreePath).Should().Be("/Test/Test1"); - results.Get(new NormalizedPath("Test2")).Get(Keys.TreePath).Should().Be("/Test/Test2"); + results.Get(new NormalizedPath("Test1")).Get(Keys.TreePath).Should().BeEquivalentTo("test", "test1"); + results.Get(new NormalizedPath("Test2")).Get(Keys.TreePath).Should().BeEquivalentTo("test", "test2"); } [Fact] public async Task It_should_work_with_lookup() { // Arrange - var group = SetupTaxonomyGroup("Sitemap", "Recipe", "Product"); + var recipe = new TestTaxonomyTerm {Codename = "recipe-tag", Name = "Recipe"}; + var featured = new TestTaxonomyTerm { Codename = "featured-tag", Name = "Featured" }; + var product = new TestTaxonomyTerm {Codename = "product-tag", Name = "Product"}; + var articles = new[] { new Article { System = new TestContentItemSystemAttributes(){ Name = "make-cookies" }, Title = "How to make cookies", - Sitemap = new ITaxonomyTerm[] - { - new TestTaxonomyTerm {Codename = "Recipe", Name = "Recipe"}, - new TestTaxonomyTerm {Codename = "Featured", Name = "Featured"} - } + Sitemap = new ITaxonomyTerm[] {recipe, featured} }, new Article { System = new TestContentItemSystemAttributes(){ Name = "make-a-cake" }, Title = "How to make a cake", - Sitemap = new ITaxonomyTerm[] - { - new TestTaxonomyTerm {Codename = "Recipe", Name = "Recipe"} - } + Sitemap = new ITaxonomyTerm[] {recipe} }, new Article { System = new TestContentItemSystemAttributes(){ Name = "buy-cookies"}, Title = "Chocolate chip cookies", - Sitemap = new ITaxonomyTerm[] - { - new TestTaxonomyTerm {Codename = "Product", Name = "Product"}, - new TestTaxonomyTerm {Codename = "Featured", Name = "Featured"} - } + Sitemap = new ITaxonomyTerm[] { product, featured } } }; var deliveryClient = A.Fake() - .WithFakeTaxonomy(group) .WithFakeContent(articles); // Act @@ -114,44 +161,103 @@ public async Task It_should_work_with_lookup() InputModules = { new Kontent
(deliveryClient), - new SetMetadata("Tags", KontentConfig.Get((Article art) => art.Sitemap.Select( t => t.Codename ).ToArray())), + // Set taxonomy terms as metadata + new SetMetadata("Tags", KontentConfig.Get((Article art) => art.Sitemap)), } }; var results = await Execute(pipeline); + var documentsByTag = results.ToLookupManyByTaxonomy("Tags"); // Assert - var documentsByTag = results.ToLookupMany("Tags"); - documentsByTag["Recipe"].Should().HaveCount(2); - documentsByTag["Product"].Should().HaveCount(1); - documentsByTag["Featured"].Should().HaveCount(2); // this fails + documentsByTag[recipe].Should().HaveCount(2); + documentsByTag[new TestTaxonomyTerm{ Codename = product.Codename }].Should().HaveCount(1); + documentsByTag[featured].Should().Contain( x => x.AsKontent
().Title == articles[0].Title ); } [Fact] - public void Test() + public async Task It_should_enable_grouping_documents_into_trees() { // Arrange - var docs = new[] + var group = SetupTaxonomyGroup("Menu", "Product", "Recipe", "Featured"); + + var recipe = new TestTaxonomyTerm { Codename = "recipe", Name = "Recipe" }; + var featured = new TestTaxonomyTerm { Codename = "featured", Name = "Featured" }; + var product = new TestTaxonomyTerm { Codename = "product", Name = "Product" }; + + var articles = new[] { - new TestDocument() + new Article { - {"Tags", new[] {"Tag1", "Tag2"}} + System = new TestContentItemSystemAttributes(){ Name = "make-cookies" }, + Title = "How to make cookies", + Sitemap = new ITaxonomyTerm[] {recipe, featured} }, - new TestDocument() + new Article { - {"Tags", new[] {"Tag1"}} + System = new TestContentItemSystemAttributes(){ Name = "make-a-cake" }, + Title = "How to make a cake", + Sitemap = new ITaxonomyTerm[] {recipe} + }, + new Article + { + System = new TestContentItemSystemAttributes(){ Name = "buy-cookies"}, + Title = "Chocolate chip cookies", + Sitemap = new ITaxonomyTerm[] { product, featured } } }; + var deliveryClient = A.Fake() + .WithFakeTaxonomy(group) + .WithFakeContent(articles); + // Act - var docsByTag = docs.ToLookupMany("Tags"); + var articlePipeline = new Pipeline + { + InputModules = + { + new Kontent
(deliveryClient) + } + }; - // Assert - docsByTag["Tag1"].Should().HaveCount(2); - docsByTag["Tag2"].Should().HaveCount(1); + var menuPipeline = new Pipeline + { + Dependencies = { "Articles" }, + InputModules = + { + new KontentTaxonomy(deliveryClient) + .WithQuery(new EqualsFilter("system.codename", "menu")), + new FlattenTree(), + }, + ProcessModules = { + new SetMetadata( Keys.Children, Config.FromDocument((doc, ctx) => + { + return ctx.Outputs.FromPipeline("Articles") + .Where( art => art.AsKontent
().Sitemap + .Contains(doc.AsKontentTaxonomyTerm()!, new TaxonomyTermComparer()) ) + .ToArray(); + }) ) + } + }; + + var engine = new Engine( + new ApplicationState(Array.Empty(), "", ""), + new TestServiceProvider(cfg => cfg.AddSingleton(new XUnitLoggerFactory(_output)))); + + engine.Pipelines.Add("Articles", articlePipeline); + engine.Pipelines.Add("Menu", menuPipeline); + var results = await engine.ExecuteAsync(); + // Assert + var sitemap = results.FromPipeline("Menu"); + + var productSection = sitemap.FirstOrDefault( p => p.Get(Keys.Title) == "Product" ); + productSection.GetChildren().Should().HaveCount(1); + var featuredSection = sitemap.FirstOrDefault(p => p.Get(Keys.Title) == "Featured"); + featuredSection.GetChildren().Should().HaveCount(2); } + private ITaxonomyGroup SetupTaxonomyGroup(string name, params string[] terms) { return new TestTaxonomyGroup @@ -159,11 +265,11 @@ private ITaxonomyGroup SetupTaxonomyGroup(string name, params string[] terms) System = new TestTaxonomyGroupSystemAttributes { Name = name, - Codename = name, + Codename = name.ToLower(), Id = Guid.NewGuid().ToString("N"), LastModified = DateTime.Now }, - Terms = terms.Select( t => new TestTaxonomyTermDetails{ Codename = t, Name = t }).Cast().ToList() + Terms = terms.Select( t => new TestTaxonomyTermDetails{ Codename = t.ToLower(), Name = t }).Cast().ToList() }; } diff --git a/Kontent.Statiq/IDocumentToLookupExtensions.cs b/Kontent.Statiq/IDocumentToLookupExtensions.cs index d902021..f250e8a 100644 --- a/Kontent.Statiq/IDocumentToLookupExtensions.cs +++ b/Kontent.Statiq/IDocumentToLookupExtensions.cs @@ -1,5 +1,6 @@ using Kentico.Kontent.Delivery.Abstractions; using Statiq.Common; +using System; using System.Collections.Generic; using System.Linq; @@ -14,7 +15,6 @@ public static class IDocumentToLookupExtensions /// Creates a lookup from a sequence of documents according to a specified metadata key /// that contains a sequence of Kontent taxonomy terms. ///
- /// The type of the key. /// The documents. /// The key metadata key. /// A lookup. @@ -27,5 +27,22 @@ public static ILookup ToLookupManyByTaxonomy( return documents.ToLookupMany(keyMetadataKey, new TaxonomyTermComparer()); } + + /// + /// Creates a lookup from a sequence of documents according to a specified metadata key + /// that contains a sequence of Kontent taxonomy terms. + /// + /// The documents. + /// A function to access the taxonomy terms of the content. + /// A lookup. + public static ILookup ToLookupManyByTaxonomy( + this IEnumerable documents, + Func> getTaxonomy) + { + documents.ThrowIfNull(nameof(documents)); + getTaxonomy.ThrowIfNull(nameof(getTaxonomy)); + + return documents.ToLookupMany( doc => getTaxonomy(doc.AsKontent()), new TaxonomyTermComparer()); + } } } \ No newline at end of file diff --git a/Kontent.Statiq/KontentDocumentHelpers.cs b/Kontent.Statiq/KontentDocumentHelpers.cs index 888f0e6..53a7824 100644 --- a/Kontent.Statiq/KontentDocumentHelpers.cs +++ b/Kontent.Statiq/KontentDocumentHelpers.cs @@ -34,7 +34,7 @@ internal static async Task CreateDocument(IExecutionContext context, return await CreateDocumentInternal(context, item, props, content).ConfigureAwait(false); } - internal static Task CreateDocumentInternal(IExecutionContext context, object item, PropertyInfo[] props, string content ) + private static Task CreateDocumentInternal(IExecutionContext context, object item, PropertyInfo[] props, string content ) { var metadata = new List> { diff --git a/Kontent.Statiq/KontentTaxonomyHelpers.cs b/Kontent.Statiq/KontentTaxonomyHelpers.cs index 2a02064..2a8b05e 100644 --- a/Kontent.Statiq/KontentTaxonomyHelpers.cs +++ b/Kontent.Statiq/KontentTaxonomyHelpers.cs @@ -1,15 +1,17 @@ using Kentico.Kontent.Delivery.Abstractions; using Statiq.Common; +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Kontent.Statiq { - internal class KontentTaxonomyHelpers + internal static class KontentTaxonomyHelpers { internal static async Task CreateDocument(IExecutionContext context, ITaxonomyGroup item) { - var treePath = $"/{item.System.Codename}"; + var treePath = new []{item.System.Codename}; var metadata = new List> { @@ -21,18 +23,19 @@ internal static async Task CreateDocument(IExecutionContext context, MapSystemMetadata(item, metadata); - if (item.Terms != null) + if (item.Terms != null && item.Terms.Count > 0) { var terms = await item.Terms.ParallelSelectAsync(async t => await CreateDocument(context, t, treePath)); metadata.Add(new KeyValuePair(Keys.Children, terms)); + metadata.Add(new KeyValuePair(TypedContentExtensions.KontentTaxonomyTermsKey, item.Terms.Select(TaxonomyTerm.CreateFrom).ToArray())); } return await context.CreateDocumentAsync(metadata, "", "text/html"); } - internal static async Task CreateDocument(IExecutionContext context, ITaxonomyTermDetails item, string parentPath) + private static async Task CreateDocument(IExecutionContext context, ITaxonomyTermDetails item, string[] parentPath) { - var treePath = $"{parentPath}/{item.Codename}"; + var treePath = parentPath.Concat(item.Codename).ToArray(); var metadata = new List> { new KeyValuePair(Keys.Title, item.Name), @@ -40,12 +43,14 @@ internal static async Task CreateDocument(IExecutionContext context, new KeyValuePair(Keys.TreePath, treePath), new KeyValuePair(KontentKeys.System.Name, item.Name), new KeyValuePair(KontentKeys.System.CodeName, item.Codename), + new KeyValuePair(TypedContentExtensions.KontentTaxonomyTermKey, TaxonomyTerm.CreateFrom(item)) }; - if (item.Terms != null) + if (item.Terms != null && item.Terms.Count > 0) { var terms = await item.Terms.ParallelSelectAsync(async t => await CreateDocument(context, t, treePath)); metadata.Add(new KeyValuePair(Keys.Children, terms)); + metadata.Add(new KeyValuePair(TypedContentExtensions.KontentTaxonomyTermsKey, item.Terms.Select( TaxonomyTerm.CreateFrom ).ToArray())); } return await context.CreateDocumentAsync(metadata, "", "text/html"); @@ -54,9 +59,8 @@ internal static async Task CreateDocument(IExecutionContext context, /// /// Map Kontent system properties directly into document metadata. /// - /// The Kontent item. - /// - /// + /// The taxonomy group. + /// The metadata collection. private static void MapSystemMetadata(ITaxonomyGroup item, List> metadata) { if( item.System != null ) @@ -71,4 +75,21 @@ private static void MapSystemMetadata(ITaxonomyGroup item, List + /// Extension methods to configure the Kontent.Statiq Taxonomy module. + /// + public static class KontentTaxonomyModuleConfiguration + { + /// + /// Add query parameters. + /// + /// The module. + /// + /// The module. + public static KontentTaxonomy WithQuery(this KontentTaxonomy module, params IQueryParameter[] queryParameters) + { + module.QueryParameters.AddRange(queryParameters); + return module; + } + } +} \ No newline at end of file diff --git a/Kontent.Statiq/TaxonomyTermComparer.cs b/Kontent.Statiq/TaxonomyTermComparer.cs index 40f531a..c0a7819 100644 --- a/Kontent.Statiq/TaxonomyTermComparer.cs +++ b/Kontent.Statiq/TaxonomyTermComparer.cs @@ -15,7 +15,6 @@ public bool Equals(ITaxonomyTerm? x, ITaxonomyTerm? y) if (ReferenceEquals(x, y)) return true; if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; - if (x.GetType() != y.GetType()) return false; return x.Codename == y.Codename; } diff --git a/Kontent.Statiq/TypedContentExtensions.cs b/Kontent.Statiq/TypedContentExtensions.cs index 370c83d..ded69ff 100644 --- a/Kontent.Statiq/TypedContentExtensions.cs +++ b/Kontent.Statiq/TypedContentExtensions.cs @@ -1,4 +1,5 @@ -using Statiq.Common; +using Kentico.Kontent.Delivery.Abstractions; +using Statiq.Common; using System; namespace Kontent.Statiq @@ -14,6 +15,7 @@ public static class TypedContentExtensions public const string KontentItemKey = "KONTENT"; public const string KontentTaxonomyGroupKey = "KONTENT-TAXONOMY-GROUP"; public const string KontentTaxonomyTermsKey = "KONTENT-TAXONOMY-TERMS"; + public const string KontentTaxonomyTermKey = "KONTENT-TAXONOMY-TERM"; /// /// Return the strong typed model for a Statiq document. @@ -36,5 +38,70 @@ public static TModel AsKontent(this IDocument document) throw new InvalidOperationException($"This is not a Kontent document: {document.Source}"); } + + /// + /// Return the strong typed model for a Statiq document. + /// + /// The Document. + /// The strong typed content model. + /// Thrown when this method is called on a document that doesn't a Kontent content item. + public static ITaxonomyGroup AsKontentTaxonomy(this IDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document), "Expected a document but got "); + } + + if (document.TryGetValue(KontentTaxonomyGroupKey, out ITaxonomyGroup contentItem)) + { + return contentItem; + } + + throw new InvalidOperationException($"This document does not contain a Kontent taxonomy group: {document.Source}"); + } + + /// + /// Return the strong typed model for a Statiq document. + /// + /// The Document. + /// The type of content to return. + /// The strong typed content model. + /// Thrown when this method is called on a document that doesn't a Kontent content item. + public static ITaxonomyTerm? AsKontentTaxonomyTerm(this IDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document), "Expected a document but got "); + } + + if (document.TryGetValue(KontentTaxonomyTermKey, out ITaxonomyTerm contentItem)) + { + return contentItem; + } + + return null; + } + + /// + /// Return the strong typed model for a Statiq document. + /// + /// The Document. + /// The type of content to return. + /// The strong typed content model. + /// Thrown when this method is called on a document that doesn't a Kontent content item. + public static ITaxonomyTerm[] AsKontentTaxonomyTerms(this IDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document), "Expected a document but got "); + } + + if (document.TryGetValue(KontentTaxonomyTermsKey, out ITaxonomyTerm[] contentItem)) + { + return contentItem; + } + + return Array.Empty(); + } } } From 42b8723e38a0278090b298142a834a5e9f618db7 Mon Sep 17 00:00:00 2001 From: Marnix van Valen Date: Sat, 19 Dec 2020 17:13:19 +0100 Subject: [PATCH 06/10] Improve test coverage --- .../When_working_with_taxonomy.cs | 44 ++++++++++++------- Kontent.Statiq/KontentTaxonomyHelpers.cs | 1 - 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs index 2d997b8..9b7d51f 100644 --- a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs +++ b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs @@ -32,6 +32,7 @@ public async Task It_should_load_taxonomy() { // Arrange var group = SetupTaxonomyGroup("Test", "Test1", "Test2"); + group.Terms.First().Terms.AddRange( CreateTaxonomyTerms("Test1a", "Test1b", "Test1c") ); var deliveryClient = A.Fake().WithFakeTaxonomy( group ); // Act @@ -48,6 +49,7 @@ public async Task It_should_load_taxonomy() // Assert results.Should().HaveCount(1); results.First().GetChildren().Should().HaveCount(2); + results.First().GetChildren().First().GetChildren().Should().HaveCount(3); } [Fact] @@ -260,17 +262,21 @@ public async Task It_should_enable_grouping_documents_into_trees() private ITaxonomyGroup SetupTaxonomyGroup(string name, params string[] terms) { - return new TestTaxonomyGroup + var group = new TestTaxonomyGroup(new TestTaxonomyGroupSystemAttributes { - System = new TestTaxonomyGroupSystemAttributes - { - Name = name, - Codename = name.ToLower(), - Id = Guid.NewGuid().ToString("N"), - LastModified = DateTime.Now - }, - Terms = terms.Select( t => new TestTaxonomyTermDetails{ Codename = t.ToLower(), Name = t }).Cast().ToList() - }; + Name = name, + Codename = name.ToLower(), + Id = Guid.NewGuid().ToString("N"), + LastModified = DateTime.Now + }); + group.Terms.AddRange(CreateTaxonomyTerms(terms)); + return group; + } + + private IList CreateTaxonomyTerms(params string[] terms) + { + return terms.Select(t => new TestTaxonomyTermDetails {Codename = t.ToLower(), Name = t}) + .Cast().ToList(); } private static Task Execute(Pipeline pipeline) @@ -291,20 +297,24 @@ internal class TestTaxonomyGroupSystemAttributes : ITaxonomyGroupSystemAttribute internal class TestTaxonomyGroup : ITaxonomyGroup { - public ITaxonomyGroupSystemAttributes System { get; set; } - public IList Terms { get; set; } + public TestTaxonomyGroup(ITaxonomyGroupSystemAttributes system) + { + System = system; + } + public ITaxonomyGroupSystemAttributes System { get; } + public IList Terms { get; } = new List(); } internal class TestTaxonomyTermDetails : ITaxonomyTermDetails { - public string Codename { get; set; } - public string Name { get; set; } - public IList Terms { get; set; } + public string Codename { get; set; } = ""; + public string Name { get; set; } = ""; + public IList Terms { get; } = new List(); } internal class TestTaxonomyTerm : ITaxonomyTerm { - public string Codename { get; set; } - public string Name { get; set; } + public string Codename { get; set; } = ""; + public string Name { get; set; } = ""; } } diff --git a/Kontent.Statiq/KontentTaxonomyHelpers.cs b/Kontent.Statiq/KontentTaxonomyHelpers.cs index 2a8b05e..096a146 100644 --- a/Kontent.Statiq/KontentTaxonomyHelpers.cs +++ b/Kontent.Statiq/KontentTaxonomyHelpers.cs @@ -1,6 +1,5 @@ using Kentico.Kontent.Delivery.Abstractions; using Statiq.Common; -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; From 271a867627f5d33805510a9f4389e918cc7f3a91 Mon Sep 17 00:00:00 2001 From: Marnix van Valen Date: Sat, 19 Dec 2020 17:51:28 +0100 Subject: [PATCH 07/10] Code quality & cleanup --- .../When_working_with_taxonomy.cs | 6 +-- Kontent.Statiq/Kontent.cs | 7 ++-- Kontent.Statiq/KontentDocumentHelpers.cs | 2 +- Kontent.Statiq/KontentKeys.cs | 28 ++++++++++++- Kontent.Statiq/KontentModuleConfiguration.cs | 3 ++ Kontent.Statiq/KontentTaxonomy.cs | 4 ++ Kontent.Statiq/KontentTaxonomyHelpers.cs | 25 ++---------- Kontent.Statiq/TaxonomyTerm.cs | 23 +++++++++++ Kontent.Statiq/TaxonomyTermComparer.cs | 2 +- Kontent.Statiq/TypedContentExtensions.cs | 39 +++++++++++-------- 10 files changed, 90 insertions(+), 49 deletions(-) create mode 100644 Kontent.Statiq/TaxonomyTerm.cs diff --git a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs index 9b7d51f..bfcbf49 100644 --- a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs +++ b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs @@ -289,10 +289,10 @@ private static Task Execute(Pipeline pipeline) internal class TestTaxonomyGroupSystemAttributes : ITaxonomyGroupSystemAttributes { - public string Codename { get; set; } - public string Id { get; set; } + public string Codename { get; set; } = ""; + public string Id { get; set; } = ""; public DateTime LastModified { get; set; } - public string Name { get; set; } + public string Name { get; set; } = ""; } internal class TestTaxonomyGroup : ITaxonomyGroup diff --git a/Kontent.Statiq/Kontent.cs b/Kontent.Statiq/Kontent.cs index 54314bd..f1b1911 100644 --- a/Kontent.Statiq/Kontent.cs +++ b/Kontent.Statiq/Kontent.cs @@ -10,13 +10,12 @@ namespace Kontent.Statiq { /// /// Retrieves content items from Kontent. + /// Use .WithContent() to specify what property to load content from and .WithQuery() to specify query parameters. /// + /// The content model. public sealed class Kontent : Module where TContentModel : class { - /// - /// The code name of the field uses to fill the main Content field on the Statiq document. This is mostly useful for untyped content. - /// - public Func? GetContent { get; set; } + internal Func? GetContent { get; set; } private readonly IDeliveryClient _client; diff --git a/Kontent.Statiq/KontentDocumentHelpers.cs b/Kontent.Statiq/KontentDocumentHelpers.cs index 53a7824..cddd99c 100644 --- a/Kontent.Statiq/KontentDocumentHelpers.cs +++ b/Kontent.Statiq/KontentDocumentHelpers.cs @@ -38,7 +38,7 @@ private static Task CreateDocumentInternal(IExecutionContext context, { var metadata = new List> { - new KeyValuePair(TypedContentExtensions.KontentItemKey, item), + new KeyValuePair(KontentKeys.Item, item), }; MapSystemMetadata(item, props, metadata); diff --git a/Kontent.Statiq/KontentKeys.cs b/Kontent.Statiq/KontentKeys.cs index 4648152..a2997b8 100644 --- a/Kontent.Statiq/KontentKeys.cs +++ b/Kontent.Statiq/KontentKeys.cs @@ -5,6 +5,11 @@ /// public static class KontentKeys { + /// + /// The key used to store the Kontent item in Statiq document metadata. + /// + public const string Item = "KONTENT"; + /// /// Keys for well-known Statiq document metadata for Kontent system metadata. /// @@ -36,13 +41,32 @@ public static class System public const string LastModified = "system.lastmodified"; } + /// + /// Keys for well-known Statiq document metadata for image processing. + /// public static class Images { - /// + /// /// The key used by the and modules to handle - /// images that need to be downloaded. + /// images that need to be downloaded. /// public const string Downloads = "KONTENT-ASSET-DOWNLOADS"; } + + public static class Taxonomy + { + /// + /// The key used to store the Kontent taxonomy group in Statiq document metadata. + /// + public const string Group = "KONTENT-TAXONOMY-GROUP"; + /// + /// The key used to store the Kontent taxonomy terms in Statiq document metadata. + /// + public const string Terms = "KONTENT-TAXONOMY-TERMS"; + /// + /// The key used to store the Kontent taxonomy term in Statiq document metadata. + /// + public const string Term = "KONTENT-TAXONOMY-TERM"; + } } } \ No newline at end of file diff --git a/Kontent.Statiq/KontentModuleConfiguration.cs b/Kontent.Statiq/KontentModuleConfiguration.cs index 982f028..04ccb50 100644 --- a/Kontent.Statiq/KontentModuleConfiguration.cs +++ b/Kontent.Statiq/KontentModuleConfiguration.cs @@ -30,6 +30,7 @@ public static Kontent WithContent(this KontentSort order /// The content model type. /// The module. + [Obsolete("Please use WithQuery to add query parameters instead.")] public static Kontent OrderBy(this Kontent module, string field, SortOrder sortOrder) where TContentModel : class { if (!field.StartsWith("elements") && !field.StartsWith("system")) @@ -48,6 +49,7 @@ public static Kontent OrderBy(this KontentThe number of items to skip. /// The content model type. /// The module. + [Obsolete("Please use WithQuery to add query parameters instead.")] public static Kontent Skip(this Kontent module, int skip) where TContentModel : class { module.QueryParameters.Add(new SkipParameter(skip)); @@ -61,6 +63,7 @@ public static Kontent Skip(this KontentThe maximum number of items to return. /// The content model type. /// The module. + [Obsolete("Please use WithQuery to add query parameters instead.")] public static Kontent Limit(this Kontent module, int limit) where TContentModel : class { module.QueryParameters.Add(new LimitParameter(limit)); diff --git a/Kontent.Statiq/KontentTaxonomy.cs b/Kontent.Statiq/KontentTaxonomy.cs index 0c4eb94..dd58310 100644 --- a/Kontent.Statiq/KontentTaxonomy.cs +++ b/Kontent.Statiq/KontentTaxonomy.cs @@ -7,6 +7,10 @@ namespace Kontent.Statiq { + /// + /// Module that retrieves Kontent taxonomy groups as a Statiq document structure. + /// The output is a page structure for each group that matches the query. Child documents are child terms. + /// public sealed class KontentTaxonomy : Module { private readonly IDeliveryClient _client; diff --git a/Kontent.Statiq/KontentTaxonomyHelpers.cs b/Kontent.Statiq/KontentTaxonomyHelpers.cs index 096a146..8815e68 100644 --- a/Kontent.Statiq/KontentTaxonomyHelpers.cs +++ b/Kontent.Statiq/KontentTaxonomyHelpers.cs @@ -17,7 +17,7 @@ internal static async Task CreateDocument(IExecutionContext context, new KeyValuePair(Keys.Title, item.System.Name), new KeyValuePair(Keys.GroupKey, item.System.Codename), new KeyValuePair(Keys.TreePath, treePath), - new KeyValuePair(TypedContentExtensions.KontentTaxonomyGroupKey, item) + new KeyValuePair(KontentKeys.Taxonomy.Group, item) }; MapSystemMetadata(item, metadata); @@ -26,7 +26,7 @@ internal static async Task CreateDocument(IExecutionContext context, { var terms = await item.Terms.ParallelSelectAsync(async t => await CreateDocument(context, t, treePath)); metadata.Add(new KeyValuePair(Keys.Children, terms)); - metadata.Add(new KeyValuePair(TypedContentExtensions.KontentTaxonomyTermsKey, item.Terms.Select(TaxonomyTerm.CreateFrom).ToArray())); + metadata.Add(new KeyValuePair(KontentKeys.Taxonomy.Terms, item.Terms.ToArray())); } return await context.CreateDocumentAsync(metadata, "", "text/html"); @@ -42,14 +42,14 @@ private static async Task CreateDocument(IExecutionContext context, I new KeyValuePair(Keys.TreePath, treePath), new KeyValuePair(KontentKeys.System.Name, item.Name), new KeyValuePair(KontentKeys.System.CodeName, item.Codename), - new KeyValuePair(TypedContentExtensions.KontentTaxonomyTermKey, TaxonomyTerm.CreateFrom(item)) + new KeyValuePair(KontentKeys.Taxonomy.Term, item) }; if (item.Terms != null && item.Terms.Count > 0) { var terms = await item.Terms.ParallelSelectAsync(async t => await CreateDocument(context, t, treePath)); metadata.Add(new KeyValuePair(Keys.Children, terms)); - metadata.Add(new KeyValuePair(TypedContentExtensions.KontentTaxonomyTermsKey, item.Terms.Select( TaxonomyTerm.CreateFrom ).ToArray())); + metadata.Add(new KeyValuePair(KontentKeys.Taxonomy.Terms, item.Terms.ToArray())); } return await context.CreateDocumentAsync(metadata, "", "text/html"); @@ -74,21 +74,4 @@ private static void MapSystemMetadata(ITaxonomyGroup item, List /// Comparer for Kontent taxonomy. - /// Use this with + /// Use this with to group content based on taxonomy terms. /// public class TaxonomyTermComparer : IEqualityComparer { diff --git a/Kontent.Statiq/TypedContentExtensions.cs b/Kontent.Statiq/TypedContentExtensions.cs index ded69ff..ebba816 100644 --- a/Kontent.Statiq/TypedContentExtensions.cs +++ b/Kontent.Statiq/TypedContentExtensions.cs @@ -1,6 +1,7 @@ using Kentico.Kontent.Delivery.Abstractions; using Statiq.Common; using System; +using System.Linq; namespace Kontent.Statiq { @@ -12,11 +13,9 @@ public static class TypedContentExtensions /// /// The key used in Statiq documents to store the Kontent item. /// - public const string KontentItemKey = "KONTENT"; - public const string KontentTaxonomyGroupKey = "KONTENT-TAXONOMY-GROUP"; - public const string KontentTaxonomyTermsKey = "KONTENT-TAXONOMY-TERMS"; - public const string KontentTaxonomyTermKey = "KONTENT-TAXONOMY-TERM"; - + [Obsolete("Please use KontentKeys.Item instead")] + public const string KontentItemKey = KontentKeys.Item; + /// /// Return the strong typed model for a Statiq document. /// @@ -31,7 +30,7 @@ public static TModel AsKontent(this IDocument document) throw new ArgumentNullException(nameof(document), $"Expected a document of type {typeof(TModel).FullName}"); } - if (document.TryGetValue(KontentItemKey, out TModel contentItem)) + if (document.TryGetValue(KontentKeys.Item, out TModel contentItem)) { return contentItem; } @@ -52,7 +51,7 @@ public static ITaxonomyGroup AsKontentTaxonomy(this IDocument document) throw new ArgumentNullException(nameof(document), "Expected a document but got "); } - if (document.TryGetValue(KontentTaxonomyGroupKey, out ITaxonomyGroup contentItem)) + if (document.TryGetValue(KontentKeys.Taxonomy.Group, out ITaxonomyGroup contentItem)) { return contentItem; } @@ -64,9 +63,7 @@ public static ITaxonomyGroup AsKontentTaxonomy(this IDocument document) /// Return the strong typed model for a Statiq document. /// /// The Document. - /// The type of content to return. - /// The strong typed content model. - /// Thrown when this method is called on a document that doesn't a Kontent content item. + /// The taxonomy term or null. public static ITaxonomyTerm? AsKontentTaxonomyTerm(this IDocument document) { if (document == null) @@ -74,9 +71,14 @@ public static ITaxonomyGroup AsKontentTaxonomy(this IDocument document) throw new ArgumentNullException(nameof(document), "Expected a document but got "); } - if (document.TryGetValue(KontentTaxonomyTermKey, out ITaxonomyTerm contentItem)) + if (document.TryGetValue(KontentKeys.Taxonomy.Term, out ITaxonomyTerm term)) { - return contentItem; + return term; + } + + if (document.TryGetValue(KontentKeys.Taxonomy.Term, out ITaxonomyTermDetails termDetails)) + { + return TaxonomyTerm.CreateFrom(termDetails); } return null; @@ -86,9 +88,7 @@ public static ITaxonomyGroup AsKontentTaxonomy(this IDocument document) /// Return the strong typed model for a Statiq document. /// /// The Document. - /// The type of content to return. - /// The strong typed content model. - /// Thrown when this method is called on a document that doesn't a Kontent content item. + /// The taxonomy terms or an empty array. public static ITaxonomyTerm[] AsKontentTaxonomyTerms(this IDocument document) { if (document == null) @@ -96,9 +96,14 @@ public static ITaxonomyTerm[] AsKontentTaxonomyTerms(this IDocument document) throw new ArgumentNullException(nameof(document), "Expected a document but got "); } - if (document.TryGetValue(KontentTaxonomyTermsKey, out ITaxonomyTerm[] contentItem)) + if (document.TryGetValue(KontentKeys.Taxonomy.Terms, out object? terms)) { - return contentItem; + return terms switch + { + ITaxonomyTerm[] items => items, + ITaxonomyTermDetails[] details => details.Select(TaxonomyTerm.CreateFrom).ToArray(), + _ => Array.Empty() + }; } return Array.Empty(); From 4296062f04916cb001fda0c5b7ea5ad7bf9ab7f0 Mon Sep 17 00:00:00 2001 From: Marnix van Valen Date: Sat, 19 Dec 2020 18:25:25 +0100 Subject: [PATCH 08/10] Code quality, warnings --- Kontent.Statiq.Tests/Models/AboutUs.cs | 2 + Kontent.Statiq.Tests/Models/Accessory.cs | 2 + Kontent.Statiq.Tests/Models/Article.cs | 2 + Kontent.Statiq.Tests/Models/Brewer.cs | 2 + Kontent.Statiq.Tests/Models/Cafe.cs | 2 + Kontent.Statiq.Tests/Models/Coffee.cs | 2 + .../Models/CustomTypeProvider.cs | 4 +- Kontent.Statiq.Tests/Models/FactAboutUs.cs | 2 + Kontent.Statiq.Tests/Models/Grinder.cs | 2 + Kontent.Statiq.Tests/Models/HeroUnit.cs | 2 + Kontent.Statiq.Tests/Models/Home.cs | 2 + Kontent.Statiq.Tests/Models/HostedVideo.cs | 2 + Kontent.Statiq.Tests/Models/Office.cs | 2 + Kontent.Statiq.Tests/Models/Tweet.cs | 2 + .../Tools/TestContentItemSystemAttributes.cs | 14 ++-- .../When_executing_a_Statiq_pipeline.cs | 2 +- .../When_rendering_a_Razor_view.cs | 8 -- .../When_working_with_related_documents.cs | 79 +++++++++---------- Kontent.Statiq/KontentDocumentHelpers.cs | 2 - Kontent.Statiq/KontentKeys.cs | 3 + 20 files changed, 75 insertions(+), 63 deletions(-) diff --git a/Kontent.Statiq.Tests/Models/AboutUs.cs b/Kontent.Statiq.Tests/Models/AboutUs.cs index 7cdea5c..34c7263 100644 --- a/Kontent.Statiq.Tests/Models/AboutUs.cs +++ b/Kontent.Statiq.Tests/Models/AboutUs.cs @@ -27,6 +27,7 @@ public partial class AboutUs public const string SitemapCodename = "sitemap"; public const string UrlPatternCodename = "url_pattern"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public IEnumerable Facts { get; set; } public string MetadataMetaDescription { get; set; } public string MetadataMetaTitle { get; set; } @@ -41,5 +42,6 @@ public partial class AboutUs public IEnumerable Sitemap { get; set; } public IContentItemSystemAttributes System { get; set; } public string UrlPattern { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/Accessory.cs b/Kontent.Statiq.Tests/Models/Accessory.cs index 9d386f7..3e11739 100644 --- a/Kontent.Statiq.Tests/Models/Accessory.cs +++ b/Kontent.Statiq.Tests/Models/Accessory.cs @@ -33,6 +33,7 @@ public partial class Accessory public const string SitemapCodename = "sitemap"; public const string UrlPatternCodename = "url_pattern"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public IEnumerable Image { get; set; } public IRichTextContent LongDescription { get; set; } public string Manufacturer { get; set; } @@ -53,5 +54,6 @@ public partial class Accessory public IEnumerable Sitemap { get; set; } public IContentItemSystemAttributes System { get; set; } public string UrlPattern { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/Article.cs b/Kontent.Statiq.Tests/Models/Article.cs index 34ef4da..c6152dd 100644 --- a/Kontent.Statiq.Tests/Models/Article.cs +++ b/Kontent.Statiq.Tests/Models/Article.cs @@ -35,6 +35,7 @@ public partial class Article public const string TitleCodename = "title"; public const string UrlPatternCodename = "url_pattern"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public IRichTextContent BodyCopy { get; set; } public string MetadataMetaDescription { get; set; } public string MetadataMetaTitle { get; set; } @@ -57,5 +58,6 @@ public partial class Article public IEnumerable TeaserImage { get; set; } public string Title { get; set; } public string UrlPattern { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/Brewer.cs b/Kontent.Statiq.Tests/Models/Brewer.cs index 14d30c8..24a37a5 100644 --- a/Kontent.Statiq.Tests/Models/Brewer.cs +++ b/Kontent.Statiq.Tests/Models/Brewer.cs @@ -33,6 +33,7 @@ public partial class Brewer public const string SitemapCodename = "sitemap"; public const string UrlPatternCodename = "url_pattern"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public IEnumerable Image { get; set; } public IRichTextContent LongDescription { get; set; } public IEnumerable Manufacturer { get; set; } @@ -53,5 +54,6 @@ public partial class Brewer public IEnumerable Sitemap { get; set; } public IContentItemSystemAttributes System { get; set; } public string UrlPattern { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/Cafe.cs b/Kontent.Statiq.Tests/Models/Cafe.cs index b94bd7a..1945f68 100644 --- a/Kontent.Statiq.Tests/Models/Cafe.cs +++ b/Kontent.Statiq.Tests/Models/Cafe.cs @@ -23,6 +23,7 @@ public partial class Cafe public const string StreetCodename = "street"; public const string ZipCodeCodename = "zip_code"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public string City { get; set; } public string Country { get; set; } public string Email { get; set; } @@ -33,5 +34,6 @@ public partial class Cafe public string Street { get; set; } public IContentItemSystemAttributes System { get; set; } public string ZipCode { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/Coffee.cs b/Kontent.Statiq.Tests/Models/Coffee.cs index 885917c..2e85be9 100644 --- a/Kontent.Statiq.Tests/Models/Coffee.cs +++ b/Kontent.Statiq.Tests/Models/Coffee.cs @@ -37,6 +37,7 @@ public partial class Coffee public const string UrlPatternCodename = "url_pattern"; public const string VarietyCodename = "variety"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public string Altitude { get; set; } public string Country { get; set; } public string Farm { get; set; } @@ -61,5 +62,6 @@ public partial class Coffee public IContentItemSystemAttributes System { get; set; } public string UrlPattern { get; set; } public string Variety { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/CustomTypeProvider.cs b/Kontent.Statiq.Tests/Models/CustomTypeProvider.cs index 4de81a3..9e9748f 100644 --- a/Kontent.Statiq.Tests/Models/CustomTypeProvider.cs +++ b/Kontent.Statiq.Tests/Models/CustomTypeProvider.cs @@ -24,14 +24,14 @@ public class CustomTypeProvider : ITypeProvider {typeof(Tweet), "tweet"} }; - public Type GetType(string contentType) + public Type? GetType(string contentType) { return _codenames.Keys.FirstOrDefault(type => GetCodename(type).Equals(contentType)); } public string GetCodename(Type contentType) { - return _codenames.TryGetValue(contentType, out var codename) ? codename : null; + return _codenames.TryGetValue(contentType, out var codename) ? codename : ""; } } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/FactAboutUs.cs b/Kontent.Statiq.Tests/Models/FactAboutUs.cs index 4d80e76..f450817 100644 --- a/Kontent.Statiq.Tests/Models/FactAboutUs.cs +++ b/Kontent.Statiq.Tests/Models/FactAboutUs.cs @@ -18,10 +18,12 @@ public partial class FactAboutUs public const string SitemapCodename = "sitemap"; public const string TitleCodename = "title"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public IRichTextContent Description { get; set; } public IEnumerable Image { get; set; } public IEnumerable Sitemap { get; set; } public IContentItemSystemAttributes System { get; set; } public string Title { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/Grinder.cs b/Kontent.Statiq.Tests/Models/Grinder.cs index 45c2cfa..fe52271 100644 --- a/Kontent.Statiq.Tests/Models/Grinder.cs +++ b/Kontent.Statiq.Tests/Models/Grinder.cs @@ -33,6 +33,7 @@ public partial class Grinder public const string SitemapCodename = "sitemap"; public const string UrlPatternCodename = "url_pattern"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public IEnumerable Image { get; set; } public IRichTextContent LongDescription { get; set; } public string Manufacturer { get; set; } @@ -53,5 +54,6 @@ public partial class Grinder public IEnumerable Sitemap { get; set; } public IContentItemSystemAttributes System { get; set; } public string UrlPattern { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/HeroUnit.cs b/Kontent.Statiq.Tests/Models/HeroUnit.cs index dc1e175..6d768da 100644 --- a/Kontent.Statiq.Tests/Models/HeroUnit.cs +++ b/Kontent.Statiq.Tests/Models/HeroUnit.cs @@ -18,10 +18,12 @@ public partial class HeroUnit public const string SitemapCodename = "sitemap"; public const string TitleCodename = "title"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public IEnumerable Image { get; set; } public IRichTextContent MarketingMessage { get; set; } public IEnumerable Sitemap { get; set; } public IContentItemSystemAttributes System { get; set; } public string Title { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/Home.cs b/Kontent.Statiq.Tests/Models/Home.cs index 45db07f..f29a1ce 100644 --- a/Kontent.Statiq.Tests/Models/Home.cs +++ b/Kontent.Statiq.Tests/Models/Home.cs @@ -31,6 +31,7 @@ public partial class Home public const string SitemapCodename = "sitemap"; public const string UrlPatternCodename = "url_pattern"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public IEnumerable Articles { get; set; } public IEnumerable Cafes { get; set; } public IRichTextContent Contact { get; set; } @@ -49,5 +50,6 @@ public partial class Home public IEnumerable Sitemap { get; set; } public IContentItemSystemAttributes System { get; set; } public string UrlPattern { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/HostedVideo.cs b/Kontent.Statiq.Tests/Models/HostedVideo.cs index 383e4b0..225f6ad 100644 --- a/Kontent.Statiq.Tests/Models/HostedVideo.cs +++ b/Kontent.Statiq.Tests/Models/HostedVideo.cs @@ -16,8 +16,10 @@ public partial class HostedVideo public const string VideoHostCodename = "video_host"; public const string VideoIdCodename = "video_id"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public IContentItemSystemAttributes System { get; set; } public IEnumerable VideoHost { get; set; } public string VideoId { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/Office.cs b/Kontent.Statiq.Tests/Models/Office.cs index 7b7c833..4f3bc7a 100644 --- a/Kontent.Statiq.Tests/Models/Office.cs +++ b/Kontent.Statiq.Tests/Models/Office.cs @@ -23,6 +23,7 @@ public partial class Office public const string StreetCodename = "street"; public const string ZipCodeCodename = "zip_code"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public string City { get; set; } public string Country { get; set; } public string Email { get; set; } @@ -33,5 +34,6 @@ public partial class Office public string Street { get; set; } public IContentItemSystemAttributes System { get; set; } public string ZipCode { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Models/Tweet.cs b/Kontent.Statiq.Tests/Models/Tweet.cs index 8a3d7ce..e1f6d0a 100644 --- a/Kontent.Statiq.Tests/Models/Tweet.cs +++ b/Kontent.Statiq.Tests/Models/Tweet.cs @@ -17,9 +17,11 @@ public partial class Tweet public const string ThemeCodename = "theme"; public const string TweetLinkCodename = "tweet_link"; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public IEnumerable DisplayOptions { get; set; } public IContentItemSystemAttributes System { get; set; } public IEnumerable Theme { get; set; } public string TweetLink { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/Tools/TestContentItemSystemAttributes.cs b/Kontent.Statiq.Tests/Tools/TestContentItemSystemAttributes.cs index e843ff5..24e3dad 100644 --- a/Kontent.Statiq.Tests/Tools/TestContentItemSystemAttributes.cs +++ b/Kontent.Statiq.Tests/Tools/TestContentItemSystemAttributes.cs @@ -9,13 +9,13 @@ namespace Kontent.Statiq.Tests.Tools /// internal sealed class TestContentItemSystemAttributes : IContentItemSystemAttributes { - public string Id { get; internal set; } - public string Name { get; internal set; } - public string Codename { get; internal set; } - public string Type { get; internal set; } - public string Collection { get; } - public IList SitemapLocation { get; internal set; } + public string Id { get; internal set; } = ""; + public string Name { get; internal set; } = ""; + public string Codename { get; internal set; } = ""; + public string Type { get; internal set; } = ""; + public string Collection { get; } = ""; + public IList SitemapLocation { get; internal set; } = Array.Empty(); public DateTime LastModified { get; internal set; } - public string Language { get; internal set; } + public string Language { get; internal set; } = ""; } } \ No newline at end of file diff --git a/Kontent.Statiq.Tests/When_executing_a_Statiq_pipeline.cs b/Kontent.Statiq.Tests/When_executing_a_Statiq_pipeline.cs index d80ce74..ec4ff2b 100644 --- a/Kontent.Statiq.Tests/When_executing_a_Statiq_pipeline.cs +++ b/Kontent.Statiq.Tests/When_executing_a_Statiq_pipeline.cs @@ -89,7 +89,7 @@ public async Task It_should_correctly_set_the_default_content_from_richtext() var deliveryClient = A.Fake().WithFakeContent(content); var sut = new Kontent
(deliveryClient) - .WithContent(item => item.BodyCopy.ToString() ); + .WithContent(item => item.BodyCopy.ToString() ?? "" ); // Act var engine = SetupExecution(sut, diff --git a/Kontent.Statiq.Tests/When_rendering_a_Razor_view.cs b/Kontent.Statiq.Tests/When_rendering_a_Razor_view.cs index 2f65894..d096a7b 100644 --- a/Kontent.Statiq.Tests/When_rendering_a_Razor_view.cs +++ b/Kontent.Statiq.Tests/When_rendering_a_Razor_view.cs @@ -10,19 +10,11 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace Kontent.Statiq.Tests { public class When_rendering_a_Razor_view { - private readonly ITestOutputHelper _testOutput; - - public When_rendering_a_Razor_view(ITestOutputHelper testOutput) - { - _testOutput = testOutput; - } - [Fact] public async Task It_should_pickup_Layout_and_view_start() { diff --git a/Kontent.Statiq.Tests/When_working_with_related_documents.cs b/Kontent.Statiq.Tests/When_working_with_related_documents.cs index 38de08e..ad61877 100644 --- a/Kontent.Statiq.Tests/When_working_with_related_documents.cs +++ b/Kontent.Statiq.Tests/When_working_with_related_documents.cs @@ -5,7 +5,6 @@ using Kontent.Statiq.Tests.Tools; using Statiq.Common; using Statiq.Core; -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -22,21 +21,21 @@ public async Task It_should_map_child_page_collections() var home = new Home(); var sub1 = CreateArticle("Sub 1"); var sub2 = CreateArticle("Sub 2"); - home.Articles = new[] { sub1, sub2 }; + home.Articles = new[] {sub1, sub2}; var deliveryClient = A.Fake().WithFakeContent(home); var sut = new Kontent(deliveryClient); // Act - var engine = SetupExecution(sut, - new[]{ - new AddDocumentsToMetadata(Keys.Children, KontentConfig.GetChildren( page => page.Articles ) ), - }, - // Assert - async docs => docs.FirstOrDefault().GetChildren().Should().HaveCount(2) - ); - await engine.ExecuteAsync(); + var docs = await Execute(sut, + new[] + { + new AddDocumentsToMetadata(Keys.Children, KontentConfig.GetChildren(page => page.Articles)), + }); + + // Assert + docs.FirstOrDefault().GetChildren().Should().HaveCount(2); } [Fact] @@ -46,7 +45,7 @@ public async Task It_should_allow_multiple_child_page_collections() var home = new Home(); var sub1 = CreateArticle("Sub 1"); var sub2 = CreateArticle("Sub 2"); - home.Articles = new[] { sub1, sub2 }; + home.Articles = new[] {sub1, sub2}; home.Cafes = new[] { CreateCafe("Ok Café") @@ -57,26 +56,22 @@ public async Task It_should_allow_multiple_child_page_collections() var sut = new Kontent(deliveryClient); // Act - var engine = SetupExecution(sut, - new[]{ + var docs = await Execute(sut, + new[] + { new AddKontentDocumentsToMetadata(page => page.Articles), new AddKontentDocumentsToMetadata("cafes", page => page.Cafes), - }, - - // Assert - async docs => - { - var articles = docs.FirstOrDefault().GetChildren(); - articles.Should().HaveCount(2); - articles.Should().Contain(a => string.Equals(a[KontentKeys.System.Name], "Sub 1")); - articles.Should().Contain(a => string.Equals(a[KontentKeys.System.Name], "Sub 2")); + }); - var cafes = docs.FirstOrDefault().GetChildren("cafes"); - cafes.Should().HaveCount(1); - cafes.First()[KontentKeys.System.Name].Should().Be("Ok Café"); + // Assert + var articles = docs.FirstOrDefault().GetChildren(); + articles.Should().HaveCount(2); + articles.Should().Contain(a => string.Equals(a[KontentKeys.System.Name], "Sub 1")); + articles.Should().Contain(a => string.Equals(a[KontentKeys.System.Name], "Sub 2")); - }); - await engine.ExecuteAsync(); + var cafes = docs.FirstOrDefault().GetChildren("cafes"); + cafes.Should().HaveCount(1); + cafes.First()[KontentKeys.System.Name].Should().Be("Ok Café"); } [Fact] @@ -85,7 +80,7 @@ public async Task It_should_not_throw_on_null_or_empty_collections() // Arrange var home = new Home { - Articles = null!, + Articles = null!, Cafes = new Cafe[0] }; @@ -94,22 +89,19 @@ public async Task It_should_not_throw_on_null_or_empty_collections() var sut = new Kontent(deliveryClient); // Act - var engine = SetupExecution(sut, - new[]{ + var docs = await Execute(sut, + new[] + { new AddKontentDocumentsToMetadata(page => page.Articles), new AddKontentDocumentsToMetadata("cafes", page => page.Cafes), - }, + }); - // Assert - async docs => - { - var articles = docs.FirstOrDefault().GetChildren(); - articles.Should().HaveCount(0); + // Assert + var articles = docs.FirstOrDefault().GetChildren(); + articles.Should().HaveCount(0); - var cafes = docs.FirstOrDefault().GetChildren("cafes"); - cafes.Should().HaveCount(0); - }); - await engine.ExecuteAsync(); + var cafes = docs.FirstOrDefault().GetChildren("cafes"); + cafes.Should().HaveCount(0); } private static Article CreateArticle(string content) @@ -124,18 +116,19 @@ private static Cafe CreateCafe(string name) return new Cafe { System = new TestContentItemSystemAttributes{ Name = name }}; } - private static Engine SetupExecution(Kontent kontentModule, IModule[] processModules, Func, Task> test) where TContent : class + private static async Task> Execute(Kontent kontentModule, IModule[] processModules) where TContent : class { var engine = new Engine(); var pipeline = new Pipeline() { InputModules = { kontentModule }, - OutputModules = { new TestModule(test) } }; pipeline.ProcessModules.AddRange(processModules); engine.Pipelines.Add("test", pipeline); - return engine; + var result = await engine.ExecuteAsync(); + + return result.FromPipeline("Test"); } } } diff --git a/Kontent.Statiq/KontentDocumentHelpers.cs b/Kontent.Statiq/KontentDocumentHelpers.cs index cddd99c..c18b27d 100644 --- a/Kontent.Statiq/KontentDocumentHelpers.cs +++ b/Kontent.Statiq/KontentDocumentHelpers.cs @@ -54,8 +54,6 @@ private static Task CreateDocumentInternal(IExecutionContext context, /// private static void MapSystemMetadata(object item, PropertyInfo[] props, List> metadata) { - // TODO : add a check here to validate that this in fact a Kontent item? - if (props.FirstOrDefault(prop => typeof(IContentItemSystemAttributes).IsAssignableFrom(prop.PropertyType)) ?.GetValue(item) is IContentItemSystemAttributes systemProp) { diff --git a/Kontent.Statiq/KontentKeys.cs b/Kontent.Statiq/KontentKeys.cs index a2997b8..2629933 100644 --- a/Kontent.Statiq/KontentKeys.cs +++ b/Kontent.Statiq/KontentKeys.cs @@ -53,6 +53,9 @@ public static class Images public const string Downloads = "KONTENT-ASSET-DOWNLOADS"; } + /// + /// Keys for well-known Statiq document metadata for strong-typed taxonomy data. + /// public static class Taxonomy { /// From 2c8991c333bc42efff54db3704b9423f77b55529 Mon Sep 17 00:00:00 2001 From: Marnix van Valen Date: Sun, 20 Dec 2020 20:40:26 +0100 Subject: [PATCH 09/10] Docs + nesting + root node collapse --- .../When_working_with_taxonomy.cs | 38 ++++- Kontent.Statiq/KontentTaxonomy.cs | 34 ++++- Kontent.Statiq/KontentTaxonomyHelpers.cs | 68 +++++---- Kontent.Statiq/TypedContentExtensions.cs | 25 ++- README.md | 142 +++++++++++++++++- 5 files changed, 264 insertions(+), 43 deletions(-) diff --git a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs index bfcbf49..ea3bf4e 100644 --- a/Kontent.Statiq.Tests/When_working_with_taxonomy.cs +++ b/Kontent.Statiq.Tests/When_working_with_taxonomy.cs @@ -41,6 +41,7 @@ public async Task It_should_load_taxonomy() InputModules = { new KontentTaxonomy(deliveryClient) + .WithNesting(), } }; @@ -65,6 +66,7 @@ public async Task It_should_load_taxonomy_and_retrieve_it() InputModules = { new KontentTaxonomy(deliveryClient) + .WithNesting() } }; @@ -87,6 +89,7 @@ public async Task It_should_load_taxonomy_and_retrieve_its_terms() InputModules = { new KontentTaxonomy(deliveryClient) + .WithNesting() } }; @@ -109,7 +112,8 @@ public async Task It_should_flatten_taxonomy() { InputModules = { - new KontentTaxonomy(deliveryClient), + new KontentTaxonomy(deliveryClient) + .WithNesting(), new FlattenTree(), new SetDestination(Config.FromDocument((doc,ctx)=> new NormalizedPath( doc.Get(KontentKeys.System.Name)))), @@ -182,10 +186,12 @@ public async Task It_should_enable_grouping_documents_into_trees() { // Arrange var group = SetupTaxonomyGroup("Menu", "Product", "Recipe", "Featured"); + group.Terms.First().Terms.AddRange(CreateTaxonomyTerms("Sugar-free","Vegan","Low-carb")); var recipe = new TestTaxonomyTerm { Codename = "recipe", Name = "Recipe" }; var featured = new TestTaxonomyTerm { Codename = "featured", Name = "Featured" }; var product = new TestTaxonomyTerm { Codename = "product", Name = "Product" }; + var lowCarb = new TestTaxonomyTerm {Codename = "low-carb", Name = "Low-carb"}; var articles = new[] { @@ -206,6 +212,12 @@ public async Task It_should_enable_grouping_documents_into_trees() System = new TestContentItemSystemAttributes(){ Name = "buy-cookies"}, Title = "Chocolate chip cookies", Sitemap = new ITaxonomyTerm[] { product, featured } + }, + new Article + { + System = new TestContentItemSystemAttributes(){ Name = "low-carb-cookies"}, + Title = "Low-carb cookies", + Sitemap = new ITaxonomyTerm[] { product, lowCarb } } }; @@ -218,7 +230,10 @@ public async Task It_should_enable_grouping_documents_into_trees() { InputModules = { - new Kontent
(deliveryClient) + new Kontent
(deliveryClient), + // Set taxonomy terms as metadata + new SetMetadata("Tags", KontentConfig.Get((Article art) => art.Sitemap)), + new GroupDocuments( "Tags" ).WithComparer(new TaxonomyTermComparer()) } }; @@ -229,16 +244,16 @@ public async Task It_should_enable_grouping_documents_into_trees() { new KontentTaxonomy(deliveryClient) .WithQuery(new EqualsFilter("system.codename", "menu")), - new FlattenTree(), }, ProcessModules = { new SetMetadata( Keys.Children, Config.FromDocument((doc, ctx) => { + var taxonomyTerm = doc.AsKontentTaxonomyTerm()?.Codename; + return ctx.Outputs.FromPipeline("Articles") - .Where( art => art.AsKontent
().Sitemap - .Contains(doc.AsKontentTaxonomyTerm()!, new TaxonomyTermComparer()) ) - .ToArray(); - }) ) + .FirstOrDefault(x => x.AsKontentTaxonomyTerm(Keys.GroupKey)?.Codename == taxonomyTerm) + ?.GetChildren().ToArray(); + })) } }; @@ -252,11 +267,18 @@ public async Task It_should_enable_grouping_documents_into_trees() // Assert var sitemap = results.FromPipeline("Menu"); + sitemap.Select(node => node.Get(Keys.Title)) + .Should().BeEquivalentTo("Product", "Featured", "Vegan", "Low-carb", "Recipe", "Sugar-free"); var productSection = sitemap.FirstOrDefault( p => p.Get(Keys.Title) == "Product" ); - productSection.GetChildren().Should().HaveCount(1); + productSection.GetChildren().Should().HaveCount(2); + var lowCarbSection = sitemap.FirstOrDefault( p => p.Get(Keys.Title) == "Low-carb" ); + lowCarbSection.GetChildren().Should().HaveCount(1); + lowCarbSection.Get(Keys.TreePath).Should().Equal( "product", "low-carb"); var featuredSection = sitemap.FirstOrDefault(p => p.Get(Keys.Title) == "Featured"); featuredSection.GetChildren().Should().HaveCount(2); + + _output.WriteLine( string.Join("\n", sitemap.Select( doc => string.Join( "/", doc.Get(Keys.TreePath)) + $" ({doc.GetChildren().Count})"))); } diff --git a/Kontent.Statiq/KontentTaxonomy.cs b/Kontent.Statiq/KontentTaxonomy.cs index dd58310..2078c03 100644 --- a/Kontent.Statiq/KontentTaxonomy.cs +++ b/Kontent.Statiq/KontentTaxonomy.cs @@ -1,5 +1,6 @@ using Kentico.Kontent.Delivery.Abstractions; using Statiq.Common; +using Statiq.Core; using System; using System.Collections.Generic; using System.Linq; @@ -14,6 +15,8 @@ namespace Kontent.Statiq public sealed class KontentTaxonomy : Module { private readonly IDeliveryClient _client; + private bool _nesting = false; + private bool _collapseRoot = true; internal List QueryParameters { get; } = new List(); @@ -27,14 +30,41 @@ public KontentTaxonomy(IDeliveryClient client) _client = client ?? throw new ArgumentNullException(nameof(client), $"{nameof(client)} must not be null"); } + /// + /// Indicates that the module should only output root nodes (instead of all + /// nodes which is the default behavior). + /// + /// true to enable nesting and only output the root nodes. + /// + /// Indicates that the root of the tree should be collapsed and the module + /// should output first-level documents as if they were root documents. This setting + /// has no effect if not nesting. + /// + /// The current module instance. + public KontentTaxonomy WithNesting(bool nesting = true, bool collapseRoot = false) + { + _nesting = nesting; + _collapseRoot = collapseRoot; + return this; + } + /// protected override async Task> ExecuteContextAsync(IExecutionContext context) { var items = await _client.GetTaxonomiesAsync(QueryParameters); - var documentTasks = items.Taxonomies.Select(item => KontentTaxonomyHelpers.CreateDocument(context, item)).ToArray(); + var documentTasks = items.Taxonomies.Select(item => KontentTaxonomyHelpers.CreateDocument(context, item, _collapseRoot)).ToArray(); + + var results = await Task.WhenAll(documentTasks); + var documents = results.SelectMany(d => d).ToArray(); + + if (!_nesting) + { + // flatten the hierarchy + documents = documents.Flatten(Keys.TreePlaceholder, Keys.Children).ToArray(); + } - return await Task.WhenAll(documentTasks); + return documents; } } } \ No newline at end of file diff --git a/Kontent.Statiq/KontentTaxonomyHelpers.cs b/Kontent.Statiq/KontentTaxonomyHelpers.cs index 8815e68..13cf615 100644 --- a/Kontent.Statiq/KontentTaxonomyHelpers.cs +++ b/Kontent.Statiq/KontentTaxonomyHelpers.cs @@ -2,34 +2,63 @@ using Statiq.Common; using System.Collections.Generic; using System.Linq; +using System; using System.Threading.Tasks; namespace Kontent.Statiq { internal static class KontentTaxonomyHelpers { - internal static async Task CreateDocument(IExecutionContext context, ITaxonomyGroup item) + internal static async Task> CreateDocument(IExecutionContext context, ITaxonomyGroup item, bool collapseRoot) { - var treePath = new []{item.System.Codename}; + string[] treePath = collapseRoot ? Array.Empty() : new[] {item.System.Codename}; + IEnumerable? terms = Array.Empty(); + + if (item.Terms != null && item.Terms.Count > 0) + { + terms = await item.Terms.ParallelSelectAsync(async t => await CreateDocument(context, t, treePath)); + } + + if (collapseRoot) + { + return terms; + } + + var metadata = BuildRootNode(item, treePath, terms); + var root = await context.CreateDocumentAsync(metadata, "", "text/html"); + return root.Yield(); + } + private static IList> BuildRootNode(ITaxonomyGroup item, string[] treePath, IEnumerable terms) + { var metadata = new List> { new KeyValuePair(Keys.Title, item.System.Name), new KeyValuePair(Keys.GroupKey, item.System.Codename), new KeyValuePair(Keys.TreePath, treePath), - new KeyValuePair(KontentKeys.Taxonomy.Group, item) + new KeyValuePair(KontentKeys.Taxonomy.Group, item), }; - - MapSystemMetadata(item, metadata); - + if (item.Terms != null && item.Terms.Count > 0) { - var terms = await item.Terms.ParallelSelectAsync(async t => await CreateDocument(context, t, treePath)); - metadata.Add(new KeyValuePair(Keys.Children, terms)); - metadata.Add(new KeyValuePair(KontentKeys.Taxonomy.Terms, item.Terms.ToArray())); + metadata.AddRange( + new KeyValuePair(Keys.Children, terms), + new KeyValuePair(KontentKeys.Taxonomy.Terms, item.Terms?.ToArray() ?? Array.Empty()) + ); } - return await context.CreateDocumentAsync(metadata, "", "text/html"); + if( item.System != null ) + { + metadata.AddRange(new[] + { + new KeyValuePair(KontentKeys.System.Name, item.System.Name), + new KeyValuePair(KontentKeys.System.CodeName, item.System.Codename), + new KeyValuePair(KontentKeys.System.Id, item.System.Id), + new KeyValuePair(KontentKeys.System.LastModified, item.System.LastModified), + }); + } + + return metadata; } private static async Task CreateDocument(IExecutionContext context, ITaxonomyTermDetails item, string[] parentPath) @@ -54,24 +83,5 @@ private static async Task CreateDocument(IExecutionContext context, I return await context.CreateDocumentAsync(metadata, "", "text/html"); } - - /// - /// Map Kontent system properties directly into document metadata. - /// - /// The taxonomy group. - /// The metadata collection. - private static void MapSystemMetadata(ITaxonomyGroup item, List> metadata) - { - if( item.System != null ) - { - metadata.AddRange(new[] - { - new KeyValuePair(KontentKeys.System.Name, item.System.Name), - new KeyValuePair(KontentKeys.System.CodeName, item.System.Codename), - new KeyValuePair(KontentKeys.System.Id, item.System.Id), - new KeyValuePair(KontentKeys.System.LastModified, item.System.LastModified), - }); - } - } } } \ No newline at end of file diff --git a/Kontent.Statiq/TypedContentExtensions.cs b/Kontent.Statiq/TypedContentExtensions.cs index ebba816..afb6f65 100644 --- a/Kontent.Statiq/TypedContentExtensions.cs +++ b/Kontent.Statiq/TypedContentExtensions.cs @@ -65,13 +65,23 @@ public static ITaxonomyGroup AsKontentTaxonomy(this IDocument document) /// The Document. /// The taxonomy term or null. public static ITaxonomyTerm? AsKontentTaxonomyTerm(this IDocument document) + { + return document.AsKontentTaxonomyTerm(KontentKeys.Taxonomy.Term); + } + + /// + /// Return the strong typed model for a Statiq document. + /// + /// The Document. + /// The taxonomy term or null. + public static ITaxonomyTerm? AsKontentTaxonomyTerm(this IDocument document, string key) { if (document == null) { throw new ArgumentNullException(nameof(document), "Expected a document but got "); } - if (document.TryGetValue(KontentKeys.Taxonomy.Term, out ITaxonomyTerm term)) + if (document.TryGetValue(key, out ITaxonomyTerm term)) { return term; } @@ -90,13 +100,24 @@ public static ITaxonomyGroup AsKontentTaxonomy(this IDocument document) /// The Document. /// The taxonomy terms or an empty array. public static ITaxonomyTerm[] AsKontentTaxonomyTerms(this IDocument document) + { + return document.AsKontentTaxonomyTerms(KontentKeys.Taxonomy.Terms); + } + + /// + /// Return the strong typed model for a Statiq document. + /// + /// The Document. + /// + /// The taxonomy terms or an empty array. + public static ITaxonomyTerm[] AsKontentTaxonomyTerms(this IDocument document, string key) { if (document == null) { throw new ArgumentNullException(nameof(document), "Expected a document but got "); } - if (document.TryGetValue(KontentKeys.Taxonomy.Terms, out object? terms)) + if (document.TryGetValue(key, out object? terms)) { return terms switch { diff --git a/README.md b/README.md index 75f08a3..e72544e 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,6 @@ Both these content models can be used with Statiq, it's up to your preferences. Where ever Statiq hands you an `IDocument`, use the extension method `.AsKontent()` to get the typed model from the document. - ## Accessing content Kontent.Statiq wraps the strong typed content model in a Statiq Document. In order to access the strong typed model within the document, a couple of helpers are available. @@ -292,6 +291,141 @@ public class DownloadImages : Pipeline The `KontentImageProcessor` will add any replaced image url to a collection in the Document metadata for processing by the `KontentDownloadImages` module at a later stage. +## Taxonomy + +The `KontentTaxonomy` module allows you to get the taxonomies defined in Kontent. You can download a specific taxonomy group or a single group using filters. + +```csharp +public class Taxonomies : Pipeline +{ + public Taxonomies(IDeliveryClient client) + { + PostProcessModules = new ModuleList( + // Load the taxonomy named 'menu' + new KontentTaxonomy(client) + .WithQuery(new EqualsFilter("system.codename", "menu")) + ); + } +} +``` +This will return all the terms for the taxonomy group _menu_ as a list. For convenience the root node is skipped because you cannot select the root node as a tag in Kontent. + +### Returning a tree or the root node + +Using the `.WithNesting(nesting:true, collapseRoot:false)` method on the module, you can make the module return a hierarchical structure of documents and control whether the root node is returned as well. + +```csharp +public class Taxonomies : Pipeline +{ + public Taxonomies(IDeliveryClient client) + { + PostProcessModules = new ModuleList( + // Load the taxonomy named 'menu' + new KontentTaxonomy(client) + .WithQuery(new EqualsFilter("system.codename", "menu")) + ); + } +} +``` + +To flatten this into a list, use the [`FlattenTree`](https://github.com/statiqdev/Statiq.Framework/blob/main/src/core/Statiq.Core/Modules/Metadata/FlattenTree.cs) module. + +### Grouping documents by taxonomy + +If you add a taxonomy field to one of your content types, this will result in a property of type `IEnumerable` in the generated model class. You can match these terms up to a taxonomy group to get a structured set of pages. This enables you to [manage the structure of the site using taxonomy](https://docs.kontent.ai/tutorials/develop-apps/optimize-your-app/sitemaps-for-seo#a-tag-items-with-a-taxonomy-to-create-a-sitemap) or generate an overview of documents per tag. Note that you don't need to load the entire taxonomy if you don't need that structure. + +```csharp +new Pipeline +{ + InputModules = + { + new Kontent
(deliveryClient), + // Set taxonomy terms as metadata + new SetMetadata("Tags", KontentConfig.Get((Article art) => art.Tags)), + // Group by taxonomy + new GroupDocuments( "Tags" ) + .WithComparer(new TaxonomyTermComparer()) + } +} +``` +The output for this pipeline is a flat list of documents, one for each taxonomy term with the documents for that term as its children. +The taxonomy term on each document is avalailable through `doc.AsTaxonomyTerm(Keys.GroupKey)`. + +### Structuring documents by taxonomy + +If you do need the structure of the taxonomy, for example to build a sitemap, you can join content to the taxonomy data returned by the `KontentTaxonomy` module. + +```csharp +var articlePipeline = new Pipeline +{ + InputModules = + { + // Load all articles + new Kontent
(deliveryClient), + // Set taxonomy terms as metadata + new SetMetadata("Tags", KontentConfig.Get((Article art) => art.Sitemap)), + // Group the articles by tag + new GroupDocuments( "Tags" ).WithComparer(new TaxonomyTermComparer()) + } +}; + +var sitemapPipeline = new Pipeline +{ + Dependencies = { "Articles" }, + InputModules = + { + // Load the taxonomy as a flat list + new KontentTaxonomy(deliveryClient) + .WithQuery(new EqualsFilter("system.codename", "menu")), + }, + ProcessModules = { + new SetMetadata( Keys.Children, Config.FromDocument((doc, ctx) => + { + var taxonomyTerm = doc.AsKontentTaxonomyTerm().Codename; + + // Find the articles for this term + return ctx.Outputs.FromPipeline("Articles") + .FirstOrDefault( x => x.AsKontentTaxonomyTerm(Keys.GroupKey)?.Codename==taxonomyTerm) + ?.GetChildren() + .ToArray(); + })) + } +}; +``` + +The output of the sitemap pipeline is a flat list of nodes representing the taxonomy terms. Each nodes children are the documents that are tagged with that term. + +Note that the path to each node in the tree is available through `doc.Get(Keys.TreePath)`. + +### Taxonomy helpers + +Extensions on `IDocument` that can be used with documents loaded through the `KontentTaxonomy` module: + +| Example | Description | +|------|------| +| `.AsKontentTaxonomy()` | Fetch the entire `ITaxonomyGroup` from a root taxonomy document. | +| `.AsKontentTaxonomyTerm()` | Fetch a single term from an IDocument | +| `.AsKomtentTaxonomyTerms()` | Fetch a list of child terms from an IDocument, if any. + +### Taxonomy metadata + +All the keys used by the Modules provided are listed in the [KontentKeys](https://github.com/alanta/Kontent.Statiq/blob/main/Kontent.Statiq/KontentKeys.cs) class. +For most cases there is a helper or extension to access the metadata but if you need direct access, you can use these keys to do so. + +| Key | Value | +|------|------| +| `Keys.Title` | The name of the term or group | +| `Keys.GroupKey` | The code name of the term or group | +| `Keys.TreePath` | A `string[]` containing the path of the node in the taxonomy by code name | +| `Keys.Children` | The list of child terms as Statiq `IDocument` | +| `KontentKeys.Taxonomy.Terms` | The child terms as `ITaxonomyTermDetails[]` | +| `KontentKeys.Taxonomy.Group` | The taxonomy group as `ITaxonomyGroup` | +| `KontentKeys.Taxonomy.Term` | The taxonomy term as `ITaxonomyTermDetails` | +| `KontentKeys.System.Name` | The name of the term or group | +| `KontentKeys.System.CodeName` | The code name of the term or group | +| `KontentKeys.System.Id` | The id of the group (not available for terms) | +| `KontentKeys.System.LastModified` | The `DateTime` the group was last modified (not available for terms) | + ## Troubleshooting > There are weird object tags like this in my content: @@ -300,12 +434,16 @@ The `KontentImageProcessor` will add any replaced image url to a collection in t ``` -Make sure you read the section on structured content and follow the configuration steps. +This means that the Kontent Delivery SDK was unable to resolve the type of content included in your rich text field. Make sure you read the section on structured content and follow the configuration steps. > Links to other pages don't work Implement and register a link resolver. See the [Kontent docs](https://github.com/Kentico/kontent-delivery-sdk-net/wiki/Resolving-Links-to-Content-Items) for more information. +> Null reference while rendering Razor + +Make sure all your classes are in a namespace. + ## How do I build this repo? You'll need a .NET Core development setup: Windows, Mac, Linux with VisualStudio, VS Code or Rider. From 7d62125d6a9ab01d463827f063085d057847908d Mon Sep 17 00:00:00 2001 From: Marnix van Valen Date: Sun, 20 Dec 2020 20:44:53 +0100 Subject: [PATCH 10/10] Code quality --- Kontent.Statiq/KontentTaxonomy.cs | 3 +-- Kontent.Statiq/TypedContentExtensions.cs | 27 ++++-------------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/Kontent.Statiq/KontentTaxonomy.cs b/Kontent.Statiq/KontentTaxonomy.cs index 2078c03..935793f 100644 --- a/Kontent.Statiq/KontentTaxonomy.cs +++ b/Kontent.Statiq/KontentTaxonomy.cs @@ -1,6 +1,5 @@ using Kentico.Kontent.Delivery.Abstractions; using Statiq.Common; -using Statiq.Core; using System; using System.Collections.Generic; using System.Linq; @@ -15,7 +14,7 @@ namespace Kontent.Statiq public sealed class KontentTaxonomy : Module { private readonly IDeliveryClient _client; - private bool _nesting = false; + private bool _nesting; private bool _collapseRoot = true; internal List QueryParameters { get; } = new List(); diff --git a/Kontent.Statiq/TypedContentExtensions.cs b/Kontent.Statiq/TypedContentExtensions.cs index afb6f65..67a271a 100644 --- a/Kontent.Statiq/TypedContentExtensions.cs +++ b/Kontent.Statiq/TypedContentExtensions.cs @@ -63,18 +63,9 @@ public static ITaxonomyGroup AsKontentTaxonomy(this IDocument document) /// Return the strong typed model for a Statiq document. ///
/// The Document. + /// The metadata key to fetch the term from. /// The taxonomy term or null. - public static ITaxonomyTerm? AsKontentTaxonomyTerm(this IDocument document) - { - return document.AsKontentTaxonomyTerm(KontentKeys.Taxonomy.Term); - } - - /// - /// Return the strong typed model for a Statiq document. - /// - /// The Document. - /// The taxonomy term or null. - public static ITaxonomyTerm? AsKontentTaxonomyTerm(this IDocument document, string key) + public static ITaxonomyTerm? AsKontentTaxonomyTerm(this IDocument document, string key = KontentKeys.Taxonomy.Term) { if (document == null) { @@ -98,19 +89,9 @@ public static ITaxonomyGroup AsKontentTaxonomy(this IDocument document) /// Return the strong typed model for a Statiq document. /// /// The Document. + /// The metadata key to get the terms from. /// The taxonomy terms or an empty array. - public static ITaxonomyTerm[] AsKontentTaxonomyTerms(this IDocument document) - { - return document.AsKontentTaxonomyTerms(KontentKeys.Taxonomy.Terms); - } - - /// - /// Return the strong typed model for a Statiq document. - /// - /// The Document. - /// - /// The taxonomy terms or an empty array. - public static ITaxonomyTerm[] AsKontentTaxonomyTerms(this IDocument document, string key) + public static ITaxonomyTerm[] AsKontentTaxonomyTerms(this IDocument document, string key = KontentKeys.Taxonomy.Terms) { if (document == null) {