Skip to content

Commit

Permalink
Merge pull request #520 from DFE-Digital/mattb/SWCD-2451-breadcrumbs
Browse files Browse the repository at this point in the history
Mattb/swcd 2451 breadcrumbs
  • Loading branch information
mattb-hippo authored Oct 14, 2024
2 parents 0dbff12 + 60d68fc commit 6349e79
Show file tree
Hide file tree
Showing 17 changed files with 565 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
using Childrens_Social_Care_CPD;
using Childrens_Social_Care_CPD.Contentful;
using Childrens_Social_Care_CPD.Contentful.Models;
using Childrens_Social_Care_CPD.Controllers;
using Childrens_Social_Care_CPD.Models;
using Contentful.Core.Models;
using Contentful.Core.Search;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Azure;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Childrens_Social_Care_CPD_Tests.Controllers;

public class ContentControllerBreadcrumbTests
{
private ContentController _contentController;
private IRequestCookieCollection _cookies;
private HttpContext _httpContext;
private HttpRequest _httpRequest;
private ICpdContentfulClient _contentfulClient;

private void SetContent(List<KeyValuePair<string, Content>> content)
{
var contentCollections = new List<KeyValuePair<string, ContentfulCollection<Content>>>();
foreach (var contentDefinition in content)
{
var contentCollection = new ContentfulCollection<Content>();

contentCollection.Items = contentDefinition.Value == null
? new List<Content>()
: contentCollection.Items = new List<Content> { contentDefinition.Value };

contentCollections.Add(new KeyValuePair<string, ContentfulCollection<Content>>(contentDefinition.Key, contentCollection));
}

_contentfulClient
.GetEntries(Arg.Any<QueryBuilder<Content>>(), Arg.Any<CancellationToken>())
.Returns(x => {
var query = x.Arg<QueryBuilder<Content>>().Build();
foreach (var contentDefinition in content)
{
if (query.Contains("fields.id=" + contentDefinition.Key)) return contentCollections.First(x => x.Key == contentDefinition.Key).Value;
}
return new ContentfulCollection<Content>();
});

}

private void SetContent() {
var page = new Content()
{
Id = "page",
Title = "Content Page"
};

var content = new List<KeyValuePair<string, Content>>()
{
new KeyValuePair<string, Content>(page.Id, page),
};

SetContent(content);
}

[SetUp]
public void SetUp()
{
_cookies = Substitute.For<IRequestCookieCollection>();
_httpContext = Substitute.For<HttpContext>();
_httpRequest = Substitute.For<HttpRequest>();
var controllerContext = Substitute.For<ControllerContext>();

_httpRequest.Cookies.Returns(_cookies);
_httpContext.Request.Returns(_httpRequest);

controllerContext.HttpContext = _httpContext;

_contentfulClient = Substitute.For<ICpdContentfulClient>();

_contentController = new ContentController(_contentfulClient)
{
ControllerContext = controllerContext,
TempData = Substitute.For<ITempDataDictionary>()
};
}

[TearDown]
public void TearDown()
{
_contentfulClient = Substitute.For<ICpdContentfulClient>();
}

[Test]
public async Task Index_Sets_Breadcrumbs_In_ContextModel()
{
// arrange
var parentPage = new Content()
{
Id = "parent",
Title = "Parent Page"
};
var childPage = new Content(){
Id = "child",
Title = "Child Page",
ParentPages = new List<Content>(){parentPage}
};

var content = new List<KeyValuePair<string, Content>>(){
new KeyValuePair<string, Content>(parentPage.Id, parentPage),
new KeyValuePair<string, Content>(childPage.Id, childPage)
};

SetContent(content);

// act
await _contentController.Index("child");
var actual = _contentController.ViewData["ContextModel"] as ContextModel;
var breadcrumbTrail = actual?.BreadcrumbTrail;

// assert
actual.Should().NotBeNull();
breadcrumbTrail[0].Key.Should().Be("Child Page");
breadcrumbTrail[0].Value.Should().Be("child");
breadcrumbTrail[1].Key.Should().Be("Parent Page");
breadcrumbTrail[1].Value.Should().Be("parent");
}

[Test]
public async Task Index_Sets_Blank_Breadcrumbs_In_ContextModel_If_Page_Has_No_Parent()
{
// arrange
SetContent();

// act
await _contentController.Index("page");
var actual = _contentController.ViewData["ContextModel"] as ContextModel;
var breadcrumbTrail = actual?.BreadcrumbTrail;

// assert
actual.Should().NotBeNull();
breadcrumbTrail.Should().BeEmpty();
}

[Test]
public async Task Index_Sets_Breadcrumbs_Where_Page_Has_Multiple_Parents()
{
// arrange
var parentPage1 = new Content()
{
Id = "parent1",
Title = "First Parent Page"
};
var parentPage2 = new Content()
{
Id = "parent2",
Title = "Second Parent Page"
};
var childPage = new Content(){
Id = "child",
Title = "Child Page",
ParentPages = new List<Content>(){parentPage1, parentPage2}
};

var content = new List<KeyValuePair<string, Content>>(){
new KeyValuePair<string, Content>(parentPage1.Id, parentPage1),
new KeyValuePair<string, Content>(parentPage2.Id, parentPage2),
new KeyValuePair<string, Content>(childPage.Id, childPage)
};

SetContent(content);

// act
await _contentController.Index("child");
var actual = _contentController.ViewData["ContextModel"] as ContextModel;
var breadcrumbTrail = actual?.BreadcrumbTrail;

// assert
actual.Should().NotBeNull();
breadcrumbTrail[0].Key.Should().Be("Child Page");
breadcrumbTrail[0].Value.Should().Be("child");
breadcrumbTrail[1].Key.Should().Be("First Parent Page");
breadcrumbTrail[1].Value.Should().Be("parent1");
}
}
2 changes: 2 additions & 0 deletions Childrens-Social-Care-CPD/Contentful/Models/Content.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class Content : IContent
public NavigationMenu Navigation { get; set; }
public RelatedContent RelatedContent { get; set; }
public int? EstimatedReadingTime { get; set; }
public List<Content> ParentPages { get; set; }
public string BreadcrumbText { get; set; }

[JsonProperty("$metadata")]
public ContentfulMetadata Metadata { get; set; }
Expand Down
61 changes: 60 additions & 1 deletion Childrens-Social-Care-CPD/Controllers/ContentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using Childrens_Social_Care_CPD.Models;
using Contentful.Core.Search;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;

namespace Childrens_Social_Care_CPD.Controllers;

Expand All @@ -20,6 +22,57 @@ private async Task<Content> FetchPageContentAsync(string contentId, Cancellation
return result?.FirstOrDefault();
}

private async Task<List<KeyValuePair<string, string>>> BuildBreadcrumbTrail(
List<KeyValuePair<string, string>> trail,
Content page,
List<string> pagesVisited,
CancellationToken ct)
{
var trailItem = new KeyValuePair<string, string>(
page.BreadcrumbText.IsNullOrEmpty() ?
page.Title :
page.BreadcrumbText,
page.Id);

if (page.ParentPages == null || page.ParentPages.Count == 0) {
if (trail.Count > 0) trail.Add(trailItem);
return trail;
}

trail.Add(trailItem);

Content parentPage = new Content();

if (page.ParentPages?.Count == 1) {
parentPage = page.ParentPages[0];
}
else
{
var parentPageIds = page.ParentPages
.Select(parent => parent.Id)
.ToList();

var checkPages = pagesVisited.Reverse<string>();
bool parentFound = false;

foreach (var pageId in checkPages)
{
if (parentPageIds.Contains(pageId))
{
parentPage = page.ParentPages.First(p => p.Id == pageId);
parentFound = true;
break;
}
};

// if we don't find a parent page in the recently vistied pages, just use the first in the list
if (!parentFound) parentPage = page.ParentPages[0];
}

var parentObject = await FetchPageContentAsync(parentPage.Id, ct);
return await BuildBreadcrumbTrail(trail, parentObject, pagesVisited, ct);
}

[HttpGet]
[Route("/")]
/*
Expand Down Expand Up @@ -49,6 +102,11 @@ public async Task<IActionResult> Index(string pageName = "home", bool preference
return NotFound();
}

var pagesVisited = HttpContext.Session.Get<List<string>>("pagesVisited");
if (pagesVisited == null) pagesVisited = new List<string>();
pagesVisited.Add(pageName);
HttpContext.Session.Set("pagesVisited", pagesVisited);

var contextModel = new ContextModel(
Id: content.Id,
Title: content.Title,
Expand All @@ -57,7 +115,8 @@ public async Task<IActionResult> Index(string pageName = "home", bool preference
UseContainers: content.Navigation == null,
PreferenceSet: preferenceSet,
BackLink: content.BackLink,
FeedbackSubmitted: fs);
FeedbackSubmitted: fs,
BreadcrumbTrail: await BuildBreadcrumbTrail(new List<KeyValuePair<string, string>>(), content, pagesVisited, cancellationToken));

ViewData["ContextModel"] = contextModel;
ViewData["StateModel"] = new StateModel();
Expand Down
19 changes: 19 additions & 0 deletions Childrens-Social-Care-CPD/Extensions/SessionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Newtonsoft.Json;

#nullable enable

namespace Childrens_Social_Care_CPD;

public static class SessionExtensions
{
public static void Set<T>(this ISession session, string key, T value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}

public static T? Get<T>(this ISession session, string key)
{
var value = session.GetString(key);
return value == null ? default : JsonConvert.DeserializeObject<T>(value);
}
}
12 changes: 11 additions & 1 deletion Childrens-Social-Care-CPD/Models/ContextModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

namespace Childrens_Social_Care_CPD.Models;

public record ContextModel(string Id, string Title, string PageName, string Category, bool UseContainers, bool PreferenceSet, bool HideConsent = false, ContentLink BackLink = null, bool FeedbackSubmitted = false)
public record ContextModel(
string Id,
string Title,
string PageName,
string Category,
bool UseContainers,
bool PreferenceSet,
bool HideConsent = false,
ContentLink BackLink = null,
bool FeedbackSubmitted = false,
List<KeyValuePair<string, string>> BreadcrumbTrail = null)
{
public Stack<string> ContentStack { get; } = new Stack<string>();
}
9 changes: 9 additions & 0 deletions Childrens-Social-Care-CPD/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
builder.AddFeatures(sw);
Console.WriteLine($"After AddFeatures {sw.ElapsedMilliseconds}ms");

builder.Services.AddDistributedMemoryCache();
Console.WriteLine($"After AddDistributedMemoryCache {sw.ElapsedMilliseconds}ms");

builder.Services.AddSession();
Console.WriteLine($"After AddSession {sw.ElapsedMilliseconds}ms");

var app = builder.Build();
Console.WriteLine($"After Application Build {sw.ElapsedMilliseconds}ms");

Expand Down Expand Up @@ -43,6 +49,9 @@
app.UseAuthorization();
Console.WriteLine($"After UseAuthorization {sw.ElapsedMilliseconds}ms");

app.UseSession();
Console.WriteLine($"After UseSession {sw.ElapsedMilliseconds}ms");

app.MapControllerRoute(
name: "default",
pattern: "{controller=Content}/{action=Index}");
Expand Down
20 changes: 20 additions & 0 deletions Childrens-Social-Care-CPD/Views/Shared/_BreadcrumbTrail.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@using Childrens_Social_Care_CPD.Contentful.Models;

@{
var contextModel = (ContextModel)ViewData["ContextModel"];
var breadcrumbs = contextModel.BreadcrumbTrail;
breadcrumbs.Reverse();
}

<div class="govuk-breadcrumbs govuk-breadcrumbs--collapse-on-mobile">
<ul>
@foreach(KeyValuePair<string, string> trailItem in breadcrumbs)
{
<li class="govuk-breadcrumbs__list-item">
<a class="govuk-breadcrumbs__link" href="@trailItem.Value">
@trailItem.Key
</a>
</li>
}
</ul>
</div>
3 changes: 3 additions & 0 deletions Childrens-Social-Care-CPD/Views/Shared/_PageBanner.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<section id="content-banner" class="app-section-content app-section-content--blue govuk-!-margin-bottom-8">
<div class="dfe-width-container">
<div class="govuk-grid-row govuk-!-padding-top-3 govuk-!-padding-bottom-0">
<div class="govuk-grid-column-two-thirds-from-desktop banner-breadcrumbs">
<partial name="_BreadcrumbTrail" />
</div>
<div class="govuk-grid-column-two-thirds-from-desktop govuk-!-padding-top-3">
<h1 id="content-banner-title" class="govuk-heading-xl">@Model.ContentTitle</h1>
<p id="content-banner-subtitle" class="govuk-body-l">@Model.ContentSubtitle</p>
Expand Down
Loading

0 comments on commit 6349e79

Please sign in to comment.