diff --git a/src/Dfe.PlanTech.Application/Content/Queries/GetPageFromDbQuery.cs b/src/Dfe.PlanTech.Application/Content/Queries/GetPageFromDbQuery.cs index d73dd72ed..fa080876a 100644 --- a/src/Dfe.PlanTech.Application/Content/Queries/GetPageFromDbQuery.cs +++ b/src/Dfe.PlanTech.Application/Content/Queries/GetPageFromDbQuery.cs @@ -46,24 +46,55 @@ public GetPageFromDbQuery(ICmsDbContext db, ILogger logger, private async Task RetrievePageFromDatabase(string slug, CancellationToken cancellationToken) { - var page = await _db.GetPageBySlug(slug, cancellationToken); + var page = await GetPageFromDb(slug, cancellationToken); if (!IsValidPage(page, slug)) { - _logger.LogError("Retrieved page {slug} is invalid", slug); return null; } await LoadPageChildrenFromDatabase(page, cancellationToken); + _logger.LogTrace("Successfully retrieved {page} from DB", slug); + return page; } + private async Task GetPageFromDb(string slug, CancellationToken cancellationToken) + { + try + { + var page = await _db.GetPageBySlug(slug, cancellationToken); + + if (page == null) + { + return null; + } + + page.OrderContents(); + + return page; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching {page} from database", slug); + throw; + } + } + private async Task LoadPageChildrenFromDatabase(PageDbEntity? page, CancellationToken cancellationToken) { - foreach (var query in _getPageChildrenQueries) + try + { + foreach (var query in _getPageChildrenQueries) + { + await query.TryLoadChildren(page!, cancellationToken); + } + } + catch (Exception ex) { - await query.TryLoadChildren(page!, cancellationToken); + _logger.LogError(ex, "Error loading children from database for {page}", page!.Id); + throw; } } diff --git a/src/Dfe.PlanTech.Application/Content/Queries/GetPageQuery.cs b/src/Dfe.PlanTech.Application/Content/Queries/GetPageQuery.cs index a36b99cd3..ed5a69266 100644 --- a/src/Dfe.PlanTech.Application/Content/Queries/GetPageQuery.cs +++ b/src/Dfe.PlanTech.Application/Content/Queries/GetPageQuery.cs @@ -21,7 +21,8 @@ public GetPageQuery(GetPageFromContentfulQuery getPageFromContentfulQuery, GetPa /// Page matching slug public async Task GetPageBySlug(string slug, CancellationToken cancellationToken = default) { - var page = await _getPageFromDbQuery.GetPageBySlug(slug, cancellationToken) ?? await _getPageFromContentfulQuery.GetPageBySlug(slug, cancellationToken); + var page = await _getPageFromDbQuery.GetPageBySlug(slug, cancellationToken) ?? + await _getPageFromContentfulQuery.GetPageBySlug(slug, cancellationToken); return page; } diff --git a/src/Dfe.PlanTech.AzureFunctions/Mappings/PageMapper.cs b/src/Dfe.PlanTech.AzureFunctions/Mappings/PageMapper.cs index 711036303..2f5d41df5 100644 --- a/src/Dfe.PlanTech.AzureFunctions/Mappings/PageMapper.cs +++ b/src/Dfe.PlanTech.AzureFunctions/Mappings/PageMapper.cs @@ -16,6 +16,12 @@ public PageMapper(CmsDbContext db, ILogger logger, JsonSerializerOpt _db = db; } + /// + /// Create joins for content, and before title content, and map the title ID to the correct expected name. + /// + /// + /// + /// public override Dictionary PerformAdditionalMapping(Dictionary values) { var id = values["id"]?.ToString() ?? throw new KeyNotFoundException("Not found id"); @@ -30,18 +36,27 @@ public PageMapper(CmsDbContext db, ILogger logger, JsonSerializerOpt private void UpdateContentIds(Dictionary values, string pageId, string currentKey) { + bool isBeforeTitleContent = currentKey == BeforeTitleContentKey; + if (values.TryGetValue(currentKey, out object? contents) && contents is object[] inners) { - foreach (var inner in inners) + for (var index = 0; index < inners.Length; index++) { - CreatePageContentEntity(inner, pageId, currentKey == BeforeTitleContentKey); + CreatePageContentEntity(inners[index], index, pageId, isBeforeTitleContent); } values.Remove(currentKey); } } - private void CreatePageContentEntity(object inner, string pageId, bool isBeforeTitleContent) + /// + /// Creates the necessary for the relationship + /// + /// The child content ID. + /// Order of the content for the page + /// + /// + private void CreatePageContentEntity(object inner, int order, string pageId, bool isBeforeTitleContent) { if (inner is not string contentId) { @@ -52,6 +67,7 @@ private void CreatePageContentEntity(object inner, string pageId, bool isBeforeT var pageContent = new PageContentDbEntity() { PageId = pageId, + Order = order }; if (isBeforeTitleContent) diff --git a/src/Dfe.PlanTech.Domain/Content/Models/ContentComponentDbEntity.cs b/src/Dfe.PlanTech.Domain/Content/Models/ContentComponentDbEntity.cs index 894839f0b..31ce7f5ac 100644 --- a/src/Dfe.PlanTech.Domain/Content/Models/ContentComponentDbEntity.cs +++ b/src/Dfe.PlanTech.Domain/Content/Models/ContentComponentDbEntity.cs @@ -17,5 +17,16 @@ public abstract class ContentComponentDbEntity : IContentComponentDbEntity public List BeforeTitleContentPages { get; set; } = []; + /// + /// Joins for + /// + public List BeforeTitleContentPagesJoins { get; set; } = []; + public List ContentPages { get; set; } = []; + + /// + /// Joins for + /// + public List ContentPagesJoins { get; set; } = []; + } \ No newline at end of file diff --git a/src/Dfe.PlanTech.Domain/Content/Models/Page.cs b/src/Dfe.PlanTech.Domain/Content/Models/Page.cs index 55f5f5c8a..84df09075 100644 --- a/src/Dfe.PlanTech.Domain/Content/Models/Page.cs +++ b/src/Dfe.PlanTech.Domain/Content/Models/Page.cs @@ -20,11 +20,11 @@ public class Page : ContentComponent, IPageContent public string? SectionTitle { get; set; } - public List BeforeTitleContent { get; init; } = new(); + public List BeforeTitleContent { get; init; } = []; public Title? Title { get; init; } public string? OrganisationName { get; set; } - public List Content { get; init; } = new(); + public List Content { get; init; } = []; } diff --git a/src/Dfe.PlanTech.Domain/Content/Models/PageContentDbEntity.cs b/src/Dfe.PlanTech.Domain/Content/Models/PageContentDbEntity.cs index 331116911..4c42f7f78 100644 --- a/src/Dfe.PlanTech.Domain/Content/Models/PageContentDbEntity.cs +++ b/src/Dfe.PlanTech.Domain/Content/Models/PageContentDbEntity.cs @@ -17,4 +17,9 @@ public class PageContentDbEntity public string? BeforeContentComponentId { get; set; } public ContentComponentDbEntity? BeforeContentComponent { get; set; } + + /// + /// What order the component should be in in its respective section (e.g. before/after) + /// + public int Order { get; set; } } diff --git a/src/Dfe.PlanTech.Domain/Content/Models/PageDbEntity.cs b/src/Dfe.PlanTech.Domain/Content/Models/PageDbEntity.cs index 2b56666ad..3dd9d6fdf 100644 --- a/src/Dfe.PlanTech.Domain/Content/Models/PageDbEntity.cs +++ b/src/Dfe.PlanTech.Domain/Content/Models/PageDbEntity.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations.Schema; using Dfe.PlanTech.Domain.Content.Interfaces; using Dfe.PlanTech.Domain.Questionnaire.Models; @@ -19,16 +20,38 @@ public class PageDbEntity : ContentComponentDbEntity, IPage BeforeTitleContent { get; set; } = new(); + public List BeforeTitleContent { get; set; } = []; public TitleDbEntity? Title { get; set; } public string? TitleId { get; set; } - public List Content { get; set; } = new(); + public List Content { get; set; } = []; public RecommendationPageDbEntity? RecommendationPage { get; set; } public SectionDbEntity? Section { get; set; } + /// + /// Combined joins for and + /// + public List AllPageContents { get; set; } = []; + + public void OrderContents() + { + BeforeTitleContent = OrderContents(BeforeTitleContent, pageContent => pageContent.BeforeContentComponentId).ToList(); + Content = OrderContents(Content, pageContent => pageContent.ContentComponentId).ToList(); + } + + private IEnumerable OrderContents(List contents, Func idSelector) + => contents.Join(AllPageContents, + content => content.Id, + idSelector, + (content, pageContent) => new + { + content, + order = pageContent.Order + }) + .OrderBy(content => content.order) + .Select(content => content.content); } \ No newline at end of file diff --git a/src/Dfe.PlanTech.Infrastructure.Data/CmsDbContext.cs b/src/Dfe.PlanTech.Infrastructure.Data/CmsDbContext.cs index 752402952..def379ab7 100644 --- a/src/Dfe.PlanTech.Infrastructure.Data/CmsDbContext.cs +++ b/src/Dfe.PlanTech.Infrastructure.Data/CmsDbContext.cs @@ -138,11 +138,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(entity => { entity.HasMany(page => page.BeforeTitleContent) - .WithMany(c => c.BeforeTitleContentPages) - .UsingEntity( - left => left.HasOne(pageContent => pageContent.BeforeContentComponent).WithMany().HasForeignKey("BeforeContentComponentId").OnDelete(DeleteBehavior.Restrict), - right => right.HasOne(pageContent => pageContent.Page).WithMany().HasForeignKey("PageId").OnDelete(DeleteBehavior.Restrict) - ); + .WithMany(c => c.BeforeTitleContentPages) + .UsingEntity( + left => left.HasOne(pageContent => pageContent.BeforeContentComponent).WithMany().HasForeignKey("BeforeContentComponentId").OnDelete(DeleteBehavior.Restrict), + right => right.HasOne(pageContent => pageContent.Page).WithMany().HasForeignKey("PageId").OnDelete(DeleteBehavior.Restrict) + ); entity.HasMany(page => page.Content) .WithMany(c => c.ContentPages) @@ -156,6 +156,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.ToTable("Pages", Schema); }); + modelBuilder.Entity(entity => + { + entity.HasOne(pc => pc.BeforeContentComponent).WithMany(c => c.BeforeTitleContentPagesJoins); + + entity.HasOne(pc => pc.ContentComponent).WithMany(c => c.ContentPagesJoins); + + entity.HasOne(pc => pc.Page).WithMany(p => p.AllPageContents); + }); + modelBuilder.Entity().ToTable("Questions", Schema); modelBuilder.Entity(entity => @@ -218,14 +227,12 @@ private Expression> ShouldShowEntity() public Task GetPageBySlug(string slug, CancellationToken cancellationToken = default) => Pages.Include(page => page.BeforeTitleContent) - .Include(page => page.Content) - .Include(page => page.Title) - .AsSplitQuery() - .FirstOrDefaultAsync(page => page.Slug == slug, cancellationToken); + .Include(page => page.Content) + .Include(page => page.Title) + .AsSplitQuery() + .FirstOrDefaultAsync(page => page.Slug == slug, cancellationToken); - public Task> ToListAsync(IQueryable queryable, CancellationToken cancellationToken = default) -=> queryable.ToListAsync(cancellationToken: cancellationToken); + public Task> ToListAsync(IQueryable queryable, CancellationToken cancellationToken = default) => queryable.ToListAsync(cancellationToken: cancellationToken); - public Task FirstOrDefaultAsync(IQueryable queryable, CancellationToken cancellationToken = default) - => queryable.FirstOrDefaultAsync(cancellationToken); + public Task FirstOrDefaultAsync(IQueryable queryable, CancellationToken cancellationToken = default) => queryable.FirstOrDefaultAsync(cancellationToken); } \ No newline at end of file diff --git a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageFromContentfulQueryTests.cs b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageFromContentfulQueryTests.cs index aad43c2f7..9da07945f 100644 --- a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageFromContentfulQueryTests.cs +++ b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageFromContentfulQueryTests.cs @@ -14,10 +14,7 @@ public class GetPageFromContentfulQueryTests { private const string TEST_PAGE_SLUG = "test-page-slug"; private const string SECTION_SLUG = "SectionSlugTest"; - private const string SECTION_TITLE = "SectionTitleTest"; private const string LANDING_PAGE_SLUG = "LandingPage"; - private const string BUTTON_REF_SLUG = "ButtonReferences"; - private const string CATEGORY_ID = "category-one"; private readonly IContentRepository _repoSubstitute = Substitute.For(); private readonly ILogger _logger = Substitute.For>(); @@ -70,7 +67,7 @@ private void SetupRepository() } } - return Array.Empty(); + return []; }); } diff --git a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageFromDbQueryTests.cs b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageFromDbQueryTests.cs index 85128874b..5aeadbca7 100644 --- a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageFromDbQueryTests.cs +++ b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageFromDbQueryTests.cs @@ -15,45 +15,24 @@ public class GetPageFromDbQueryTests { private const string TEST_PAGE_SLUG = "test-page-slug"; private const string SECTION_SLUG = "SectionSlugTest"; - private const string SECTION_TITLE = "SectionTitleTest"; private const string LANDING_PAGE_SLUG = "LandingPage"; private const string BUTTON_REF_SLUG = "ButtonReferences"; private const string CATEGORY_ID = "category-one"; + private const string LANDING_PAGE_ID = "LANDING_PAGE_ID"; + private const string HEADER_ID = "Header-Id"; private readonly ICmsDbContext _cmsDbSubstitute = Substitute.For(); private readonly ILogger _logger = Substitute.For>(); private readonly IMapper _mapperSubstitute = Substitute.For(); private readonly GetPageFromDbQuery _getPageFromDbQuery; - private readonly List _pages = new() { - new Page(){ - Slug = "Index" - }, - new Page(){ - Slug = LANDING_PAGE_SLUG, - }, - new Page(){ - Slug = "AuditStart" - }, - new Page(){ - Slug = SECTION_SLUG, - DisplayTopicTitle = true, - DisplayHomeButton= false, - DisplayBackButton = false, - }, - new Page(){ - Slug = TEST_PAGE_SLUG, - Sys = new() { - Id = "test-page-id" - } - }, - }; - - private readonly List _pagesFromDb = new() { + private readonly List _pagesFromDb = [ new PageDbEntity(){ Slug = "Index", - Content = new(){ - new HeaderDbEntity() - } + Content = [ + new HeaderDbEntity(){ + Id = "Header-Id" + } + ] }, new PageDbEntity(){ Slug = TEST_PAGE_SLUG, @@ -61,9 +40,22 @@ public class GetPageFromDbQueryTests }, new PageDbEntity(){ Slug = LANDING_PAGE_SLUG, - Content = new(){ - new HeaderDbEntity(), - } + Id = LANDING_PAGE_ID, + BeforeTitleContent = [ + + ], + Content = [ + new HeaderDbEntity(){ + Id = HEADER_ID + } + ], + AllPageContents = [ + new PageContentDbEntity(){ + Id = 1, + PageId = LANDING_PAGE_ID, + ContentComponentId = HEADER_ID + } + ] }, new PageDbEntity(){ Slug = "AuditStart", @@ -78,36 +70,28 @@ public class GetPageFromDbQueryTests _pageWithButton, _pageWithCategories, _pageWithRichTextContent, - }; + ]; private readonly static PageDbEntity _pageWithCategories = new() { Slug = "categories_page", Id = CATEGORY_ID, - Content = new() - { - } + Content = [] }; private readonly static CategoryDbEntity _category = new() { Id = CATEGORY_ID, - ContentPages = new() - { - - }, - Sections = new() - { - - } + ContentPages = [], + Sections = [] }; - private readonly static List _sections = new(){ + private readonly static List _sections = [ new SectionDbEntity(){ Name = "sSection one", CategoryId = CATEGORY_ID, Order = 0, - Recommendations = new(){ + Recommendations = [ new RecommendationPageDbEntity(){ DisplayName = "Recommendation one", Maturity = Maturity.High, @@ -122,8 +106,8 @@ public class GetPageFromDbQueryTests Slug = "recommendation-medium" } } - }, - Questions = new(){ + ], + Questions = [ new QuestionDbEntity(){ Order = 0, Slug = "question-one-slug", @@ -132,35 +116,35 @@ public class GetPageFromDbQueryTests Order = 1, Slug = "question-two-slug" } - } + ] }, - }; + ]; private readonly static PageDbEntity _pageWithButton = new() { Slug = BUTTON_REF_SLUG, - Content = new(){ + Content = [ new ButtonWithEntryReferenceDbEntity(){ Button = new ButtonDbEntity(){ Value = "Button value", IsStartButton = true } } - } + ] }; private static readonly PageDbEntity _pageWithRichTextContent = new() { Slug = "rich_text_content", - Content = new() - { + Content = + [ new TextBodyDbEntity(){ RichTextId = 1 }, new TextBodyDbEntity(){ RichTextId = 2 }, - } + ] }; private readonly IGetPageChildrenQuery _getPageChildrenQuery = Substitute.For(); @@ -237,9 +221,9 @@ public GetPageFromDbQueryTests() }); } - _category.ContentPages = new(){ + _category.ContentPages = [ _pageWithCategories - }; + ]; _pageWithCategories.Content.Add(_category); diff --git a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageQueryTests.cs b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageQueryTests.cs index 2b6ee3865..382b2aeec 100644 --- a/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageQueryTests.cs +++ b/tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetPageQueryTests.cs @@ -30,12 +30,20 @@ public class GetPageQueryTests private readonly PageDbEntity _pageDbEntity = new() { + Id = "PageId", Slug = DbPageSlug, - Content = new(){ + Content = [ new QuestionDbEntity(){ - + Id="QuestionId" + } + ], + AllPageContents = [ + new PageContentDbEntity(){ + Id = 1, + PageId = "PageId", + ContentComponentId = "QuestionId" } - } + ] }; public GetPageQueryTests() diff --git a/tests/Dfe.PlanTech.AzureFunctions.UnitTests/Mappers/PageMapperTests.cs b/tests/Dfe.PlanTech.AzureFunctions.UnitTests/Mappers/PageMapperTests.cs index 0bd08b36a..1321c769c 100644 --- a/tests/Dfe.PlanTech.AzureFunctions.UnitTests/Mappers/PageMapperTests.cs +++ b/tests/Dfe.PlanTech.AzureFunctions.UnitTests/Mappers/PageMapperTests.cs @@ -34,7 +34,6 @@ public PageMapperTests() var pageContent = callinfo.ArgAt(0); _attachedPageContents.Add(pageContent); }); - } [Fact] @@ -60,16 +59,18 @@ public void Mapper_Should_Map_Page() Assert.Equal(PageId, concrete.Id); - foreach (var beforeTitle in beforeTitleContent) - { - var matching = _attachedPageContents.FirstOrDefault(pageContent => pageContent.BeforeContentComponentId == beforeTitle.Sys.Id); - Assert.NotNull(matching); - } + ContentsExistAndAreCorrect(beforeTitleContent, content => content.BeforeContentComponentId); + ContentsExistAndAreCorrect(content, content => content.ContentComponentId); + } - foreach (var afterTitleContent in content) + private void ContentsExistAndAreCorrect(CmsWebHookSystemDetailsInnerContainer[] content, Func idSelector) + { + for (var index = 0; index < content.Length; index++) { - var matching = _attachedPageContents.FirstOrDefault(pageContent => pageContent.ContentComponentId == afterTitleContent.Sys.Id); + var beforeTitle = content[index]; + var matching = _attachedPageContents.FirstOrDefault(pc => idSelector(pc) == beforeTitle.Sys.Id); Assert.NotNull(matching); + Assert.Equal(index, matching.Order); } }