-
Notifications
You must be signed in to change notification settings - Fork 26
Set data field by expression #1683
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
Changes from 30 commits
43f09b3
03339cd
dd637ed
53f29af
a11d093
ce5f067
c589a4c
1ea1ece
1f961e4
e9712ef
9bd887e
8858280
7b3f5ab
a86344d
6778191
8275fe0
6f8d2bd
2816359
913b06e
210b986
7325206
0e987e9
30c6eeb
c3c4922
c5103e9
6c3c960
ad2d54c
df10adf
efdebbe
7306126
4b54345
a02f2be
301d08c
b584ccc
5ceb31b
fae5d7d
00b87d5
8b1e11e
fa24d2a
2f9c307
8a1285e
042794e
b65afdc
b7565cc
9dad1af
a767975
d56a5b3
360aad7
844aa5a
9cbfcce
f946e06
ebcb7a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| using System.Text.Json; | ||
| using Altinn.App.Core.Internal.App; | ||
| using Altinn.App.Core.Internal.Data; | ||
| using Altinn.App.Core.Internal.Expressions; | ||
| using Altinn.App.Core.Models; | ||
| using Altinn.App.Core.Models.Layout; | ||
| using Altinn.Platform.Storage.Interface.Models; | ||
| using Microsoft.Extensions.Logging; | ||
| using ComponentContext = Altinn.App.Core.Models.Expressions.ComponentContext; | ||
|
|
||
| namespace Altinn.App.Core.Features.DataProcessing; | ||
|
|
||
| internal sealed class DataModelFieldCalculator | ||
| { | ||
| private static readonly JsonSerializerOptions _jsonSerializerOptions = new() | ||
| { | ||
| ReadCommentHandling = JsonCommentHandling.Skip, | ||
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | ||
| }; | ||
|
|
||
| private readonly ILogger<DataModelFieldCalculator> _logger; | ||
| private readonly IAppResources _appResourceService; | ||
| private readonly ILayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer; | ||
| private readonly IDataElementAccessChecker _dataElementAccessChecker; | ||
| private readonly Telemetry? _telemetry; | ||
|
|
||
| public DataModelFieldCalculator( | ||
| ILogger<DataModelFieldCalculator> logger, | ||
| ILayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, | ||
| IAppResources appResourceService, | ||
| IDataElementAccessChecker dataElementAccessChecker, | ||
| Telemetry? telemetry = null | ||
| ) | ||
| { | ||
| _logger = logger; | ||
| _appResourceService = appResourceService; | ||
| _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; | ||
| _dataElementAccessChecker = dataElementAccessChecker; | ||
| _telemetry = telemetry; | ||
| } | ||
|
|
||
| public async Task Calculate(IInstanceDataAccessor dataAccessor, string taskId) | ||
| { | ||
| using var activity = _telemetry?.StartCalculateActivity(dataAccessor.Instance.Id, taskId); | ||
| foreach (var (dataType, dataElement) in dataAccessor.GetDataElementsWithFormDataForTask(taskId)) | ||
| { | ||
| if (await _dataElementAccessChecker.CanRead(dataAccessor.Instance, dataType) is false) | ||
|
Check warning on line 47 in src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs
|
||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var calculationConfig = _appResourceService.GetCalculationConfiguration(dataType.Id); | ||
| if (!string.IsNullOrEmpty(calculationConfig)) | ||
| { | ||
| await CalculateFormData(dataAccessor, dataElement, taskId, calculationConfig); | ||
| } | ||
| } | ||
| } | ||
|
olavsorl marked this conversation as resolved.
|
||
|
|
||
| internal async Task CalculateFormData( | ||
| IInstanceDataAccessor dataAccessor, | ||
| DataElement dataElement, | ||
| string taskId, | ||
| string rawCalculationConfig | ||
| ) | ||
| { | ||
| var evaluatorState = await _layoutEvaluatorStateInitializer.Init(dataAccessor, taskId); | ||
| var hiddenFields = await LayoutEvaluator.GetHiddenFieldsForRemoval( | ||
| evaluatorState, | ||
| evaluateRemoveWhenHidden: false | ||
| ); | ||
| DataElementIdentifier dataElementIdentifier = dataElement; | ||
| var dataModelFieldCalculations = ParseDataModelFieldCalculationConfig(rawCalculationConfig); | ||
| var formDataWrapper = await dataAccessor.GetFormDataWrapper(dataElement); | ||
|
|
||
| foreach (var (baseField, calculation) in dataModelFieldCalculations) | ||
| { | ||
| var resolvedFields = await evaluatorState.GetResolvedKeys( | ||
| new DataReference() { Field = baseField, DataElementIdentifier = dataElementIdentifier }, | ||
| true | ||
| ); | ||
| foreach (var resolvedField in resolvedFields) | ||
| { | ||
| if ( | ||
| hiddenFields.Exists(d => | ||
| d.DataElementIdentifier == resolvedField.DataElementIdentifier | ||
| && IsSameOrDescendantField(resolvedField.Field, d.Field) | ||
| ) | ||
| ) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var context = new ComponentContext( | ||
| evaluatorState, | ||
| component: null, | ||
| rowIndices: ExpressionHelper.GetRowIndices(resolvedField.Field), | ||
| dataElementIdentifier: resolvedField.DataElementIdentifier | ||
| ); | ||
| var positionalArguments = new object[] { resolvedField.Field }; | ||
|
|
||
| await RunCalculation( | ||
| formDataWrapper, | ||
| evaluatorState, | ||
| resolvedField, | ||
| context, | ||
| positionalArguments, | ||
| calculation | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private async Task RunCalculation( | ||
| IFormDataWrapper formDataWrapper, | ||
| LayoutEvaluatorState evaluatorState, | ||
| DataReference resolvedField, | ||
| ComponentContext context, | ||
| object[] positionalArguments, | ||
| DataModelFieldCalculation calculation | ||
| ) | ||
| { | ||
| try | ||
| { | ||
| var calculationResult = await ExpressionEvaluator.EvaluateExpressionToExpressionValue( | ||
| evaluatorState, | ||
| calculation.Expression, | ||
| context, | ||
| positionalArguments | ||
| ); | ||
| if (!formDataWrapper.Set(resolvedField.Field, calculationResult)) | ||
| { | ||
| _logger.LogWarning( | ||
| "Could not set calculated value for field {Field} in data element {DataElementId}. " | ||
| + "This is because the type conversion failed.", | ||
| resolvedField.Field, | ||
| resolvedField.DataElementIdentifier.Id | ||
| ); | ||
| } | ||
| } | ||
| catch (Exception e) | ||
|
Check warning on line 141 in src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs
|
||
| { | ||
| _logger.LogError(e, "Error while evaluating calculation for field {Field}", resolvedField.Field); | ||
| throw; | ||
| } | ||
| } | ||
|
|
||
| private Dictionary<string, DataModelFieldCalculation> ParseDataModelFieldCalculationConfig( | ||
| string rawCalculationConfig | ||
| ) | ||
| { | ||
| JsonDocument calculationConfigDocument; | ||
| try | ||
| { | ||
| calculationConfigDocument = JsonDocument.Parse( | ||
| rawCalculationConfig, | ||
| new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip } | ||
| ); | ||
| } | ||
| catch (JsonException e) | ||
| { | ||
| _logger.LogError(e, "Failed to parse calculation configuration JSON"); | ||
| return new Dictionary<string, DataModelFieldCalculation>(); | ||
|
Check warning on line 163 in src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs
|
||
| } | ||
| using (calculationConfigDocument) | ||
| { | ||
| var dataModelFieldCalculations = new Dictionary<string, DataModelFieldCalculation>(); | ||
| var hasCalculations = calculationConfigDocument.RootElement.TryGetProperty( | ||
| "calculations", | ||
| out JsonElement calculationsObject | ||
| ); | ||
| if (hasCalculations) | ||
| { | ||
| foreach (var calculationArray in calculationsObject.EnumerateObject()) | ||
| { | ||
| var field = calculationArray.Name; | ||
| var calculation = calculationArray.Value; | ||
| var resolvedDataModelFieldCalculation = ResolveDataModelFieldCalculation(field, calculation); | ||
| if (resolvedDataModelFieldCalculation == null) | ||
| { | ||
| _logger.LogError("Calculation for field {Field} could not be resolved", field); | ||
| continue; | ||
| } | ||
| dataModelFieldCalculations[field] = resolvedDataModelFieldCalculation; | ||
| } | ||
| } | ||
| return dataModelFieldCalculations; | ||
| } | ||
| } | ||
|
|
||
| private DataModelFieldCalculation? ResolveDataModelFieldCalculation(string field, JsonElement definition) | ||
| { | ||
| var dataModelFieldCalculationDefinition = definition.Deserialize<RawDataModelFieldCalculation>( | ||
| _jsonSerializerOptions | ||
| ); | ||
| if (dataModelFieldCalculationDefinition == null) | ||
| { | ||
| _logger.LogError("Calculation for field {Field} could not be parsed", field); | ||
| return null; | ||
| } | ||
|
|
||
| if (dataModelFieldCalculationDefinition.Expression == null) | ||
| { | ||
| _logger.LogError("Calculation for field {Field} is missing expression", field); | ||
| return null; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| var dataModelFieldCalculation = new DataModelFieldCalculation | ||
| { | ||
| Expression = dataModelFieldCalculationDefinition.Expression.Value, | ||
| }; | ||
|
|
||
| return dataModelFieldCalculation; | ||
| } | ||
|
|
||
| private static bool IsSameOrDescendantField(string candidate, string hiddenField) | ||
| { | ||
| if (candidate.Equals(hiddenField, StringComparison.Ordinal)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return candidate.StartsWith(hiddenField, StringComparison.Ordinal) | ||
| && candidate.Length > hiddenField.Length | ||
| && (candidate[hiddenField.Length] == '.' || candidate[hiddenField.Length] == '['); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| using Altinn.App.Core.Models; | ||
|
|
||
| namespace Altinn.App.Core.Features.DataProcessing; | ||
|
|
||
| /// <summary> | ||
| /// Processing data model fields that is calculated by expressions provided in [modelName].calculation.json. | ||
| /// </summary> | ||
| internal sealed class DataModelFieldCalculatorProcessor : IDataWriteProcessor | ||
| { | ||
| private readonly DataModelFieldCalculator _dataModelFieldCalculator; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="DataModelFieldCalculatorProcessor"/> class. | ||
| /// </summary> | ||
| /// <param name="dataModelFieldCalculator"></param> | ||
| public DataModelFieldCalculatorProcessor(DataModelFieldCalculator dataModelFieldCalculator) | ||
| { | ||
| _dataModelFieldCalculator = dataModelFieldCalculator; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Processes data write operations on properties in the data model. | ||
| /// </summary> | ||
| /// <param name="instanceDataMutator">Object to fetch data elements not included in changes</param> | ||
| /// <param name="taskId">The current task ID</param> | ||
| /// <param name="changes">Not used in this context</param> | ||
| /// <param name="language">Not used in this context</param> | ||
| public async Task ProcessDataWrite( | ||
| IInstanceDataMutator instanceDataMutator, | ||
| string taskId, | ||
| DataElementChanges changes, | ||
| string? language | ||
| ) | ||
| { | ||
| await _dataModelFieldCalculator.Calculate(instanceDataMutator, taskId); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| using System.Diagnostics; | ||
| using static Altinn.App.Core.Features.Telemetry.DataModelFieldCalculator; | ||
|
|
||
| namespace Altinn.App.Core.Features; | ||
|
|
||
| partial class Telemetry | ||
| { | ||
| internal Activity? StartCalculateActivity(string instanceId, string taskId) | ||
| { | ||
| var activity = ActivitySource.StartActivity($"{Prefix}.Calculate"); | ||
| activity?.SetInstanceId(instanceId); | ||
| activity?.SetTaskId(taskId); | ||
| return activity; | ||
| } | ||
|
|
||
| internal static class DataModelFieldCalculator | ||
| { | ||
| internal const string Prefix = "DataModelFieldCalculator"; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.