-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
43f09b3
POC
olavsorl 03339cd
Added data processor
olavsorl dd637ed
Modified logic in DataModelWrapper so that the GetResolvedKeysRecursi…
olavsorl 53f29af
Merge branch 'main' into feature/set-data-field-by-expression
olavsorl a11d093
Temp fix for unit test
olavsorl ce5f067
Fix
olavsorl c589a4c
Implemented suggested changes from code rabbit
olavsorl 1ea1ece
Fixed spacing
olavsorl 1f961e4
Fixed DI
olavsorl e9712ef
Fixed unit tests
olavsorl 9bd887e
Fixed unit tests
olavsorl 8858280
Implemented suggested changes from Code Rabbit
olavsorl 7b3f5ab
Implemented suggested changes from code rabbit
olavsorl a86344d
Merge branch 'main' into feature/set-data-field-by-expression
olavsorl 6778191
Implemented suggested change by Sonar Cloud
olavsorl 8275fe0
Implemented suggested changes
olavsorl 6f8d2bd
Cleanup
olavsorl 2816359
Implemented suggested change by code rabbit.
olavsorl 913b06e
Renamed DataFieldValueCalculator to DataModelFieldCalculator
olavsorl 210b986
Implemented suggested changes from code rabbit
olavsorl 7325206
Implemented code rabbit suggestion
olavsorl 0e987e9
Moved json test files outside folder.
olavsorl 30c6eeb
Implemented suggested change by code rabbit
olavsorl c3c4922
Removed array of expressions
olavsorl c5103e9
Merge branch 'main' into feature/set-data-field-by-expression
olavsorl 6c3c960
Modified ExpressionValue to support underlayingType of string when js…
olavsorl ad2d54c
Cleanup in unit tests
olavsorl df10adf
Implemented suggested change by code rabbit
olavsorl efdebbe
Implemented suggested changes
olavsorl 7306126
Fixed unit tests
olavsorl 4b54345
Fixed unit tests
olavsorl a02f2be
Changed to null forgiving initializer
olavsorl 301d08c
Implemented suggested change by code rabbit
olavsorl b584ccc
Merge branch 'main' into feature/set-data-field-by-expression
olavsorl 5ceb31b
Reverted change to expression value
olavsorl fae5d7d
Merge branch 'main' into feature/set-data-field-by-expression
olavsorl 00b87d5
Merge branch 'main' into feature/set-data-field-by-expression
olavsorl 8b1e11e
Merge branch 'main' into feature/set-data-field-by-expression
ivarne fa24d2a
Workflow tweaking (#1802)
ivarne 2f9c307
Merge branch 'main' into feature/set-data-field-by-expression
ivarne 8a1285e
Fix merge conflict and don't use object[] for positional arguments
ivarne 042794e
Don't filter calcluations to only run when the display of the value i…
ivarne b65afdc
Get rid of LayoutEvaluatorStateInitializer in ExpressionValidator
ivarne b7565cc
Unify GetResolvedKeys to always return all resolved keys
ivarne 9dad1af
Apply suggestions from code review
ivarne a767975
Update src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
ivarne d56a5b3
Add tests for GetResolvedKeys
ivarne 360aad7
Improve testing and do some cleanup
ivarne 844aa5a
First batch code review
ivarne 9cbfcce
Accept Olav's Claude suggestion for GetResolvedKeysRecursive
ivarne f946e06
Minor changes to types on GetResolvedKeysRecursive
ivarne ebcb7a5
Let GetResolvedKeys accept empty []
ivarne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
198 changes: 198 additions & 0 deletions
198
src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| 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 IDataElementAccessChecker _dataElementAccessChecker; | ||
| private readonly Telemetry? _telemetry; | ||
|
|
||
| public DataModelFieldCalculator( | ||
| ILogger<DataModelFieldCalculator> logger, | ||
| IAppResources appResourceService, | ||
| IDataElementAccessChecker dataElementAccessChecker, | ||
| Telemetry? telemetry = null | ||
| ) | ||
| { | ||
| _logger = logger; | ||
| _appResourceService = appResourceService; | ||
| _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 44 in src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs
|
||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var calculationConfig = _appResourceService.GetCalculationConfiguration(dataType.Id); | ||
| if (!string.IsNullOrEmpty(calculationConfig)) | ||
| { | ||
| await CalculateFormData(dataAccessor, dataElement, calculationConfig); | ||
| } | ||
| } | ||
| } | ||
|
olavsorl marked this conversation as resolved.
|
||
|
|
||
| internal async Task CalculateFormData( | ||
| IInstanceDataAccessor dataAccessor, | ||
| DataElement dataElement, | ||
| string rawCalculationConfig | ||
| ) | ||
| { | ||
| DataElementIdentifier dataElementIdentifier = dataElement; | ||
| var dataModelFieldCalculations = ParseDataModelFieldCalculationConfig(rawCalculationConfig); | ||
| var formDataWrapper = await dataAccessor.GetFormDataWrapper(dataElement); | ||
|
|
||
| foreach (var (baseField, calculation) in dataModelFieldCalculations) | ||
| { | ||
| var resolvedFields = formDataWrapper.GetResolvedKeys(baseField); | ||
| foreach (var resolvedField in resolvedFields) | ||
| { | ||
| var resolvedFieldReference = new DataReference() | ||
| { | ||
| Field = resolvedField, | ||
| DataElementIdentifier = dataElementIdentifier, | ||
| }; | ||
| var context = new ComponentContext( | ||
| dataAccessor, | ||
| component: null, | ||
| rowIndices: ExpressionHelper.GetRowIndices(resolvedField), | ||
| dataElementIdentifier: dataElementIdentifier | ||
| ); | ||
| var positionalArguments = new ExpressionValue[] { resolvedField }; | ||
|
|
||
| await RunCalculation( | ||
| dataAccessor, | ||
| context, | ||
| formDataWrapper, | ||
| resolvedFieldReference, | ||
| positionalArguments, | ||
| calculation | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private async Task RunCalculation( | ||
| IInstanceDataAccessor dataAccessor, | ||
| ComponentContext context, | ||
| IFormDataWrapper formDataWrapper, | ||
| DataReference resolvedField, | ||
| ExpressionValue[] positionalArguments, | ||
| DataModelFieldCalculation calculation | ||
| ) | ||
| { | ||
| try | ||
| { | ||
| var calculationResult = await ExpressionEvaluator.EvaluateExpressionToExpressionValue( | ||
| dataAccessor, | ||
| 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) | ||
| { | ||
| _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 146 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; | ||
| } | ||
| } | ||
37 changes: 37 additions & 0 deletions
37
src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculatorProcessor.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
src/Altinn.App.Core/Features/Telemetry/Telemetry.DataFieldValueCalculator.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.