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

feat: add contact link to concurrent user error message #891

Merged
merged 24 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3968558
add contact link to error message
jack-coggin Nov 20, 2024
540ad61
Merge branch 'development' into feat/236954/add-contact-link-error-me…
jack-coggin Nov 20, 2024
cb0a481
chore: Linted code for plan-technology-for-your-school.sln solution
github-actions[bot] Nov 20, 2024
291d349
add private method
jack-coggin Nov 20, 2024
7ab8ad8
add class for config error mesage
jack-coggin Nov 21, 2024
2ea1f2f
add class for config contact information
jack-coggin Nov 21, 2024
030c714
chore: Linted code for plan-technology-for-your-school.sln solution
github-actions[bot] Nov 21, 2024
bc6fd63
add missing namespace
jack-coggin Nov 21, 2024
ad84ca5
chore: Linted code for plan-technology-for-your-school.sln solution
github-actions[bot] Nov 21, 2024
fa6bea9
fix sonarcloud issues
jack-coggin Nov 21, 2024
01dadf5
Merge branch 'development' into feat/236954/add-contact-link-error-me…
jack-coggin Nov 21, 2024
6eca68c
remove unused configuration arg
jack-coggin Nov 21, 2024
39442ca
add method to fetch nav links by id
jack-coggin Nov 21, 2024
b45d978
chore: Linted code for plan-technology-for-your-school.sln solution
github-actions[bot] Nov 21, 2024
da65749
wip add unit tests for navigation query
jack-coggin Nov 22, 2024
bd7584a
chore: Linted code for plan-technology-for-your-school.sln solution
github-actions[bot] Nov 22, 2024
c15d35c
fix unit tests
jack-coggin Nov 22, 2024
ac84344
chore: Linted code for plan-technology-for-your-school.sln solution
github-actions[bot] Nov 22, 2024
275b989
update unit tests
jack-coggin Nov 22, 2024
dfbf54c
Update src/Dfe.PlanTech.Web/Controllers/PagesController.cs
jack-coggin Nov 26, 2024
2a2828c
Update src/Dfe.PlanTech.Domain/Content/Interfaces/IGetNavigationQuery.cs
jack-coggin Nov 26, 2024
cc656ae
remove contact email from app settings
jack-coggin Nov 26, 2024
54c9c1a
update config options to records
jack-coggin Nov 26, 2024
cd0a6ae
Merge branch 'development' into feat/236954/add-contact-link-error-me…
jack-coggin Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Dfe.PlanTech.Application/Content/Queries/GetNavigationQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,17 @@ public async Task<IEnumerable<INavigationLink>> GetNavigationLinks(CancellationT
return [];
}
}

public async Task<INavigationLink?> GetLinkById(string contentId, CancellationToken cancellationToken = default)
{
try
{
return await _cache.GetOrCreateAsync($"NavigationLink:{contentId}", () => repository.GetEntityById<NavigationLink?>(contentId, cancellationToken: cancellationToken));
}
catch (Exception ex)
{
_logger.LogError(ex, ExceptionMessageContentful);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ public interface IGetNavigationQuery
/// </summary>
/// <returns>Found navigation links</returns>
Task<IEnumerable<INavigationLink>> GetNavigationLinks(CancellationToken cancellationToken = default);

/// <summary>
/// Retrieve link by id
/// </summary>
/// <returns>Found navigation link</returns>
Task<INavigationLink?> GetLinkById(string contentId, CancellationToken cancellationToken = default);
}
6 changes: 6 additions & 0 deletions src/Dfe.PlanTech.Web/Configuration/ContactOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Dfe.PlanTech.Web.Configuration;

public record ContactOptions
{
public string LinkId { get; set; } = "";
}
6 changes: 6 additions & 0 deletions src/Dfe.PlanTech.Web/Configuration/ErrorMessages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Dfe.PlanTech.Web.Configuration;

public record ErrorMessages
{
public string ConcurrentUsersOrContentChange { get; set; } = "";
}
22 changes: 14 additions & 8 deletions src/Dfe.PlanTech.Web/Controllers/PagesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@
using Dfe.PlanTech.Domain.Users.Interfaces;
using Dfe.PlanTech.Web.Authorisation;
using Dfe.PlanTech.Web.Binders;
using Dfe.PlanTech.Web.Configuration;
using Dfe.PlanTech.Web.Helpers;
using Dfe.PlanTech.Web.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace Dfe.PlanTech.Web.Controllers;

[LogInvalidModelState]
[Route("/")]
public class PagesController(ILogger<PagesController> logger, IGetEntityFromContentfulQuery getEntityByIdQuery) : BaseController<PagesController>(logger)
public class PagesController(ILogger<PagesController> logger, IGetNavigationQuery getNavigationQuery, IOptions<ContactOptions> contactOptions) : BaseController<PagesController>(logger)
{
private readonly ILogger _logger = logger;
private readonly IGetEntityFromContentfulQuery _getEntityFromContentfulQuery = getEntityByIdQuery;
private readonly IGetNavigationQuery _getNavigationQuery = getNavigationQuery;
private readonly ContactOptions _contactOptions = contactOptions.Value;
public const string ControllerName = "Pages";
public const string GetPageByRouteAction = nameof(GetByRoute);
public const string NotFoundPage = "NotFoundError";
Expand All @@ -42,9 +45,9 @@
=> View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });

[HttpGet(UrlConstants.ServiceUnavailable, Name = UrlConstants.ServiceUnavailable)]
public async Task<IActionResult> ServiceUnavailable([FromServices] IConfiguration configuration)
public async Task<IActionResult> ServiceUnavailable()
{
var contactLink = await _getEntityFromContentfulQuery.GetEntityById<NavigationLink>(configuration["ContactUs:LinkId"]);
var contactLink = await GetContactLinkAsync();

var viewModel = new ServiceUnavailableViewModel
{
Expand All @@ -55,11 +58,9 @@
}

[HttpGet(UrlConstants.NotFound, Name = UrlConstants.NotFound)]
public async Task<IActionResult> NotFoundError([FromServices] IConfiguration configuration)
public async Task<IActionResult> NotFoundError()
{
var contentId = configuration["ContactUs:LinkId"];
var contactLink = await _getEntityFromContentfulQuery.GetEntityById<NavigationLink>(contentId) ??
throw new KeyNotFoundException($"Could not find navigation link with Id {contentId}");
var contactLink = await GetContactLinkAsync();

var viewModel = new NotFoundViewModel
jack-coggin marked this conversation as resolved.
Show resolved Hide resolved
{
Expand All @@ -68,4 +69,9 @@

return View(viewModel);
}

private async Task<INavigationLink> GetContactLinkAsync()
{
return await _getNavigationQuery.GetLinkById(_contactOptions.LinkId);

Check warning on line 75 in src/Dfe.PlanTech.Web/Controllers/PagesController.cs

View workflow job for this annotation

GitHub Actions / Build and run unit tests

Possible null reference return.
jack-coggin marked this conversation as resolved.
Show resolved Hide resolved
}
}
30 changes: 27 additions & 3 deletions src/Dfe.PlanTech.Web/Controllers/QuestionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
using Dfe.PlanTech.Domain.Questionnaire.Models;
using Dfe.PlanTech.Domain.Submissions.Interfaces;
using Dfe.PlanTech.Domain.Users.Interfaces;
using Dfe.PlanTech.Web.Configuration;
using Dfe.PlanTech.Web.Helpers;
using Dfe.PlanTech.Web.Models;
using Dfe.PlanTech.Web.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace Dfe.PlanTech.Web.Controllers;

Expand All @@ -23,18 +25,27 @@ public class QuestionsController : BaseController<QuestionsController>
private readonly IGetSectionQuery _getSectionQuery;
private readonly IGetLatestResponsesQuery _getResponseQuery;
private readonly IGetEntityFromContentfulQuery _getEntityFromContentfulQuery;
private readonly IGetNavigationQuery _getNavigationQuery;
private readonly IUser _user;
private readonly ErrorMessages _errorMessages;
private readonly ContactOptions _contactOptions;

public QuestionsController(ILogger<QuestionsController> logger,
IGetSectionQuery getSectionQuery,
IGetLatestResponsesQuery getResponseQuery,
IGetEntityFromContentfulQuery getEntityByIdQuery,
IUser user) : base(logger)
IGetNavigationQuery getNavigationQuery,
IUser user,
IOptions<ErrorMessages> errorMessageOptions,
IOptions<ContactOptions> contactOptions) : base(logger)
{
_getResponseQuery = getResponseQuery;
_getSectionQuery = getSectionQuery;
_getEntityFromContentfulQuery = getEntityByIdQuery;
_getNavigationQuery = getNavigationQuery;
_user = user;
_errorMessages = errorMessageOptions.Value;
_contactOptions = contactOptions.Value;
}

[LogInvalidModelState]
Expand Down Expand Up @@ -74,7 +85,6 @@ public async Task<IActionResult> GetQuestionPreviewById(string questionId,
public async Task<IActionResult> GetNextUnansweredQuestion(string sectionSlug,
[FromServices] IGetNextUnansweredQuestionQuery getQuestionQuery,
[FromServices] IDeleteCurrentSubmissionCommand deleteCurrentSubmissionCommand,
[FromServices] IConfiguration configuration,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(sectionSlug))
Expand All @@ -97,14 +107,28 @@ public async Task<IActionResult> GetNextUnansweredQuestion(string sectionSlug,
{
// Remove the current invalid submission and redirect to self-assessment page
await deleteCurrentSubmissionCommand.DeleteCurrentSubmission(section, cancellationToken);
TempData["SubtopicError"] = configuration["ErrorMessages:ConcurrentUsersOrContentChange"];

TempData["SubtopicError"] = await BuildErrorMessage();
return RedirectToAction(
PagesController.GetPageByRouteAction,
PagesController.ControllerName,
new { route = "self-assessment" });
}
}

private async Task<string> BuildErrorMessage()
{
var contactLink = await _getNavigationQuery.GetLinkById(_contactOptions.LinkId);
var errorMessage = _errorMessages.ConcurrentUsersOrContentChange;

if (contactLink != null && !string.IsNullOrEmpty(contactLink.Href))
{
errorMessage = errorMessage.Replace("contact us", $"<a href=\"{contactLink.Href}\" target=\"_blank\">contact us</a>");
}

return errorMessage;
}

[HttpPost("{sectionSlug}/{questionSlug}")]
public async Task<IActionResult> SubmitAnswer(
string sectionSlug,
Expand Down
3 changes: 3 additions & 0 deletions src/Dfe.PlanTech.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Dfe.PlanTech.Infrastructure.ServiceBus;
using Dfe.PlanTech.Infrastructure.SignIns;
using Dfe.PlanTech.Web;
using Dfe.PlanTech.Web.Configuration;
using Dfe.PlanTech.Web.Middleware;
using GovUk.Frontend.AspNetCore;

Expand All @@ -27,6 +28,8 @@
}

builder.Services.AddCustomTelemetry();
builder.Services.Configure<ErrorMessages>(builder.Configuration.GetSection("ErrorMessages"));
builder.Services.Configure<ContactOptions>(builder.Configuration.GetSection("ContactUs"));
jack-coggin marked this conversation as resolved.
Show resolved Hide resolved

builder.AddContentAndSupportServices()
.AddAuthorisationServices()
Expand Down
2 changes: 1 addition & 1 deletion src/Dfe.PlanTech.Web/Views/Pages/Page.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</h2>
@if (!string.IsNullOrEmpty(error))
{
<div class="govuk-error-summary__body whitespace-preline">@error</div>
<div class="govuk-error-summary__body whitespace-preline">@Html.Raw(error)</div>
}
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion src/Dfe.PlanTech.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
},
"CacheTimeOutMs": 30000,
"ContactUs": {
"Email": "technology.planning@education.gov.uk",
"LinkId": "7ezhOgrTAdhP4NeGiNj8VY"
},
"ErrorMessages": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,35 @@
private readonly IContentRepository _contentRepository = Substitute.For<IContentRepository>();
private readonly ICmsCache _cache = Substitute.For<ICmsCache>();

private readonly IList<NavigationLink> _contentfulLinks = new List<NavigationLink>()
private readonly NavigationLink _contentfulLink = new NavigationLink
{
new()
{
Href = "ContentfulHref",
DisplayText = "ContentfulDisplayText"
}
Href = "ContentfulHref",
DisplayText = "ContentfulDisplayText"
};

private readonly IList<NavigationLink> _contentfulLinks;

private readonly ILogger<GetNavigationQuery> _logger = Substitute.For<ILogger<GetNavigationQuery>>();

public GetNavigationQueryTests()
{
_contentfulLinks = new List<NavigationLink> { _contentfulLink };

_cache.GetOrCreateAsync(Arg.Any<string>(), Arg.Any<Func<Task<IEnumerable<NavigationLink>>>>())

Check warning on line 30 in tests/Dfe.PlanTech.Application.UnitTests/Content/Queries/GetNavigationQueryTests.cs

View workflow job for this annotation

GitHub Actions / Build and run unit tests

Argument of type 'Task<IEnumerable<NavigationLink>?>' cannot be used for parameter 'value' of type 'Task<IEnumerable<NavigationLink>>' in 'ConfiguredCall SubstituteExtensions.Returns<Task<IEnumerable<NavigationLink>>>(Task<IEnumerable<NavigationLink>> value, Func<CallInfo, Task<IEnumerable<NavigationLink>>> returnThis, params Func<CallInfo, Task<IEnumerable<NavigationLink>>>[] returnThese)' due to differences in the nullability of reference types.
.Returns(callInfo =>
{
var func = callInfo.ArgAt<Func<Task<IEnumerable<NavigationLink>>>>(1);
return func();
});

_cache.GetOrCreateAsync<NavigationLink?>(
Arg.Any<string>(),
Arg.Any<Func<Task<NavigationLink?>>>())
.Returns(callInfo =>
{
var func = callInfo.ArgAt<Func<Task<NavigationLink?>>>(1);
return func();
});
}

[Fact]
Expand Down Expand Up @@ -60,4 +70,28 @@
Assert.Single(receivedLoggerMessages);
Assert.Empty(result);
}

[Fact]
public async Task Should_Retrieve_Nav_Link_By_Id_When_Exists()
{
_contentRepository.GetEntityById<NavigationLink>(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(_contentfulLink);
var navQuery = new GetNavigationQuery(_logger, _contentRepository, _cache);
var result = await navQuery.GetLinkById("contentId");

Assert.NotNull(result);
Assert.Equal(_contentfulLink.Href, result.Href);
Assert.Equal(_contentfulLink.DisplayText, result.DisplayText);
}

[Fact]
public async Task Should_Return_Null_When_Nav_Link_Does_Not_Exist()
{
_contentRepository.GetEntityById<NavigationLink?>(Arg.Any<string>(), Arg.Any<int>(), cancellationToken: CancellationToken.None).Returns((NavigationLink?)null);

var navQuery = new GetNavigationQuery(_logger, _contentRepository, _cache);

var result = await navQuery.GetLinkById("NonExistentId");

Assert.Null(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
using Dfe.PlanTech.Domain.Cookie.Interfaces;
using Dfe.PlanTech.Domain.Establishments.Models;
using Dfe.PlanTech.Domain.Users.Interfaces;
using Dfe.PlanTech.Web.Configuration;
using Dfe.PlanTech.Web.Controllers;
using Dfe.PlanTech.Web.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using Xunit;

Expand All @@ -25,21 +26,27 @@ public class PagesControllerTests
private const string SELF_ASSESSMENT_SLUG = "self-assessment";
readonly ICookieService cookiesSubstitute = Substitute.For<ICookieService>();
readonly IUser userSubstitute = Substitute.For<IUser>();
private readonly IConfiguration _configuration = Substitute.For<IConfiguration>();
private readonly IGetEntityFromContentfulQuery _getEntityFromContentfulQuery;
private readonly IGetNavigationQuery _getNavigationQuery;
private readonly PagesController _controller;
private readonly ControllerContext _controllerContext;
private readonly IOptions<ContactOptions> _contactOptions;

public PagesControllerTests()
{
var Logger = Substitute.For<ILogger<PagesController>>();

_controllerContext = ControllerHelpers.SubstituteControllerContext();

_getEntityFromContentfulQuery = Substitute.For<IGetEntityFromContentfulQuery>();
_getEntityFromContentfulQuery.GetEntityById<NavigationLink>(Arg.Any<string>()).Returns(new NavigationLink { DisplayText = "contact us", Href = "/contact-us", OpenInNewTab = true });
_getNavigationQuery = Substitute.For<IGetNavigationQuery>();
_getNavigationQuery.GetLinkById(Arg.Any<string>()).Returns(new NavigationLink { DisplayText = "contact us", Href = "/contact-us", OpenInNewTab = true });

_controller = new PagesController(Logger, _getEntityFromContentfulQuery)
var contactUs = new ContactOptions
{
LinkId = "LinkId"
};
_contactOptions = Options.Create(contactUs);

_controller = new PagesController(Logger, _getNavigationQuery, _contactOptions)
{
ControllerContext = _controllerContext,
TempData = Substitute.For<ITempDataDictionary>()
Expand Down Expand Up @@ -211,7 +218,7 @@ public async Task Should_Render_Service_Unavailable_Page()

_controller.ControllerContext = controllerContext;

var result = _controller.ServiceUnavailable(_configuration);
var result = _controller.ServiceUnavailable();

var viewResult = await result as ViewResult;

Expand Down Expand Up @@ -251,7 +258,7 @@ public async Task Should_Render_NotFound_Page()
HttpContext = httpContextSubstitute
};
_controller.ControllerContext = controllerContext;
var result = _controller.NotFoundError(_configuration);
var result = _controller.NotFoundError();
var viewResult = await result as ViewResult;
Assert.NotNull(viewResult);
}
Expand Down
Loading
Loading