Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mattb/swcd 2451 breadcrumbs #520

Merged
merged 11 commits into from
Oct 14, 2024
Merged
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
Loading