Skip to content

Commit 4db331c

Browse files
authored
fix: element formulas are imported once all elements exist (baserow#4952)
1 parent c639c99 commit 4db331c

File tree

6 files changed

+59
-57
lines changed

6 files changed

+59
-57
lines changed

backend/src/baserow/contrib/builder/elements/mixins.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747
class ContainerElementTypeMixin:
4848
# Container element types are imported first.
49-
import_element_priority = 2
49+
import_element_priority = 1
5050

5151
class SerializedDict(ElementDict):
5252
pass
@@ -801,9 +801,6 @@ def extract_properties(
801801

802802

803803
class FormElementTypeMixin:
804-
# Form element types are imported second, after containers.
805-
import_element_priority = 1
806-
807804
def is_valid(
808805
self,
809806
element: Type[FormElement],

backend/src/baserow/contrib/builder/elements/registries.py

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from rest_framework import serializers
2121
from rest_framework.exceptions import ValidationError
2222

23-
from baserow.contrib.builder.formula_importer import import_formula
2423
from baserow.contrib.builder.mixins import BuilderInstanceWithFormulaMixin
2524
from baserow.contrib.builder.pages.models import Page
2625
from baserow.core.formula.types import BaserowFormulaObject
@@ -202,8 +201,6 @@ def import_serialized(
202201
cache: Dict[str, Any] | None = None,
203202
**kwargs,
204203
) -> ElementSubClass:
205-
from baserow.contrib.builder.elements.handler import ElementHandler
206-
207204
# Add mapping for builder element event uids (for collection field or other
208205
# elements that are using dynamic events.
209206
if "builder_element_event_uids" not in id_mapping:
@@ -212,20 +209,6 @@ def import_serialized(
212209
if cache is None:
213210
cache = {}
214211

215-
import_context = {}
216-
217-
parent_element_id = serialized_values["parent_element_id"]
218-
219-
# If we have a parent element then we want to add used its import context
220-
if parent_element_id:
221-
imported_parent_element_id = id_mapping["builder_page_elements"][
222-
parent_element_id
223-
]
224-
import_context = ElementHandler().get_import_context_addition(
225-
imported_parent_element_id,
226-
element_map=cache.get("imported_element_map", None),
227-
)
228-
229212
existing_roles = cache.get("existing_roles", {}).get(page.builder.id)
230213
if not existing_roles:
231214
existing_roles = UserSourceHandler().get_all_roles_for_application(
@@ -246,16 +229,9 @@ def import_serialized(
246229
files_zip,
247230
storage,
248231
cache,
249-
**(kwargs | import_context),
250-
)
251-
252-
# Update formulas of the current element
253-
updated_models = self.import_formulas(
254-
created_instance, id_mapping, import_formula, **(kwargs | import_context)
232+
**kwargs,
255233
)
256234

257-
[m.save() for m in updated_models]
258-
259235
# Add created instance to an element cache
260236
cache.setdefault("imported_element_map", {})[created_instance.id] = (
261237
created_instance

backend/src/baserow/contrib/builder/pages/handler.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from baserow.contrib.builder.elements.handler import ElementHandler
1212
from baserow.contrib.builder.elements.registries import element_type_registry
1313
from baserow.contrib.builder.elements.types import ElementDictSubClass
14+
from baserow.contrib.builder.formula_importer import import_formula
1415
from baserow.contrib.builder.models import Builder
1516
from baserow.contrib.builder.pages.constants import (
1617
ILLEGAL_PATH_SAMPLE_CHARACTER,
@@ -808,7 +809,6 @@ def import_elements(
808809

809810
# Sort the serialized elements so that we import:
810811
# Containers first
811-
# Form elements second
812812
# Everything else after that.
813813
def element_priority_sort(element_to_sort):
814814
return element_type_registry.get(
@@ -849,6 +849,30 @@ def element_priority_sort(element_to_sort):
849849
if progress:
850850
progress.increment(state=IMPORT_SERIALIZED_IMPORTING)
851851

852+
# Now that all elements have been imported, loop back over them
853+
# and start import their formulas. We do this because formulas can
854+
# reference one another, so we need the full set of elements to be
855+
# imported before we can safely import the formulas without running
856+
# into issues of missing referenced elements in `id_mapping`.
857+
updated_models = set()
858+
for elt in imported_elements:
859+
import_context = {}
860+
if elt.parent_element_id:
861+
import_context = ElementHandler().get_import_context_addition(
862+
elt.parent_element_id,
863+
element_map=cache.get("imported_element_map", None)
864+
if cache
865+
else None,
866+
)
867+
updated_models = updated_models | elt.get_type().import_formulas(
868+
elt,
869+
id_mapping,
870+
import_formula,
871+
**import_context,
872+
)
873+
874+
[m.save() for m in updated_models]
875+
852876
return imported_elements
853877

854878
def import_workflow_actions(

backend/tests/baserow/contrib/builder/elements/test_element_types.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
from baserow.contrib.builder.elements.handler import ElementHandler
3939
from baserow.contrib.builder.elements.mixins import (
4040
ContainerElementTypeMixin,
41-
FormElementTypeMixin,
4241
)
4342
from baserow.contrib.builder.elements.models import (
4443
ButtonElement,
@@ -64,6 +63,7 @@
6463
element_type_registry,
6564
)
6665
from baserow.contrib.builder.elements.service import ElementService
66+
from baserow.contrib.builder.pages.handler import PageHandler
6767
from baserow.contrib.builder.pages.service import PageService
6868
from baserow.contrib.database.fields.handler import FieldHandler
6969
from baserow.core.handler import CoreHandler
@@ -269,7 +269,7 @@ def test_link_element_import_export_formula(data_fixture):
269269
# After applying the ID mapping the imported formula should have updated
270270
# the data source IDs
271271
id_mapping = {"builder_data_sources": {data_source_1.id: data_source_2.id}}
272-
imported_element = element_type.import_serialized(page, serialized, id_mapping)
272+
[imported_element] = PageHandler().import_elements(page, [serialized], id_mapping)
273273

274274
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
275275
expected_query_formula = f"get('data_source.{data_source_2.id}.field_2')"
@@ -300,7 +300,7 @@ def test_form_container_element_import_export_formula(data_fixture):
300300
# After applying the ID mapping the imported formula should have updated
301301
# the data source IDs
302302
id_mapping = {"builder_data_sources": {data_source_1.id: data_source_2.id}}
303-
imported_element = element_type.import_serialized(page, serialized, id_mapping)
303+
[imported_element] = PageHandler().import_elements(page, [serialized], id_mapping)
304304

305305
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
306306
assert imported_element.submit_button_label["formula"] == expected_formula
@@ -339,7 +339,7 @@ def test_text_element_import_export_formula(data_fixture):
339339
# After applying the ID mapping the imported formula should have updated
340340
# the data source IDs
341341
id_mapping = {"builder_data_sources": {data_source_1.id: data_source_2.id}}
342-
imported_element = element_type.import_serialized(page, serialized, id_mapping)
342+
[imported_element] = PageHandler().import_elements(page, [serialized], id_mapping)
343343

344344
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
345345
assert imported_element.value["formula"] == expected_formula
@@ -363,7 +363,7 @@ def test_input_text_element_import_export_formula(data_fixture):
363363
# After applying the ID mapping the imported formula should have updated
364364
# the data source IDs
365365
id_mapping = {"builder_data_sources": {data_source_1.id: data_source_2.id}}
366-
imported_element = element_type.import_serialized(page, serialized, id_mapping)
366+
[imported_element] = PageHandler().import_elements(page, [serialized], id_mapping)
367367

368368
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
369369
assert imported_element.label["formula"] == expected_formula
@@ -388,7 +388,7 @@ def test_image_element_import_export_formula(data_fixture):
388388
# After applying the ID mapping the imported formula should have updated
389389
# the data source IDs
390390
id_mapping = {"builder_data_sources": {data_source_1.id: data_source_2.id}}
391-
imported_element = element_type.import_serialized(page, serialized, id_mapping)
391+
[imported_element] = PageHandler().import_elements(page, [serialized], id_mapping)
392392

393393
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
394394
assert imported_element.image_url["formula"] == expected_formula
@@ -411,7 +411,7 @@ def test_button_element_import_export_formula(data_fixture):
411411
# After applying the ID mapping the imported formula should have updated
412412
# the data source IDs
413413
id_mapping = {"builder_data_sources": {data_source_1.id: data_source_2.id}}
414-
imported_element = element_type.import_serialized(page, serialized, id_mapping)
414+
[imported_element] = PageHandler().import_elements(page, [serialized], id_mapping)
415415

416416
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
417417
assert imported_element.value["formula"] == expected_formula
@@ -500,7 +500,7 @@ def test_choice_element_import_export_formula(data_fixture):
500500
# After applying the ID mapping the imported formula should have updated
501501
# the data source IDs
502502
id_mapping = {"builder_data_sources": {data_source_1.id: data_source_2.id}}
503-
imported_element = element_type.import_serialized(page, serialized, id_mapping)
503+
[imported_element] = PageHandler().import_elements(page, [serialized], id_mapping)
504504

505505
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
506506
assert imported_element.label["formula"] == expected_formula
@@ -793,26 +793,20 @@ def test_element_type_import_element_priority():
793793
for element_type in element_types
794794
if isinstance(element_type, ContainerElementTypeMixin)
795795
]
796-
form_element_types = [
797-
element_type
798-
for element_type in element_types
799-
if isinstance(element_type, FormElementTypeMixin)
800-
]
801796
other_element_types = [
802797
element_type
803798
for element_type in element_types
804799
if not isinstance(element_type, ContainerElementTypeMixin)
805-
and not isinstance(element_type, FormElementTypeMixin)
806800
]
807-
manual_ordering = container_element_types + form_element_types + other_element_types
801+
manual_ordering = container_element_types + other_element_types
808802
expected_ordering = sorted(
809803
element_types,
810804
key=lambda element_type: element_type.import_element_priority,
811805
reverse=True,
812806
)
813807
assert manual_ordering == expected_ordering, (
814808
"The element types ordering are expected to be: "
815-
"containers first, then form elements, then everything else."
809+
"containers first, then everything else."
816810
)
817811

818812

@@ -852,7 +846,7 @@ def test_checkbox_element_import_export_formula(data_fixture):
852846
# After applying the ID mapping the imported formula should have updated
853847
# the data source IDs
854848
id_mapping = {"builder_data_sources": {data_source_1.id: data_source_2.id}}
855-
imported_element = element_type.import_serialized(page, serialized, id_mapping)
849+
[imported_element] = PageHandler().import_elements(page, [serialized], id_mapping)
856850

857851
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
858852
assert imported_element.label["formula"] == expected_formula
@@ -909,8 +903,9 @@ def test_iframe_element_import_export_formula(data_fixture):
909903

910904
# After applying the ID mapping the imported formula should have updated
911905
# the data source IDs
906+
# rather than in element_type.import_serialized.
912907
id_mapping = {"builder_data_sources": {data_source_1.id: data_source_2.id}}
913-
imported_element = element_type.import_serialized(page, serialized, id_mapping)
908+
[imported_element] = PageHandler().import_elements(page, [serialized], id_mapping)
914909

915910
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
916911
assert imported_element.url["formula"] == expected_formula
@@ -961,8 +956,8 @@ def test_image_element_import_export(data_fixture, fake, storage):
961956
image_file.delete()
962957

963958
with ZipFile(zip_buffer, "r", ZIP_DEFLATED, False) as files_zip:
964-
imported_element = element_type.import_serialized(
965-
page, serialized, id_mapping, files_zip=files_zip, storage=storage
959+
[imported_element] = PageHandler().import_elements(
960+
page, [serialized], id_mapping, files_zip=files_zip, storage=storage
966961
)
967962

968963
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
@@ -995,7 +990,7 @@ def test_choice_element_import_export(data_fixture):
995990
# the data source IDs
996991
id_mapping = {"builder_data_sources": {42: data_source_2.id}}
997992

998-
imported_element = element_type.import_serialized(page, serialized, id_mapping)
993+
[imported_element] = PageHandler().import_elements(page, [serialized], id_mapping)
999994

1000995
expected_formula = f"get('data_source.{data_source_2.id}.field_1')"
1001996
assert imported_element.label["formula"] == expected_formula

backend/tests/baserow/contrib/builder/test_element_formula_mixin.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
LinkElement,
2828
TextElement,
2929
)
30+
from baserow.contrib.builder.pages.handler import PageHandler
3031
from baserow.core.formula import BaserowFormulaObject
3132
from baserow.core.formula.field import BASEROW_FORMULA_VERSION_INITIAL
3233
from baserow.core.formula.types import BASEROW_FORMULA_MODE_SIMPLE
@@ -96,9 +97,9 @@ def test_element_formula_generator_mixin(
9697
)
9798
serialized_element = element_type().export_serialized(exported_element)
9899

99-
imported_element = element_type().import_serialized(
100+
[imported_element] = PageHandler().import_elements(
100101
formula_generator_fixture["page"],
101-
serialized_element,
102+
[serialized_element],
102103
formula_generator_fixture["id_mapping"],
103104
)
104105

@@ -143,9 +144,9 @@ def test_link_element_formula_generator(data_fixture, formula_generator_fixture)
143144
)
144145
serialized_element = LinkElementType().export_serialized(exported_element)
145146

146-
imported_element = LinkElementType().import_serialized(
147+
[imported_element] = PageHandler().import_elements(
147148
formula_generator_fixture["page"],
148-
serialized_element,
149+
[serialized_element],
149150
formula_generator_fixture["id_mapping"],
150151
)
151152

@@ -248,9 +249,9 @@ def test_menu_element_formula_generator(data_fixture, formula_generator_fixture)
248249

249250
serialized_element = MenuElementType().export_serialized(menu_element)
250251

251-
imported_element = MenuElementType().import_serialized(
252+
[imported_element] = PageHandler().import_elements(
252253
formula_generator_fixture["page"],
253-
serialized_element,
254+
[serialized_element],
254255
formula_generator_fixture["id_mapping"],
255256
)
256257

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Resolved a bug where a container-type element with a visibility condition would be unable to use form data in its formula.",
4+
"issue_origin": "github",
5+
"issue_number": null,
6+
"domain": "builder",
7+
"bullet_points": [],
8+
"created_at": "2026-03-11"
9+
}

0 commit comments

Comments
 (0)