Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/src/baserow/api/admin/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ class BaserowImpersonateAuthTokenSerializer(serializers.Serializer):
"""

User = get_user_model()
# It's not allowed to impersonate a superuser or staff.
# It's not allowed to impersonate a superuser, staff or deactivated user.
user = serializers.PrimaryKeyRelatedField(
queryset=User.objects.filter(is_superuser=False, is_staff=False)
queryset=User.objects.filter(is_superuser=False, is_staff=False, is_active=True)
)

class Meta:
Expand Down
3 changes: 2 additions & 1 deletion backend/src/baserow/api/admin/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,8 @@ class UserAdminImpersonateView(GenericAPIView):
description=(
"This endpoint allows staff to impersonate another user by requesting a "
"JWT token and user object. The requesting user must have staff access in "
"order to do this. It's not possible to impersonate a superuser or staff."
"order to do this. It's not possible to impersonate a superuser, staff "
"or deactivated user."
),
request=BaserowImpersonateAuthTokenSerializer,
responses={
Expand Down
26 changes: 26 additions & 0 deletions backend/tests/baserow/api/admin/users/test_users_admin_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,32 @@ def test_admin_impersonate_staff_or_superuser(api_client, data_fixture):
assert response_json["user"][0].startswith("Invalid pk")


@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_impersonate_deactivated_user(api_client, data_fixture):
_, token = data_fixture.create_user_and_token(
email="test@test.nl",
password="password",
first_name="Test1",
is_staff=True,
)
user_to_impersonate = data_fixture.create_user(
email="specific_user@test.nl",
password="password",
first_name="Test1",
is_active=False,
)
response = api_client.post(
reverse("api:admin:users:impersonate"),
{"user": user_to_impersonate.id},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
response_json = response.json()
assert response_json["user"][0].startswith("Invalid pk")


@pytest.mark.django_db
@override_settings(DEBUG=True)
def test_admin_users_endpoint_includes_two_factor_auth(api_client, data_fixture):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "bug",
"message": "Fix link row field modal unselecting newly created rows due to a race condition with real-time updates",
"issue_origin": "github",
"issue_number": 5735,
"domain": "database",
"bullet_points": [],
"created_at": "2026-07-17"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "bug",
"message": "Prevent impersonating deactivated users via the admin impersonate endpoint",
"issue_origin": "github",
"issue_number": null,
"domain": "core",
"bullet_points": [],
"created_at": "2026-07-20"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "refactor",
"message": "Updated 'formula' references in builder, automation & integrations module so that they now refer to 'expressions'.",
"issue_origin": "github",
"issue_number": null,
"domain": "builder",
"bullet_points": [],
"created_at": "2026-04-23"
}
73 changes: 73 additions & 0 deletions docs/decisions/005-formulas-vs-expressions-terminology.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Distinguishing "Formulas" and "Expressions" in Baserow

## Summary

Baserow uses the word "formula" across two fundamentally different evaluation systems.
This decision splits the terminology: **Formula** is retained for the database engine,
and **Expression** replaces "formula" everywhere it refers to the runtime-evaluated
engine used in Builder, Automation, and the AI Field. This is a UX and documentation
change — the underlying code and data model are not renamed.

Originates from a dev retro suggestion in February 2026.

## The Problem

Both systems are surfaced to users as "formulas" today, but they behave very
differently:

- **Different function sets.** The database formula engine exposes functions that map to
SQL and Django ORM expressions. The runtime engine exposes a different set suited to
dynamic data resolution (`get`, `concat`, data source accessors, etc.). Users report
confusion when a function they know from one context doesn't exist or behaves
differently in another.

- **Different evaluation models.** Database formulas are compiled into SQL and evaluated
by the database at query time. Runtime expressions can be evaluated client-side (for
simple, data-independent expressions) or server-side during automation or AI prompt
resolution. Calling both systems "formulas" obscures this distinction.

- **Support and documentation burden.** Without a shared vocabulary, support staff,
documentation authors, and engineers cannot easily triage whether a problem is with
the database formula engine or the runtime expression engine.

## The Proposed Solution

### Terminology split

| System | Term | Where it appears |
|---|---|---|
| Database `FormulaField` engine | **Formula** | Database module, formula editor |
| Runtime-evaluated engine | **Expression** | Builder, Automation, AI Field |

**Formulas** are persistent, computed columns defined on a `FormulaField`. They are
parsed and compiled at save time, evaluated by the database at query time, and have
their own type system, validation, and dependency graph. The editor in the Database
module continues to be called the *Formula editor*.

**Expressions** are inline dynamic values accepted by element configuration forms,
automation node forms, and the AI Field prompt editor. They are evaluated at runtime —
either in the browser or on the server — and use a different function registry from the
database formula engine. The editor for these becomes the *Expression editor*.

### Scope of changes

1. **UI labels** — Rename "Formula" to "Expression" in Builder element forms,
automation node forms, and the AI Field prompt editor. Update tooltips, placeholder
text, and error messages in those contexts. The Database `FormulaField` UI is
unchanged.

2. **Documentation** — Audit and update all help articles that describe the Builder or
Automation engine to use "expression". Add a disambiguation article: *Formulas vs.
Expressions — what's the difference?*

3. **Translations (Weblate)** — Language maintainers will need to agree on the best
translation for "expression" in each locale.

4. **Internal alignment** — Update technical documentation and code comments. Agree on
a cut-off date so engineering and support adopt the new terms together.

### What this does not change

- The underlying code and data model are not renamed.
- The function sets are not merged or altered.
- Existing user data (saved formulas/expressions) is unaffected.
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@
"codePlaceholder": "Enter the code to execute",
"editCode": "Edit code",
"injectionsLabel": "Data injections",
"injectionsHelperText": "Injections are formulas evaluated before the code runs. Each injection name becomes a property of context. For example an injection named customerName is available as context.customerName.",
"injectionsHelperText": "Injections are expressions evaluated before the code runs. Each injection name becomes a property of context. For example an injection named customerName is available as context.customerName.",
"nameLabel": "Name",
"namePlaceholder": "Variable name",
"valueLabel": "Value",
"valuePlaceholder": "Value formula",
"valuePlaceholder": "Value expression",
"addInjection": "Add injection",
"nameFieldRequired": "The name field is required.",
"nameFieldInvalid": "The name must be a valid variable name."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ const nodesHierarchy = computed(() => {
})

/**
* Extract the formula string from the value object, the FormulaInputField
* component only needs the formula string itself.
* @returns {String} The formula string.
* Extract the expression string from the value object, the FormulaInputField
* component only needs the expression string itself.
* @returns {String} The expression string.
*/
const formulaStr = computed(() => {
return currentValue.value.formula
Expand Down Expand Up @@ -142,9 +142,9 @@ const dataExplorerLoading = computed(() => {
})

/**
* When `FormulaInputField` emits a new formula string, we need to emit the
* entire value object with the updated formula string.
* @param {String} newFormulaStr The new formula string.
* When `FormulaInputField` emits a new expression string, we need to emit the
* entire value object with the updated expression string.
* @param {String} newFormulaStr The new expression string.
*/
const updatedFormulaStr = (newFormulaStr) => {
emit('input', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ export default {
* @property {boolean} show_as_dropdown - If the choice element should be displayed as a dropdown
* @property {Array} options - The options of the choice element
* @property {string} option_type - The type of the options
* @property {string} formula_name - The formula for the name of the option
* @property {string} formula_value - The formula for the value of the option
* @property {string} formula_name - The expression for the name of the option
* @property {string} formula_value - The expression for the value of the option
*/
element: {
type: Object,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ export default {
* @property {string} data_source_id - The data source for the record selector element
* @property {number} items_per_page - Number of items to show per page
* @property {string} label - The label displayed above the record selector element
* @property {string} default_value - The formula to generate the displayed name
* @property {string} default_value - The expression to generate the displayed name
* @property {string} placeholder - The placeholder text which should be applied to the element
* @property {boolean} multiple - Whether this element can hold multiple values
* @property {string} option_name_suffix - The formula to generate the displayed suffix name
* @property {string} option_name_suffix - The expression to generate the displayed suffix name
*/
element: {
type: Object,
Expand Down Expand Up @@ -145,7 +145,7 @@ export default {
},
resolvedOptions() {
// Fill the dropdown options with an array containing
// the record id and name from the resolved formula
// the record id and name from the resolved expression
const options = this.elementContent.map((record, recordIndex) => ({
value: record?.id,
nameSuffix: ensureString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export default {
icon: 'iconoir-open-select-hand-gesture',
},
{
label: this.$t('choiceOptionSelector.formulas'),
label: this.$t('choiceOptionSelector.expressions'),
value: CHOICE_OPTION_TYPES.FORMULAS,
icon: 'iconoir-sigma-function',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
</FormGroup>
<!--
We use a key here otherwise Vuejs reuse the components of the following
formula input with the old application context when the v-if becomes true.
Therefore the "allowSameElement" value is wrong showing invalid formula when they
expression input with the old application context when the v-if becomes true.
Therefore the "allowSameElement" value is wrong showing invalid expression when they
are valid. We could also solve this by using a v-show instead of a v-if.
-->
<FormGroup
Expand Down Expand Up @@ -208,7 +208,7 @@ export default {
handler(value) {
this.values.data_source_id = value

// If the data source was removed we should also delete the name formula
// If the data source was removed we should also delete the name expression
if (value === null) {
this.values.option_name_suffix = ''
}
Expand Down
2 changes: 1 addition & 1 deletion web-frontend/modules/builder/components/event/Event.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
This value will change after an action is ordered, thus triggering the
rendering engine, which can be useful when we want instant visual
feedback.
One example would be to highlight formulas that become invalid after
One example would be to highlight expressions that become invalid after
action ordering.
-->
<WorkflowAction
Expand Down
9 changes: 4 additions & 5 deletions web-frontend/modules/builder/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,7 @@
},
"elementForms": {
"textInputPlaceholder": "Enter text...",
"urlInputPlaceholder": "Enter url...",
"invalidFormula": "The formula is invalid"
"urlInputPlaceholder": "Enter url..."
},
"headingElement": {
"missingValue": "Missing title...",
Expand Down Expand Up @@ -732,7 +731,7 @@
"submitLabel": "On submit",
"afterLoginLabel": "After login"
},
"getFormulaComponent": {
"getExpressionComponent": {
"errorTooltip": "Invalid reference"
},
"fontSidePanelForm": {
Expand Down Expand Up @@ -928,7 +927,7 @@
"optionType": "Options type",
"manual": "Manual",
"dataSource": "Data source",
"formulas": "Formulas"
"expressions": "Expressions"
},
"fieldMappingContext": {
"enableField": "Enable field",
Expand Down Expand Up @@ -975,7 +974,7 @@
"errorFetchingRolesTitle": "Could not fetch User Roles",
"errorFetchingRolesMessage": "There was a problem while fetching User Roles.",
"visibilityCondition": "Visibility condition",
"visibilityConditionHelper": "If the result of this formula is true, and the visitor choice above is true, the element will be visible. This condition only affects the element’s visibility. To exclude data from the server response instead, use the user role filtering option above.",
"visibilityConditionHelper": "If the result of this expression is true, and the visitor choice above is true, the element will be visible. This condition only affects the element’s visibility. To exclude data from the server response instead, use the user role filtering option above.",
"visibilityConditionPlaceholder": "Condition..."
},
"userDataProviderType": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
}"
>
<div
v-tooltip="$t('getFormulaComponent.errorTooltip')"
v-tooltip="$t('getExpressionComponent.errorTooltip')"
tooltip-position="top"
:hide-tooltip="!isInvalid"
@click.stop="emitToEditor('data-node-clicked', node)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,14 @@ export default {
{},
'page/'
)
this.select(populateRow(rowCreated))
const populated = populateRow(rowCreated)
if (!this.selectedRows.includes(populated.id)) {
this.$emit('selected', {
row: populated,
primary: this.primary,
fields: this.fields,
})
}
}
},
toggleFieldsContext() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
class="filters__value--formula-input"
:placeholder="
$t(
'localBaserowTableServiceConditionalForm.formulaFilterInputPlaceholder'
'localBaserowTableServiceConditionalForm.expressionFilterInputPlaceholder'
)
"
@input="
Expand All @@ -58,7 +58,9 @@
"
:title="
!filter.value_is_formula
? $t('localBaserowTableServiceConditionalForm.useFormulaForValue')
? $t(
'localBaserowTableServiceConditionalForm.useExpressionForValue'
)
: $t('localBaserowTableServiceConditionalForm.useDefaultForValue')
"
class="filters__value--formula-toggle"
Expand Down
9 changes: 4 additions & 5 deletions web-frontend/modules/integrations/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@
},
"localBaserowGetRowForm": {
"rowFieldLabel": "Row ID",
"invalidFormula": "The formula is invalid",
"rowFieldPlaceHolder": "Choose a single row ID",
"rowFieldHelpText": "Leave this value empty to return the first row.",
"searchFieldPlaceHolder": "Enter a search term...",
Expand Down Expand Up @@ -179,8 +178,8 @@
"noCompatibleFilterTypesErrorTitle": "No compatible filter types",
"noCompatibleFilterTypesErrorMessage": "None of your fields have any compatible filter types",
"textFilterInputPlaceholder": "Enter text...",
"formulaFilterInputPlaceholder": "Choose a formula...",
"useFormulaForValue": "Use a formula for this filter",
"expressionFilterInputPlaceholder": "Choose an expression...",
"useExpressionForValue": "Use an expression for this filter",
"useDefaultForValue": "Use the default filter for this field"
},
"localBaserowTableServiceSortForm": {
Expand Down Expand Up @@ -383,11 +382,11 @@
"localBaserowCreateRowsServiceForm": {
"rowsLabel": "Rows",
"rowsPlaceholder": "Choose a list of row objects",
"rowsHelper": "The formula must return an array of row objects, or a JSON string that parses to one. Use field names or field IDs as object keys. Up to {limit} rows can be created at once."
"rowsHelper": "The expression must return an array of row objects, or a JSON string that parses to one. Use field names or field IDs as object keys. Up to {limit} rows can be created at once."
},
"localBaserowUpdateRowsServiceForm": {
"rowsLabel": "Rows",
"rowsPlaceholder": "Choose a list of row objects with IDs",
"rowsHelper": "The formula must return an array of row objects, or a JSON string that parses to one. Each object must include an id property. Use field names or field IDs as object keys. Up to {limit} rows can be updated at once."
"rowsHelper": "The expression must return an array of row objects, or a JSON string that parses to one. Each object must include an id property. Use field names or field IDs as object keys. Up to {limit} rows can be updated at once."
}
}
Loading
Loading