From 8c514c49cd94df06cd2b7491bbefbaa63608b944 Mon Sep 17 00:00:00 2001 From: dimmur-brw Date: Fri, 15 May 2026 11:41:49 +0200 Subject: [PATCH 1/7] fix periodic formula updates now cascade to dependent formulas across linked tables (#5373) --- .../contrib/database/fields/field_types.py | 18 ++++++--- .../database/field/test_field_handler.py | 1 + .../database/field/test_field_tasks.py | 39 +++++++++++++++++++ ...ation_of_formulas_that_depend_on_now_.json | 9 +++++ 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 changelog/entries/unreleased/bug/5370_fixed_periodic_recalculation_of_formulas_that_depend_on_now_.json diff --git a/backend/src/baserow/contrib/database/fields/field_types.py b/backend/src/baserow/contrib/database/fields/field_types.py index 6ecb170df1..302bc1ff44 100755 --- a/backend/src/baserow/contrib/database/fields/field_types.py +++ b/backend/src/baserow/contrib/database/fields/field_types.py @@ -5734,19 +5734,27 @@ def run_periodic_update( # LinkRowFields might depends on FormulaFields, but we can't update # them here because this is only valid for FormulaFields. continue + if dependant_field in updated_fields: + continue + if table_id not in update_collectors: + update_collectors[table_id] = FieldUpdateCollector( + dependant_field.table, update_changes_only=True + ) self._update_field_values( dependant_field, update_collectors[table_id], field_cache, via_path_to_starting_table, ) - updated_fields |= set( - update_collector.apply_updates_and_get_updated_fields( - field_cache, skip_search_updates=skip_search_updates + for collector in update_collectors.values(): + updated_fields |= set( + collector.apply_updates_and_get_updated_fields( + field_cache, skip_search_updates=skip_search_updates + ) ) - ) - update_collector.send_force_refresh_signals_for_all_updated_tables() + for collector in update_collectors.values(): + collector.send_force_refresh_signals_for_all_updated_tables() return list(updated_fields) diff --git a/backend/tests/baserow/contrib/database/field/test_field_handler.py b/backend/tests/baserow/contrib/database/field/test_field_handler.py index 103dde5b4c..11fcbda4b9 100644 --- a/backend/tests/baserow/contrib/database/field/test_field_handler.py +++ b/backend/tests/baserow/contrib/database/field/test_field_handler.py @@ -101,6 +101,7 @@ def clean_registry_cache(): field_type_registry.get_for_class.cache_clear() yield + field_type_registry.get_for_class.cache_clear() def _test_can_convert_between_fields(data_fixture, field_type_to_test): diff --git a/backend/tests/baserow/contrib/database/field/test_field_tasks.py b/backend/tests/baserow/contrib/database/field/test_field_tasks.py index b1a104749d..166fc28929 100644 --- a/backend/tests/baserow/contrib/database/field/test_field_tasks.py +++ b/backend/tests/baserow/contrib/database/field/test_field_tasks.py @@ -605,3 +605,42 @@ def test_invalid_formula_is_skipped_by_periodic_update(data_fixture): assert bool_formula.needs_periodic_update is True assert FormulaFieldType().get_fields_needing_periodic_update().exists() is False + + +@pytest.mark.django_db +def test_cross_table_dependent_formulas_update_when_multiple_tables_have_now( + data_fixture, +): + with freeze_time("2023-01-01"): + database = data_fixture.create_database_application() + + table_b = data_fixture.create_database_table(database=database) + primary_b = data_fixture.create_formula_field( + table=table_b, primary=True, formula="now()" + ) + + table_a = data_fixture.create_database_table(database=database) + link_a_to_b = data_fixture.create_link_row_field( + table=table_a, link_row_table=table_b + ) + formula_a = data_fixture.create_formula_field( + table=table_a, + formula=(f"join(datetime_format(field('{link_a_to_b.name}'), 'DD'), ',')"), + ) + + # Second formula with now() + now_in_a = data_fixture.create_formula_field( + table=table_a, + formula="datetime_format(now(), 'YYYY-MM-DD')", + ) + + row_b = RowHandler().force_create_row(None, table_b, {}) + row_a = RowHandler().force_create_row( + None, table_a, {link_a_to_b.db_column: [row_b.id]} + ) + + with freeze_time("2023-01-02"), local_cache.context(): + run_periodic_fields_updates() + + row_a.refresh_from_db() + assert getattr(row_a, formula_a.db_column) == "02" diff --git a/changelog/entries/unreleased/bug/5370_fixed_periodic_recalculation_of_formulas_that_depend_on_now_.json b/changelog/entries/unreleased/bug/5370_fixed_periodic_recalculation_of_formulas_that_depend_on_now_.json new file mode 100644 index 0000000000..6e74e10665 --- /dev/null +++ b/changelog/entries/unreleased/bug/5370_fixed_periodic_recalculation_of_formulas_that_depend_on_now_.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fixed periodic recalculation of formulas that depend on now() across linked tables.", + "issue_origin": "github", + "issue_number": 5370, + "domain": "database", + "bullet_points": [], + "created_at": "2026-05-15" +} \ No newline at end of file From 090fc3550da0ffa4e2a8341cd3490a4957e0eb81 Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 15 May 2026 11:45:00 +0200 Subject: [PATCH 2/7] feat: Add clear button to the view search (#5351) --- .../2753_clear_button_for_view_search.json | 9 +++ .../assets/scss/components/form_input.scss | 18 ++++++ .../core/assets/scss/components/header.scss | 23 +++++++ .../modules/core/components/FormInput.vue | 14 ++++- .../database/components/view/ViewSearch.vue | 16 +++++ .../components/view/ViewSearchContext.vue | 60 +++++++++++++------ web-frontend/modules/database/locales/en.json | 3 + .../__snapshots__/userAdmin.spec.js.snap | 2 + .../__snapshots__/publicView.spec.js.snap | 9 +++ .../database/__snapshots__/table.spec.js.snap | 4 +- .../__snapshots__/viewFilterForm.spec.js.snap | 1 + 11 files changed, 137 insertions(+), 22 deletions(-) create mode 100644 changelog/entries/unreleased/feature/2753_clear_button_for_view_search.json diff --git a/changelog/entries/unreleased/feature/2753_clear_button_for_view_search.json b/changelog/entries/unreleased/feature/2753_clear_button_for_view_search.json new file mode 100644 index 0000000000..9b8241ef6a --- /dev/null +++ b/changelog/entries/unreleased/feature/2753_clear_button_for_view_search.json @@ -0,0 +1,9 @@ +{ + "type": "feature", + "message": "Add clear button to the view search", + "issue_origin": "github", + "issue_number": 2753, + "domain": "database", + "bullet_points": [], + "created_at": "2026-05-11" +} diff --git a/web-frontend/modules/core/assets/scss/components/form_input.scss b/web-frontend/modules/core/assets/scss/components/form_input.scss index 5888f4676d..fa5570a3d5 100644 --- a/web-frontend/modules/core/assets/scss/components/form_input.scss +++ b/web-frontend/modules/core/assets/scss/components/form_input.scss @@ -100,6 +100,10 @@ padding-right: 8px; } + .form-input--can-clear & { + padding-right: 26px; + } + .form-input--disabled & { cursor: not-allowed; background-color: $palette-neutral-100; @@ -181,6 +185,20 @@ } } +.form-input__clear { + color: $palette-neutral-900; + font-size: 14px; + line-height: 20px; + margin-top: -10px; + + @include absolute(50%, 8px, 0, auto); + + &:hover { + text-decoration: none; + color: $palette-neutral-1200; + } +} + .form-input__suffix { display: flex; align-items: center; diff --git a/web-frontend/modules/core/assets/scss/components/header.scss b/web-frontend/modules/core/assets/scss/components/header.scss index 6277f9daec..a705a7d7b5 100644 --- a/web-frontend/modules/core/assets/scss/components/header.scss +++ b/web-frontend/modules/core/assets/scss/components/header.scss @@ -238,6 +238,7 @@ } .header__search { + position: relative; margin-left: auto; flex-direction: row-reverse; display: flex; @@ -248,6 +249,28 @@ @extend %ellipsis; display: inline-block; + + &.active { + padding-right: 26px; + } + } +} + +.header__search-form { + position: relative; +} + +.header__search-clear { + color: $palette-neutral-900; + font-size: 14px; + line-height: 20px; + margin-top: -10px; + + @include absolute(50%, 8px, 0, auto); + + &:hover { + text-decoration: none; + color: $palette-neutral-1200; } } diff --git a/web-frontend/modules/core/components/FormInput.vue b/web-frontend/modules/core/components/FormInput.vue index ea1d4f78a9..62116fa1ef 100644 --- a/web-frontend/modules/core/components/FormInput.vue +++ b/web-frontend/modules/core/components/FormInput.vue @@ -13,6 +13,7 @@ 'form-input--xlarge': size === 'xlarge', 'form-input--suffix': hasSuffixSlot, 'form-input--no-controls': removeNumberInputControls, + 'form-input--can-clear': canClear, }" @click="focusOnClick && focus()" > @@ -27,7 +28,9 @@ :id="forInput" ref="input" class="form-input__input" - :class="{ 'form-input__input--text-invisible': textInvisible }" + :class="{ + 'form-input__input--text-invisible': textInvisible, + }" :value="fromValue(innerValue)" :disabled="disabled" :type="type" @@ -52,6 +55,14 @@ class="form-input__icon form-input__icon-right" :class="iconRight" /> + + +
@@ -95,6 +106,7 @@ const props = defineProps({ step: { type: Number, default: -1 }, focusOnClick: { type: Boolean, default: true }, textInvisible: Boolean, + canClear: { type: Boolean, default: false }, }) const emit = defineEmits([ diff --git a/web-frontend/modules/database/components/view/ViewSearch.vue b/web-frontend/modules/database/components/view/ViewSearch.vue index 3272153c3b..ce9842a412 100644 --- a/web-frontend/modules/database/components/view/ViewSearch.vue +++ b/web-frontend/modules/database/components/view/ViewSearch.vue @@ -11,6 +11,14 @@ {{ headerSearchTerm }} + + + diff --git a/web-frontend/modules/database/components/view/ViewSearchContext.vue b/web-frontend/modules/database/components/view/ViewSearchContext.vue index d894274514..677b291ee8 100644 --- a/web-frontend/modules/database/components/view/ViewSearchContext.vue +++ b/web-frontend/modules/database/components/view/ViewSearchContext.vue @@ -7,15 +7,18 @@ @shown="focus" >
- +
+ +
A users attributes will be displayed 1`] =
+
@@ -155,6 +156,7 @@ exports[`User Admin Component Tests > you can sort by multiple columns which wil
+
diff --git a/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap b/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap index 1c4064e9b5..0c7cc4e1a2 100644 --- a/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap +++ b/web-frontend/test/unit/database/__snapshots__/publicView.spec.js.snap @@ -313,6 +313,15 @@ exports[`Public View Page Tests > Can see a publicly shared grid view 1`] = ` /> +
diff --git a/web-frontend/test/unit/database/__snapshots__/table.spec.js.snap b/web-frontend/test/unit/database/__snapshots__/table.spec.js.snap index e877299937..524eff220e 100644 --- a/web-frontend/test/unit/database/__snapshots__/table.spec.js.snap +++ b/web-frontend/test/unit/database/__snapshots__/table.spec.js.snap @@ -68,7 +68,7 @@ exports[`Table Component Tests > Adding a row to a table increases the row count
  • - +
  • @@ -289,7 +289,7 @@ exports[`Table Component Tests > Adding a row to a table increases the row count
  • - +
  • diff --git a/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap b/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap index 2635962548..a28c194666 100644 --- a/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap +++ b/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap @@ -559,6 +559,7 @@ exports[`ViewFilterForm match snapshots > Full view filter component 1`] = ` value="test" /> + From ef90048fc42b5c6510df91ab10bc21ebaca2579b Mon Sep 17 00:00:00 2001 From: Davide Silvestri <75379892+silvestrid@users.noreply.github.com> Date: Fri, 15 May 2026 15:13:21 +0200 Subject: [PATCH 3/7] fix: keep field_cache consistent during formula dependency cascade (#5372) * fix: keep field_cache consistent during formula dependency cascade * fix: clear cached_typed_internal_expression in clear_cached_properties clear_cached_properties only cleared cached_untyped_expression and cached_formula_type, leaving cached_typed_internal_expression stuck on the pre-refresh value. Any caller that mutates a FormulaField in the DB and then calls refresh_from_db on a held instance would still get the stale typed expression, which masks bugs of the same shape as #5371. * fix flaky test --- .../baserow/contrib/database/fields/models.py | 26 +++--- .../database/api/views/test_view_views.py | 1 + .../database/field/test_formula_field_type.py | 82 +++++++++++++++++++ .../field/test_multiple_select_field_type.py | 40 ++++----- .../database/view/test_view_handler.py | 1 + ...te_crash_after_deleting_a_field_refer.json | 9 ++ 6 files changed, 122 insertions(+), 37 deletions(-) create mode 100644 changelog/entries/unreleased/bug/5371_fix_periodic_field_update_crash_after_deleting_a_field_refer.json diff --git a/backend/src/baserow/contrib/database/fields/models.py b/backend/src/baserow/contrib/database/fields/models.py index f311f6a136..07051d3466 100644 --- a/backend/src/baserow/contrib/database/fields/models.py +++ b/backend/src/baserow/contrib/database/fields/models.py @@ -730,18 +730,17 @@ def cached_formula_type(self): return FormulaHandler.get_formula_type_from_field(self) def clear_cached_properties(self): - try: - # noinspection PyPropertyAccess - del self.cached_untyped_expression - except AttributeError: - # It has not been cached yet so nothing to deleted. - pass - try: - # noinspection PyPropertyAccess - del self.cached_formula_type - except AttributeError: - # It has not been cached yet so nothing to deleted. - pass + for attr in ( + "cached_untyped_expression", + "cached_typed_internal_expression", + "cached_formula_type", + ): + try: + # noinspection PyPropertyAccess + delattr(self, attr) + except AttributeError: + # It has not been cached yet so nothing to delete. + pass def recalculate_internal_fields(self, raise_if_invalid=False, field_cache=None): self.clear_cached_properties() @@ -781,6 +780,9 @@ def save(self, *args, **kwargs): field_cache=field_cache, raise_if_invalid=raise_if_invalid ) super().save(*args, **kwargs) + # Keep field_cache consistent to avoid stale type info downstream. See GH #5371. + if field_cache is not None: + field_cache.cache_field(self) def refresh_from_db(self, *args, **kwargs) -> None: super().refresh_from_db(*args, **kwargs) diff --git a/backend/tests/baserow/contrib/database/api/views/test_view_views.py b/backend/tests/baserow/contrib/database/api/views/test_view_views.py index 25a9e4d787..20f7a0177d 100644 --- a/backend/tests/baserow/contrib/database/api/views/test_view_views.py +++ b/backend/tests/baserow/contrib/database/api/views/test_view_views.py @@ -43,6 +43,7 @@ def clean_registry_cache(): view_type_registry.get_for_class.cache_clear() yield + view_type_registry.get_for_class.cache_clear() @pytest.mark.django_db diff --git a/backend/tests/baserow/contrib/database/field/test_formula_field_type.py b/backend/tests/baserow/contrib/database/field/test_formula_field_type.py index 2d09cb6b4d..98057ce9b4 100644 --- a/backend/tests/baserow/contrib/database/field/test_formula_field_type.py +++ b/backend/tests/baserow/contrib/database/field/test_formula_field_type.py @@ -9,6 +9,7 @@ from django.urls import reverse import pytest +from freezegun import freeze_time from rest_framework.status import HTTP_200_OK, HTTP_204_NO_CONTENT from baserow.contrib.database.fields.dependencies.update_collector import ( @@ -2453,3 +2454,84 @@ def test_count_formula_for_link_row_field_with_file_primary_field(data_fixture): ).created_rows[0] assert getattr(row_a, count_formula.db_column) == 2 + + +@pytest.mark.django_db +def test_periodic_update_does_not_crash_on_outer_if_after_inner_invalidated( + data_fixture, +): + """ + Regression test for Sentry issue BASEROW-SAAS-BACKEND-5 / GH #5371. + + When deleting a field referenced by a chain of formulas, the propagation + used to leave the FieldCache populated with the pre-mutation instance of + an inner formula. A later same-cascade re-type of an outer IF formula + then read the stale 'boolean' type from the cache, stayed valid as + 'text', and the next periodic update crashed in BaserowIf with + "When() supports a Q object, a boolean expression, or lookups as a + condition." because the underlying column is generated as TextField + for invalid formulas. + """ + user = data_fixture.create_user() + table = data_fixture.create_database_table(user=user) + + primary = data_fixture.create_text_field( + table=table, name="identifier", primary=True + ) + seed = data_fixture.create_text_field(table=table, name="seed") + FieldHandler().update_field( + user=user, + field=primary, + new_type_name="formula", + formula=f"field('{seed.name}')", + ) + + inner_flag = data_fixture.create_formula_field( + table=table, + name="inner_flag", + formula=f"field('{primary.name}') = 'yes'", + ) + inner_flag.refresh_from_db() + assert inner_flag.formula_type == "boolean" + + with freeze_time("2026-01-01"): + ticker = data_fixture.create_formula_field( + table=table, + name="ticker", + formula="now()", + date_include_time=True, + ) + + outer_icon = data_fixture.create_formula_field( + table=table, + name="outer_icon", + formula=( + f"if(field('{inner_flag.name}'), " + f"datetime_format(field('{ticker.name}'), 'YYYY'), " + f"'no')" + ), + ) + outer_icon.refresh_from_db() + assert outer_icon.formula_type == "text" + + FieldHandler().delete_field(user=user, field=seed) + + fields_for_periodic = list( + FormulaFieldType() + .get_fields_needing_periodic_update() + .filter(table__database__workspace_id=table.database.workspace_id) + ) + assert ticker in fields_for_periodic + + FormulaFieldType().run_periodic_update( + fields_for_periodic, + skip_search_updates=True, + database_id=table.database_id, + ) + + outer_icon_after = FormulaField.objects.get(pk=outer_icon.pk) + assert outer_icon_after.formula_type == "invalid", ( + f"outer IF formula was not re-typed to invalid when inner_flag " + f"became invalid. formula_type={outer_icon_after.formula_type!r}, " + f"internal_formula={outer_icon_after.internal_formula!r}" + ) diff --git a/backend/tests/baserow/contrib/database/field/test_multiple_select_field_type.py b/backend/tests/baserow/contrib/database/field/test_multiple_select_field_type.py index 5d2f84da9b..d18c73daca 100644 --- a/backend/tests/baserow/contrib/database/field/test_multiple_select_field_type.py +++ b/backend/tests/baserow/contrib/database/field/test_multiple_select_field_type.py @@ -2217,25 +2217,20 @@ def test_conversion_to_multiple_select_with_same_option_value_on_same_row( type_name="long_text", ) - row_1 = row_handler.create_row( - user=user, - table=table, - values={ - f"field_{field_1.id}": "test,test", - }, - ) - - row_2 = row_handler.create_row( + row_1, row_2 = row_handler.force_create_rows( user=user, table=table, - values={ - f"field_{field_1.id}": "test, test", - }, - ) + rows_values=[ + {f"field_{field_1.id}": "test,test"}, + {f"field_{field_1.id}": "test, test"}, + ], + ).created_rows unique_values = field_handler.get_unique_row_values( field=field_1, limit=10, split_comma_separated=True ) + assert unique_values == ["test"] + field_handler.update_field( user=user, field=field_1, @@ -2243,23 +2238,18 @@ def test_conversion_to_multiple_select_with_same_option_value_on_same_row( select_options=[{"value": value, "color": "blue"} for value in unique_values], ) - field_type = field_type_registry.get_by_model(field_1) select_options = field_1.select_options.all() id_of_only_select_option = select_options[0].id - assert field_type.type == "multiple_select" assert len(select_options) == 1 model = table.get_model() - rows = model.objects.all() - row_1, row_2 = rows - cell_1 = getattr(row_1, f"field_{field_1.id}").all() - cell_2 = getattr(row_2, f"field_{field_1.id}").all() - - assert len(cell_1) == 1 - assert cell_1[0].id == id_of_only_select_option - - assert len(cell_2) == 1 - assert cell_2[0].id == id_of_only_select_option + row_1, row_2 = model.objects.all() + assert list(getattr(row_1, f"field_{field_1.id}").values_list("id", flat=True)) == [ + id_of_only_select_option + ] + assert list(getattr(row_2, f"field_{field_1.id}").values_list("id", flat=True)) == [ + id_of_only_select_option + ] @pytest.mark.django_db diff --git a/backend/tests/baserow/contrib/database/view/test_view_handler.py b/backend/tests/baserow/contrib/database/view/test_view_handler.py index ced3995518..155fa2dc92 100755 --- a/backend/tests/baserow/contrib/database/view/test_view_handler.py +++ b/backend/tests/baserow/contrib/database/view/test_view_handler.py @@ -94,6 +94,7 @@ def clean_registry_cache(): view_type_registry.get_for_class.cache_clear() yield + field_type_registry.get_for_class.cache_clear() @pytest.mark.django_db diff --git a/changelog/entries/unreleased/bug/5371_fix_periodic_field_update_crash_after_deleting_a_field_refer.json b/changelog/entries/unreleased/bug/5371_fix_periodic_field_update_crash_after_deleting_a_field_refer.json new file mode 100644 index 0000000000..94372d8685 --- /dev/null +++ b/changelog/entries/unreleased/bug/5371_fix_periodic_field_update_crash_after_deleting_a_field_refer.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fix periodic field update crash after deleting a field referenced by a formula chain", + "issue_origin": "github", + "issue_number": 5371, + "domain": "database", + "bullet_points": [], + "created_at": "2026-05-15" +} \ No newline at end of file From 0bda673cac6469222dfc6b68a0aeece1c6e56a7f Mon Sep 17 00:00:00 2001 From: Davide Silvestri <75379892+silvestrid@users.noreply.github.com> Date: Fri, 15 May 2026 15:13:38 +0200 Subject: [PATCH 4/7] fix: handle malformed token in verify_email_address (#5375) * fix: convert BadSignature to InvalidVerificationToken in verify_email_address * fix flaky test --- backend/src/baserow/core/user/handler.py | 5 ++++- .../baserow/contrib/database/field/test_field_tasks.py | 4 +++- backend/tests/baserow/core/user/test_user_handler.py | 6 ++++++ ...er_error_when_submitting_an_invalid_email_verifi.json | 9 +++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 changelog/entries/unreleased/bug/5374_fixes_a_server_error_when_submitting_an_invalid_email_verifi.json diff --git a/backend/src/baserow/core/user/handler.py b/backend/src/baserow/core/user/handler.py index eccff2eb02..7bbfebf625 100755 --- a/backend/src/baserow/core/user/handler.py +++ b/backend/src/baserow/core/user/handler.py @@ -938,7 +938,10 @@ def verify_email_address(self, token: str) -> User: """ signer = self._get_email_verification_signer() - token_data = signer.loads(token) + try: + token_data = signer.loads(token) + except BadSignature as ex: + raise InvalidVerificationToken() from ex if datetime.fromisoformat(token_data["expires_at"]) < datetime.now( tz=timezone.utc diff --git a/backend/tests/baserow/contrib/database/field/test_field_tasks.py b/backend/tests/baserow/contrib/database/field/test_field_tasks.py index 166fc28929..db7f1d0cdf 100644 --- a/backend/tests/baserow/contrib/database/field/test_field_tasks.py +++ b/backend/tests/baserow/contrib/database/field/test_field_tasks.py @@ -363,9 +363,10 @@ def test_all_formula_that_needs_updates_are_periodically_updated(data_fixture): table = data_fixture.create_database_table(database=database) with freeze_time("2023-02-27 10:15"): now_field = data_fixture.create_formula_field( - table=table, formula="now()", date_include_time=True + name="now", table=table, formula="now()", date_include_time=True ) data_fixture.create_formula_field( + name="ref_now", table=table, formula=f"field('{now_field.name}')", date_include_time=True, @@ -373,6 +374,7 @@ def test_all_formula_that_needs_updates_are_periodically_updated(data_fixture): date_field = data_fixture.create_date_field(table=table, date_include_time=True) data_fixture.create_formula_field( + name="now_vs_date", table=table, formula=f"now() > field('{date_field.name}')", date_include_time=True, diff --git a/backend/tests/baserow/core/user/test_user_handler.py b/backend/tests/baserow/core/user/test_user_handler.py index beaf9fe50b..d87a3b6719 100755 --- a/backend/tests/baserow/core/user/test_user_handler.py +++ b/backend/tests/baserow/core/user/test_user_handler.py @@ -780,6 +780,12 @@ def test_verify_email_address_expired(data_fixture): UserHandler().verify_email_address(token) +@pytest.mark.django_db +def test_verify_email_address_malformed_token(): + with pytest.raises(InvalidVerificationToken): + UserHandler().verify_email_address("not-a-real-token") + + @pytest.mark.django_db def test_verify_email_address_user_doesnt_exist(data_fixture): user = data_fixture.create_user() diff --git a/changelog/entries/unreleased/bug/5374_fixes_a_server_error_when_submitting_an_invalid_email_verifi.json b/changelog/entries/unreleased/bug/5374_fixes_a_server_error_when_submitting_an_invalid_email_verifi.json new file mode 100644 index 0000000000..9d3a15cb97 --- /dev/null +++ b/changelog/entries/unreleased/bug/5374_fixes_a_server_error_when_submitting_an_invalid_email_verifi.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fixes a server error when submitting an invalid email verification token", + "issue_origin": "github", + "issue_number": 5374, + "domain": "core", + "bullet_points": [], + "created_at": "2026-05-15" +} \ No newline at end of file From b66235f4ae7ed2f84137313b9e6d7fe09badf843 Mon Sep 17 00:00:00 2001 From: Davide Silvestri <75379892+silvestrid@users.noreply.github.com> Date: Fri, 15 May 2026 15:36:52 +0200 Subject: [PATCH 5/7] fix: guard guided tour highlight against missing target elements (#5378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: guard guided tour highlight against missing target elements Resolves #5377 * fix: hide highlight overlay when no target elements match Toggle a separate `visible` flag instead of leaving `selector` set when no elements match. Otherwise the .highlight div stays rendered with its 5000px dimming box-shadow over an empty 0×0 box, dimming the whole UI without showing a target. * fix: keep guided tour controls mounted when highlight has no target Falling back to the centred position (the same path empty-selector steps use) instead of unmounting the highlight root keeps the slot — including GuidedTourStep's Next/Finish controls — visible, so users can always advance or finish the tour. --- ...r_crash_when_the_highlighted_element_is_no.json | 9 +++++++++ web-frontend/modules/core/components/Highlight.vue | 14 +++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 changelog/entries/unreleased/bug/fixes_a_guided_tour_crash_when_the_highlighted_element_is_no.json diff --git a/changelog/entries/unreleased/bug/fixes_a_guided_tour_crash_when_the_highlighted_element_is_no.json b/changelog/entries/unreleased/bug/fixes_a_guided_tour_crash_when_the_highlighted_element_is_no.json new file mode 100644 index 0000000000..d308c1c00c --- /dev/null +++ b/changelog/entries/unreleased/bug/fixes_a_guided_tour_crash_when_the_highlighted_element_is_no.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fixes a guided tour crash when the highlighted element is not on the page", + "issue_origin": "github", + "issue_number": 5377, + "domain": "core", + "bullet_points": [], + "created_at": "2026-05-15" +} \ No newline at end of file diff --git a/web-frontend/modules/core/components/Highlight.vue b/web-frontend/modules/core/components/Highlight.vue index 33ac318114..49f278e9dc 100644 --- a/web-frontend/modules/core/components/Highlight.vue +++ b/web-frontend/modules/core/components/Highlight.vue @@ -1,6 +1,6 @@