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

Introducing JsonValue field and extending ChoiceProviderStrategies to allow for json values in radio button options #686

Merged
merged 8 commits into from
Oct 2, 2024
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