Skip to content

Commit

Permalink
Merge pull request #686 from cabinetoffice/feature/radio-buttons-json…
Browse files Browse the repository at this point in the history
…-data

Introducing JsonValue field and extending ChoiceProviderStrategies to allow for json values in radio button options
  • Loading branch information
andymantell authored Oct 2, 2024
2 parents 09e912f + 20dc5eb commit 704801e
Show file tree
Hide file tree
Showing 23 changed files with 1,988 additions and 67 deletions.
20 changes: 14 additions & 6 deletions Frontend/CO.CDP.OrganisationApp.Tests/FormsEngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ private static WebApiClient.SectionQuestionsResponse CreateApiSectionQuestionsRe
);
}

private static SectionQuestionsResponse CreateModelSectionQuestionsResponse(Guid sectionId, Guid questionId, Guid nextQuestionId, string? choiceProviderStrategy = null, List<string>? options = null)
private static SectionQuestionsResponse CreateModelSectionQuestionsResponse(Guid sectionId, Guid questionId, Guid nextQuestionId, string? choiceProviderStrategy = null, Dictionary<string, string>? options = null)
{
return new SectionQuestionsResponse
{
Expand All @@ -108,8 +108,9 @@ private static SectionQuestionsResponse CreateModelSectionQuestionsResponse(Guid
NextQuestion = nextQuestionId,
Options = new FormQuestionOptions
{
Choices = options == null ? new List<string> { "Option1" } : options,
ChoiceProviderStrategy = choiceProviderStrategy
Choices = options == null ? new Dictionary<string, string>() { { "Option1", "Option1" } } : options,
ChoiceProviderStrategy = choiceProviderStrategy,
ChoiceAnswerFieldName = "OptionValue",
}
}
}
Expand Down Expand Up @@ -254,12 +255,19 @@ public async Task GetFormSectionAsync_ShouldFetchChoicesFromCustomChoiceProvider
var questionId = Guid.NewGuid();
var nextQuestionId = Guid.NewGuid();
var apiResponse = CreateApiSectionQuestionsResponse(sectionId, questionId, nextQuestionId, "ExclusionAppliesToChoiceProviderStrategy");
var expectedResponse = CreateModelSectionQuestionsResponse(sectionId, questionId, nextQuestionId, "ExclusionAppliesToChoiceProviderStrategy", ["User's current organisation", "Connected person", "Connected organisation"]);
var expectedResponse = CreateModelSectionQuestionsResponse(sectionId, questionId, nextQuestionId, "ExclusionAppliesToChoiceProviderStrategy");

expectedResponse.Questions[0].Options.Choices = new Dictionary<string, string>() {
{ $@"{{""id"":""{organisationId}"",""type"":""organisation""}}", "User's current organisation" },
{ "{\"id\":\"e4bdd7ef-8200-4257-9892-b16f43d1803e\",\"type\":\"connected-entity\"}", "Connected person" },
{ "{\"id\":\"4c8dccba-df39-4997-814b-7599ed9b5bed\",\"type\":\"connected-entity\"}", "Connected organisation" } };

expectedResponse.Questions[0].Options.ChoiceAnswerFieldName = "JsonValue";

_organisationClientMock.Setup(c => c.GetConnectedEntitiesAsync(It.IsAny<Guid>()))
.ReturnsAsync([
new ConnectedEntityLookup(new Guid(), ConnectedEntityType.Individual, "Connected person", new Uri("http://whatever")),
new ConnectedEntityLookup(new Guid(), ConnectedEntityType.Organisation, "Connected organisation", new Uri("http://whatever"))
new ConnectedEntityLookup(new Guid("e4bdd7ef-8200-4257-9892-b16f43d1803e"), ConnectedEntityType.Individual, "Connected person", new Uri("http://whatever")),
new ConnectedEntityLookup(new Guid("4c8dccba-df39-4997-814b-7599ed9b5bed"), ConnectedEntityType.Organisation, "Connected organisation", new Uri("http://whatever"))
]);
_organisationClientMock.Setup(c => c.GetOrganisationAsync(organisationId))
.ReturnsAsync(new Organisation.WebApiClient.Organisation([], [],null, null, organisationId, null, "User's current organisation", []));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class FormElementSingleChoiceModelTest
public FormElementSingleChoiceModelTest()
{
_model = new FormElementSingleChoiceModel();
_model.Options = new FormQuestionOptions() { Choices = ["Option 1", "Option 2", "Option 3"] } ;
_model.Options = new FormQuestionOptions() { ChoiceAnswerFieldName = "OptionValue", Choices = new Dictionary<string, string>() {{ "Option 1", "Option 1" }, { "Option 2", "Option 2" }, { "Option 3", "Option 3" }}};
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CO.CDP.Forms.WebApiClient;
using CO.CDP.OrganisationApp.Pages.Forms;
using CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
Expand All @@ -12,12 +13,14 @@ public class FormsAnswerSetSummaryModelTest
private readonly Mock<IFormsClient> _formsClientMock;
private readonly Mock<IFormsEngine> _formsEngineMock;
private readonly Mock<ITempDataService> _tempDataServiceMock;
private readonly Mock<IChoiceProviderService> _choiceProviderService;
private readonly FormsAnswerSetSummaryModel _model;
private readonly Guid AnswerSetId = Guid.NewGuid();

public FormsAnswerSetSummaryModelTest()
{
_tempDataServiceMock = new Mock<ITempDataService>();
_choiceProviderService = new Mock<IChoiceProviderService>();
_formsEngineMock = new();

_formsClientMock = new Mock<IFormsClient>();
Expand All @@ -42,7 +45,7 @@ public FormsAnswerSetSummaryModelTest()
furtherQuestionsExempted : false)]
));

_model = new FormsAnswerSetSummaryModel(_formsClientMock.Object, _formsEngineMock.Object, _tempDataServiceMock.Object)
_model = new FormsAnswerSetSummaryModel(_formsClientMock.Object, _formsEngineMock.Object, _tempDataServiceMock.Object, _choiceProviderService.Object)
{
OrganisationId = Guid.NewGuid(),
FormId = Guid.NewGuid(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ public FormsQuestionPageModelTest()
{
Questions = [new FormQuestion { Id = TextQuestionId, Type = FormQuestionType.Text, SummaryTitle = "Sample Question" }]
};

_formsEngineMock.Setup(f => f.ExecuteChoiceProviderStrategy(It.IsAny<CO.CDP.Forms.WebApiClient.FormQuestionOptions>()))
.ReturnsAsync(["Choices"]);


_formsEngineMock.Setup(f => f.GetFormSectionAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<Guid>()))
.ReturnsAsync(form);
_tempDataServiceMock = new Mock<ITempDataService>();
Expand All @@ -38,7 +35,7 @@ public FormsQuestionPageModelTest()
_tempDataServiceMock.Setup(t => t.PeekOrDefault<FormQuestionAnswerState>(It.IsAny<string>()))
.Returns(new FormQuestionAnswerState());
_tempDataServiceMock.Setup(t => t.Remove(It.IsAny<string>()));
_pageModel = new FormsQuestionPageModel(_formsEngineMock.Object, _tempDataServiceMock.Object, _fileHostManagerMock.Object);
_pageModel = new FormsQuestionPageModel(_formsEngineMock.Object, _tempDataServiceMock.Object, _fileHostManagerMock.Object, _choiceProviderServiceMock.Object);

_pageModel.OrganisationId = Guid.NewGuid();
_pageModel.FormId = Guid.NewGuid();
Expand Down
44 changes: 22 additions & 22 deletions Frontend/CO.CDP.OrganisationApp/FormsEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,35 +34,34 @@ public async Task<SectionQuestionsResponse> GetFormSectionAsync(Guid organisatio
Title = response.Section.Title,
AllowsMultipleAnswerSets = response.Section.AllowsMultipleAnswerSets
},
Questions = (await Task.WhenAll(response.Questions.Select(async q => new Models.FormQuestion
{
Id = q.Id,
Title = q.Title,
Description = q.Description,
Caption = q.Caption,
SummaryTitle = q.SummaryTitle,
Type = (Models.FormQuestionType)q.Type,
IsRequired = q.IsRequired,
NextQuestion = q.NextQuestion,
NextQuestionAlternative = q.NextQuestionAlternative,
Options = new Models.FormQuestionOptions
Questions = (await Task.WhenAll(response.Questions.Select(async q => {
IChoiceProviderStrategy choiceProviderStrategy = choiceProviderService.GetStrategy(q.Options.ChoiceProviderStrategy);
return new Models.FormQuestion
{
Choices = await ExecuteChoiceProviderStrategy(q.Options),
ChoiceProviderStrategy = q.Options.ChoiceProviderStrategy
}
Id = q.Id,
Title = q.Title,
Description = q.Description,
Caption = q.Caption,
SummaryTitle = q.SummaryTitle,
Type = (Models.FormQuestionType)q.Type,
IsRequired = q.IsRequired,
NextQuestion = q.NextQuestion,
NextQuestionAlternative = q.NextQuestionAlternative,
Options = new Models.FormQuestionOptions
{
Choices = await choiceProviderStrategy.Execute(q.Options),
ChoiceProviderStrategy = q.Options.ChoiceProviderStrategy,
ChoiceAnswerFieldName = choiceProviderStrategy.AnswerFieldName
}
};
}))).ToList()
};

tempDataService.Put(sessionKey, sectionQuestionsResponse);
return sectionQuestionsResponse;
}

public async Task<List<string>?> ExecuteChoiceProviderStrategy(Forms.WebApiClient.FormQuestionOptions options)
{
IChoiceProviderStrategy strategy = choiceProviderService.GetStrategy(options.ChoiceProviderStrategy);
return await strategy.Execute(options);
}

public async Task<Models.FormQuestion?> GetNextQuestion(Guid organisationId, Guid formId, Guid sectionId, Guid currentQuestionId)
{
var section = await GetFormSectionAsync(organisationId, formId, sectionId);
Expand Down Expand Up @@ -118,7 +117,8 @@ public async Task SaveUpdateAnswers(Guid formId, Guid sectionId, Guid organisati
textValue: a.Answer?.TextValue,
optionValue: a.Answer?.OptionValue,
questionId: a.QuestionId,
addressValue: MapAddress(a.Answer?.AddressValue)
addressValue: MapAddress(a.Answer?.AddressValue),
jsonValue: a.Answer?.JsonValue
)).ToArray(),
furtherQuestionsExempted: answerSet.FurtherQuestionsExempted
);
Expand Down
1 change: 0 additions & 1 deletion Frontend/CO.CDP.OrganisationApp/IFormsEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@ public interface IFormsEngine

Task SaveUpdateAnswers(Guid formId, Guid sectionId, Guid organisationId, FormQuestionAnswerState answerSet);
Task<string> CreateShareCodeAsync(Guid formId, Guid organisationId);
Task<List<string>?> ExecuteChoiceProviderStrategy(Forms.WebApiClient.FormQuestionOptions options);
}
4 changes: 3 additions & 1 deletion Frontend/CO.CDP.OrganisationApp/Models/DynamicForms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ public class FormQuestion

public class FormQuestionOptions
{
public List<string>? Choices { get; set; }
public Dictionary<string, string>? Choices { get; set; }
public string? ChoiceProviderStrategy { get; set; }
public string? ChoiceAnswerFieldName { get; set; }
}

public class FormQuestionAnswerState
Expand All @@ -64,6 +65,7 @@ public class FormAnswer
public string? TextValue { get; init; }
public string? OptionValue { get; init; }
public Address? AddressValue { get; init; }
public string? JsonValue { get; init; }
}

public enum FormQuestionType
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies;
public class ChoiceProviderService(IServiceProvider serviceProvider) : IChoiceProviderService
{
public IChoiceProviderStrategy GetStrategy(string strategyType)
public IChoiceProviderStrategy GetStrategy(string? strategyType)
{
strategyType ??= "DefaultChoiceProviderStrategy";
return serviceProvider.GetKeyedService<IChoiceProviderStrategy>(strategyType)!;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies;
using CO.CDP.Forms.WebApiClient;
namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies;

public class DefaultChoiceProviderStrategy() : IChoiceProviderStrategy
{
public async Task<List<string>?> Execute(FormQuestionOptions options)
public string AnswerFieldName { get; } = "OptionValue";
public async Task<Dictionary<string, string>?> Execute(FormQuestionOptions options)
{
return await Task.FromResult(options.Choices.ToDictionary(c => c.Title, c => c.Title));
}

public async Task<string?> RenderOption(CO.CDP.Forms.WebApiClient.FormAnswer? answer)
{
return await RenderOption(answer?.OptionValue);
}

public async Task<string?> RenderOption(CO.CDP.OrganisationApp.Models.FormAnswer? answer)
{
return await RenderOption(answer?.OptionValue);
}

private async Task<string?> RenderOption(string? optionValue)
{
return await Task.FromResult(options.Choices.Select(c => c.Title).ToList());
return await Task.FromResult(optionValue);
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,84 @@
namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies;
using CO.CDP.Forms.WebApiClient;
using CO.CDP.Organisation.WebApiClient;
using CO.CDP.Tenant.WebApiClient;
using System.Text.Json;

public class ExclusionAppliesToChoiceProviderStrategy(IUserInfoService userInfoService, IOrganisationClient organisationClient) : IChoiceProviderStrategy
{
public async Task<List<string>?> Execute(FormQuestionOptions options)
public string AnswerFieldName { get; } = "JsonValue";

public async Task<Dictionary<string, string>?> Execute(FormQuestionOptions options)
{
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

var organisationId = userInfoService.GetOrganisationId();
if (organisationId != null)
{
var connectedEntities = await organisationClient.GetConnectedEntitiesAsync((Guid)organisationId);
List<string> returnList = [
(await organisationClient.GetOrganisationAsync((Guid)organisationId)).Name
];
returnList.AddRange(connectedEntities.Select(entity => entity.Name));
var organisation = await organisationClient.GetOrganisationAsync((Guid)organisationId);

var result = new Dictionary<string, string>();

result[JsonSerializer.Serialize(new { id = organisation.Id, type = "organisation" }, jsonSerializerOptions)] = organisation.Name;

return returnList;
foreach (var entity in connectedEntities)
{
result[JsonSerializer.Serialize(new { id = entity.EntityId, type = "connected-entity" }, jsonSerializerOptions)] = entity.Name;
}


return result;
}

return null;
}

public async Task<string?> RenderOption(CO.CDP.Forms.WebApiClient.FormAnswer? answer)
{
return await RenderOption(answer?.JsonValue);
}

public async Task<string?> RenderOption(CO.CDP.OrganisationApp.Models.FormAnswer? answer)
{
return await RenderOption(answer?.JsonValue);
}

private async Task<string?> RenderOption(string? jsonValue)
{
if (jsonValue != null)
{
var jsonSerializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};

ExclusionAppliesToChoiceProviderStrategyAnswer? answerValues = JsonSerializer.Deserialize<ExclusionAppliesToChoiceProviderStrategyAnswer>(jsonValue, jsonSerializerOptions);

switch (answerValues?.Type)
{
case "organisation":
var organisation = await organisationClient.GetOrganisationAsync(answerValues.Id);
return organisation.Name;

case "connected-entity":
var organisationId = userInfoService.GetOrganisationId();
if (organisationId != null)
{
var connectedEntities = await organisationClient.GetConnectedEntitiesAsync((Guid)organisationId);
var entity = connectedEntities.FirstOrDefault(e => e.EntityId == answerValues.Id);
return entity?.Name;
}

break;
}
}

return null;
}
}

public class ExclusionAppliesToChoiceProviderStrategyAnswer()
{
required public Guid Id { get; set; }
required public string Type { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies;

public interface IChoiceProviderService
{
IChoiceProviderStrategy GetStrategy(string strategyType);
IChoiceProviderStrategy GetStrategy(string? strategyType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ namespace CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies;

public interface IChoiceProviderStrategy
{
Task<List<string>?> Execute(FormQuestionOptions options);
public string AnswerFieldName { get; }
Task<Dictionary<string, string>?> Execute(FormQuestionOptions options);
public Task<string?> RenderOption(CO.CDP.OrganisationApp.Models.FormAnswer? answer);
public Task<string?> RenderOption(CO.CDP.Forms.WebApiClient.FormAnswer? answer);
}
Loading

0 comments on commit 704801e

Please sign in to comment.