Skip to content

Commit dc6988a

Browse files
authored
In local baserow integrations, allow to convert field values before they are returned (baserow#4231)
1 parent 92c39b1 commit dc6988a

File tree

16 files changed

+197
-52
lines changed

16 files changed

+197
-52
lines changed

backend/src/baserow/contrib/automation/data_providers/data_provider_types.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,15 @@ def get_data_chunk(
9393
parent_node_id, *rest = path
9494

9595
parent_node_id = int(parent_node_id)
96+
try:
97+
parent_node = AutomationNodeHandler().get_node(parent_node_id)
98+
except AutomationNodeDoesNotExist as exc:
99+
message = "The parent node doesn't exist"
100+
raise InvalidFormulaContext(message) from exc
96101

97102
try:
98103
parent_node_results = dispatch_context.previous_nodes_results[
99-
parent_node_id
104+
parent_node.id
100105
]
101106
except KeyError as exc:
102107
message = (

backend/src/baserow/contrib/builder/data_providers/data_provider_types.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -490,13 +490,15 @@ def get_data_chunk(self, dispatch_context: DispatchContext, path: List[str]):
490490
cache_key = self.get_dispatch_action_cache_key(
491491
dispatch_id, workflow_action.id
492492
)
493-
prepared_path = workflow_action.service.get_type().prepare_value_path(
494-
workflow_action.service.specific, rest
495-
)
496-
return get_value_at_path(cache.get(cache_key), prepared_path)
493+
dispatch_result = cache.get(cache_key)
494+
service = workflow_action.service.specific
495+
prepared_path = service.get_type().prepare_value_path(service, rest)
497496
else:
498497
# Frontend actions
499-
return get_value_at_path(previous_action_results[previous_action_id], rest)
498+
dispatch_result = previous_action_results[previous_action_id]
499+
prepared_path = rest
500+
501+
return get_value_at_path(dispatch_result, prepared_path)
500502

501503
def post_dispatch(
502504
self,

backend/src/baserow/contrib/database/fields/field_types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,19 @@ def parse_filter_value(self, field, model_field, value):
867867
raise ValueError(f"Invalid value for number field: {value}")
868868
return value
869869

870+
def to_runtime_formula_value(self, field, value):
871+
"""
872+
Transform the value to be usable in runtime formula land.
873+
"""
874+
875+
if value is None or value == "":
876+
return None
877+
878+
if field.number_decimal_places == 0:
879+
return int(Decimal(value))
880+
881+
return float(Decimal(value))
882+
870883

871884
class RatingFieldType(FieldType):
872885
type = "rating"

backend/src/baserow/contrib/database/fields/registries.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2024,6 +2024,13 @@ def get_distribution_group_by_value(self, field_name: str):
20242024

20252025
return field_name
20262026

2027+
def to_runtime_formula_value(self, field, value):
2028+
"""
2029+
Transform the value to be usable in runtime formula land.
2030+
"""
2031+
2032+
return value
2033+
20272034

20282035
class ReadOnlyFieldType(FieldType):
20292036
read_only = True

backend/src/baserow/contrib/integrations/local_baserow/service_types.py

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,34 @@ def prepare_value_path(self, service: Service, path: List[str]):
241241

242242
return [human_name, *rest]
243243

244+
def _prepare_one_row(self, mapping, row):
245+
def convert(key, value):
246+
if key in mapping:
247+
return mapping[key]["type"].to_runtime_formula_value(
248+
mapping[key]["field"], value
249+
)
250+
return value
251+
252+
return {key: convert(key, value) for key, value in row.items()}
253+
254+
def _prepare_result(self, table_model, dispatch_result):
255+
mapping = {
256+
field_obj["field"].name: field_obj
257+
for field_obj in table_model.get_field_objects() or []
258+
}
259+
260+
if self.returns_list:
261+
return {
262+
**dispatch_result,
263+
"results": [
264+
self._prepare_one_row(mapping, r)
265+
for r in dispatch_result["results"]
266+
],
267+
}
268+
269+
else:
270+
return self._prepare_one_row(mapping, dispatch_result)
271+
244272
def build_queryset(
245273
self,
246274
service: LocalBaserowTableService,
@@ -1093,13 +1121,16 @@ def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> DispatchResult:
10931121
user_field_names=True,
10941122
)
10951123

1096-
return DispatchResult(
1097-
data={
1124+
result = self._prepare_result(
1125+
dispatch_data["baserow_table_model"],
1126+
{
10981127
"results": serializer(dispatch_data["results"], many=True).data,
10991128
"has_next_page": dispatch_data["has_next_page"],
1100-
}
1129+
},
11011130
)
11021131

1132+
return DispatchResult(data=result)
1133+
11031134
def get_record_names(
11041135
self,
11051136
service: LocalBaserowListRows,
@@ -1611,32 +1642,6 @@ def import_context_path(
16111642

16121643
return self.import_path(path, id_mapping)
16131644

1614-
def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> DispatchResult:
1615-
"""
1616-
Responsible for serializing the `dispatch_data` row.
1617-
1618-
:param dispatch_data: The `dispatch_data` result.
1619-
:return:
1620-
"""
1621-
1622-
field_ids = (
1623-
extract_field_ids_from_list(dispatch_data["public_allowed_properties"])
1624-
if isinstance(dispatch_data["public_allowed_properties"], list)
1625-
else None
1626-
)
1627-
1628-
serializer = get_row_serializer_class(
1629-
dispatch_data["baserow_table_model"],
1630-
RowSerializer,
1631-
is_response=True,
1632-
field_ids=field_ids,
1633-
user_field_names=True,
1634-
)
1635-
1636-
serialized_row = serializer(dispatch_data["data"]).data
1637-
1638-
return DispatchResult(data=serialized_row)
1639-
16401645
def dispatch_data(
16411646
self,
16421647
service: LocalBaserowGetRow,
@@ -1683,6 +1688,34 @@ def dispatch_data(
16831688
except table_model.DoesNotExist:
16841689
raise DoesNotExist()
16851690

1691+
def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> DispatchResult:
1692+
"""
1693+
Responsible for serializing the `dispatch_data` row.
1694+
1695+
:param dispatch_data: The `dispatch_data` result.
1696+
:return:
1697+
"""
1698+
1699+
field_ids = (
1700+
extract_field_ids_from_list(dispatch_data["public_allowed_properties"])
1701+
if isinstance(dispatch_data["public_allowed_properties"], list)
1702+
else None
1703+
)
1704+
1705+
serializer = get_row_serializer_class(
1706+
dispatch_data["baserow_table_model"],
1707+
RowSerializer,
1708+
is_response=True,
1709+
field_ids=field_ids,
1710+
user_field_names=True,
1711+
)
1712+
1713+
serialized_row = self._prepare_result(
1714+
dispatch_data["baserow_table_model"], serializer(dispatch_data["data"]).data
1715+
)
1716+
1717+
return DispatchResult(data=serialized_row)
1718+
16861719

16871720
class LocalBaserowUpsertRowServiceType(
16881721
LocalBaserowTableServiceSpecificRowMixin, LocalBaserowTableServiceType
@@ -2123,7 +2156,10 @@ def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> DispatchResult:
21232156
field_ids=field_ids,
21242157
user_field_names=True,
21252158
)
2126-
serialized_row = serializer(dispatch_data["data"]).data
2159+
2160+
serialized_row = self._prepare_result(
2161+
dispatch_data["baserow_table_model"], serializer(dispatch_data["data"]).data
2162+
)
21272163

21282164
return DispatchResult(data=serialized_row)
21292165

@@ -2296,6 +2332,8 @@ def get_data():
22962332
"has_next_page": False,
22972333
}
22982334

2335+
data_to_process = self._prepare_result(table.get_model(), data_to_process)
2336+
22992337
self._process_event(
23002338
self.model_class.objects.filter(table=table),
23012339
get_data,

backend/src/baserow/core/formula/parser/python_executor.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from decimal import Decimal
2-
31
from baserow.core.formula import BaserowFormula, BaserowFormulaVisitor
42
from baserow.core.formula.parser.exceptions import (
53
BaserowFormulaSyntaxError,
@@ -31,7 +29,7 @@ def visitStringLiteral(self, ctx: BaserowFormula.StringLiteralContext):
3129
return self.process_string(ctx)
3230

3331
def visitDecimalLiteral(self, ctx: BaserowFormula.DecimalLiteralContext):
34-
return Decimal(ctx.getText())
32+
return float(ctx.getText())
3533

3634
def visitBooleanLiteral(self, ctx: BaserowFormula.BooleanLiteralContext):
3735
return ctx.TRUE() is not None

backend/src/baserow/core/formula/validator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ def ensure_string(value: Any, allow_empty: bool = True) -> str:
122122
raise ValidationError("A valid String is required.")
123123
return ""
124124

125+
if isinstance(value, bool):
126+
# To match the frontend
127+
return "true" if value else "false"
125128
if isinstance(value, list):
126129
results = [ensure_string(item) for item in value if item]
127130
return ",".join(results)

backend/src/baserow/core/services/registries.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,10 @@ def resolve_service_formulas(
312312
return resolved_values
313313

314314
def prepare_value_path(self, service: Service, path: List[str]):
315+
"""
316+
Allow to change the path inside a service before it's used.
317+
"""
318+
315319
return path
316320

317321
def dispatch_transform(

backend/tests/baserow/contrib/automation/nodes/test_node_dispatch.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
)
66
from baserow.contrib.automation.nodes.handler import AutomationNodeHandler
77
from baserow.contrib.automation.workflows.constants import WorkflowState
8+
from baserow.contrib.database.rows.handler import RowHandler
89

910

1011
@pytest.mark.django_db
@@ -42,6 +43,71 @@ def test_run_workflow_with_create_row_action(data_fixture):
4243
assert dispatch_context.dispatch_history == [trigger.id, action_node.id]
4344

4445

46+
@pytest.mark.django_db(transaction=True)
47+
def test_run_workflow_with_create_row_action_and_advanced_formula(data_fixture):
48+
user = data_fixture.create_user()
49+
workspace = data_fixture.create_workspace(user=user)
50+
integration = data_fixture.create_local_baserow_integration(user=user)
51+
database = data_fixture.create_database_application(workspace=workspace)
52+
53+
trigger_table, trigger_table_fields, _ = data_fixture.build_table(
54+
user=user,
55+
columns=[
56+
("Food", "text"),
57+
("Spiciness", "number"),
58+
],
59+
rows=[
60+
["Paneer Tikka", 5],
61+
["Gobi Manchurian", 8],
62+
],
63+
)
64+
65+
action_table, action_table_fields, action_rows = data_fixture.build_table(
66+
database=database,
67+
user=user,
68+
columns=[("Name", "text")],
69+
rows=[],
70+
)
71+
workflow = data_fixture.create_automation_workflow(user, state="live")
72+
trigger = workflow.get_trigger()
73+
trigger_service = trigger.service.specific
74+
trigger_service.table = trigger_table
75+
trigger_service.integration = integration
76+
trigger_service.save()
77+
action_node = data_fixture.create_local_baserow_create_row_action_node(
78+
workflow=workflow,
79+
service=data_fixture.create_local_baserow_upsert_row_service(
80+
table=action_table,
81+
integration=integration,
82+
),
83+
)
84+
action_node.service.field_mappings.create(
85+
field=action_table_fields[0],
86+
value=f"concat('The comparaison is ', "
87+
f"get('previous_node.{trigger.id}.0.{trigger_table_fields[1].db_column}') > 7)",
88+
)
89+
90+
action_table_model = action_table.get_model()
91+
assert action_table_model.objects.count() == 0
92+
93+
# Triggers a row creation
94+
RowHandler().create_rows(
95+
user=user,
96+
table=trigger_table,
97+
model=trigger_table.get_model(),
98+
rows_values=[
99+
{
100+
trigger_table_fields[0].db_column: "Spice",
101+
trigger_table_fields[1].db_column: "4.14",
102+
},
103+
],
104+
skip_search_update=True,
105+
)
106+
107+
row = action_table_model.objects.first()
108+
assert getattr(row, action_table_fields[0].db_column) == "The comparaison is false"
109+
110+
45111
@pytest.mark.django_db
46112
def test_run_workflow_with_update_row_action(data_fixture):
47113
user = data_fixture.create_user()

backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2385,13 +2385,13 @@ def test_private_dispatch_data_source_view_returns_all_fields(api_client, data_f
23852385
# Although only field_1 is explicitly used by an element in this
23862386
# page, field_2 is still returned because the Editor page needs
23872387
# access to all data source fields.
2388-
fields[1].name: "5",
2388+
fields[1].name: 5,
23892389
"id": AnyInt(),
23902390
"order": AnyStr(),
23912391
},
23922392
{
23932393
fields[0].name: "Gobi Manchurian",
2394-
fields[1].name: "8",
2394+
fields[1].name: 8,
23952395
"id": AnyInt(),
23962396
"order": AnyStr(),
23972397
},

0 commit comments

Comments
 (0)