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
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
from collections import defaultdict
from typing import Dict, List, Optional, Set, Tuple, cast

Expand All @@ -16,6 +17,20 @@
StartingRowIdsType = Optional[List[int]]


@dataclasses.dataclass
class DependencyContext:
"""
DependencyContext is used to pass additional dependency-related information
to callbacks.
"""

# The depth of the dependency chain from the starting
# field to the field parameter. 0 means the field is a direct dependency of
# the updated row's field. 1 means the field depends on a field which depends
# on the updated row's field, etc.
depth: int = 0


class PathBasedUpdateStatementCollector:
def __init__(
self,
Expand Down
6 changes: 5 additions & 1 deletion backend/src/baserow/contrib/database/fields/field_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@
from .dependencies.handler import FieldDependants, FieldDependencyHandler
from .dependencies.models import FieldDependency
from .dependencies.types import FieldDependencies
from .dependencies.update_collector import FieldUpdateCollector
from .dependencies.update_collector import DependencyContext, FieldUpdateCollector
from .exceptions import (
AllProvidedCollaboratorIdsMustBeValidUsers,
AllProvidedMultipleSelectValuesMustBeSelectOption,
Expand Down Expand Up @@ -3582,6 +3582,7 @@ def row_of_dependency_updated(
update_collector: FieldUpdateCollector,
field_cache: "FieldCache",
via_path_to_starting_table: List["LinkRowField"],
dependency_context: DependencyContext,
):
update_collector.add_field_which_has_changed(
field, via_path_to_starting_table, send_field_updated_signal=False
Expand All @@ -3592,6 +3593,7 @@ def row_of_dependency_updated(
update_collector,
field_cache,
via_path_to_starting_table,
dependency_context,
)

def field_dependency_updated(
Expand Down Expand Up @@ -5697,6 +5699,7 @@ def row_of_dependency_updated(
update_collector: FieldUpdateCollector,
field_cache: "FieldCache",
via_path_to_starting_table: Optional[List[LinkRowField]],
dependency_context: DependencyContext,
):
self._update_field_values(
field, update_collector, field_cache, via_path_to_starting_table
Expand All @@ -5708,6 +5711,7 @@ def row_of_dependency_updated(
update_collector,
field_cache,
via_path_to_starting_table,
dependency_context,
)

def _update_field_values(
Expand Down
8 changes: 8 additions & 0 deletions backend/src/baserow/contrib/database/fields/registries.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
from baserow.contrib.database.fields.dependencies.handler import FieldDependants
from baserow.contrib.database.fields.dependencies.types import FieldDependencies
from baserow.contrib.database.fields.dependencies.update_collector import (
DependencyContext,
FieldUpdateCollector,
)
from baserow.contrib.database.fields.field_cache import FieldCache
Expand Down Expand Up @@ -1446,6 +1447,7 @@ def row_of_dependency_created(
update_collector: "FieldUpdateCollector",
field_cache: "FieldCache",
via_path_to_starting_table: Optional[List[LinkRowField]],
dependency_context: "DependencyContext",
):
"""
Called when a row is created in a dependency field (a field that the field
Expand All @@ -1469,6 +1471,7 @@ def row_of_dependency_created(
update_collector,
field_cache,
via_path_to_starting_table,
dependency_context,
)

def row_of_dependency_updated(
Expand All @@ -1478,6 +1481,7 @@ def row_of_dependency_updated(
update_collector: "FieldUpdateCollector",
field_cache: "FieldCache",
via_path_to_starting_table: List["LinkRowField"],
dependency_context: "DependencyContext",
):
"""
Called when a row or rows are updated in a dependency field (a field that the
Expand All @@ -1495,6 +1499,8 @@ def row_of_dependency_updated(
:param field_cache: An optional field cache to be used when fetching fields.
:param via_path_to_starting_table: A list of link row fields if any leading
back to the starting table where the first row was changed.
:param dependency_context: A DependencyContext object containing additional
information about the dependency and the triggering change.
"""

def row_of_dependency_deleted(
Expand All @@ -1504,6 +1510,7 @@ def row_of_dependency_deleted(
update_collector: "FieldUpdateCollector",
field_cache: "FieldCache",
via_path_to_starting_table: Optional[List[LinkRowField]],
dependency_context: "DependencyContext",
):
"""
Called when a row is deleted in a dependency field (a field that the
Expand All @@ -1527,6 +1534,7 @@ def row_of_dependency_deleted(
update_collector,
field_cache,
via_path_to_starting_table,
dependency_context,
)

def field_dependency_created(
Expand Down
26 changes: 22 additions & 4 deletions backend/src/baserow/contrib/database/rows/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from baserow.contrib.database.field_rules.handlers import FieldRuleHandler
from baserow.contrib.database.fields.dependencies.handler import FieldDependencyHandler
from baserow.contrib.database.fields.dependencies.update_collector import (
DependencyContext,
FieldUpdateCollector,
)
from baserow.contrib.database.fields.exceptions import (
Expand Down Expand Up @@ -1154,7 +1155,10 @@ def update_dependencies_of_rows_updated(
deleted_m2m_rels_per_link_field=deleted_m2m_rels_per_link_field,
)
updated_fields = []
for dependant_fields_group in all_dependent_fields_grouped_by_depth:
for depth, dependant_fields_group in enumerate(
all_dependent_fields_grouped_by_depth
):
dependency_context = DependencyContext(depth=depth)
for (
dependant_field,
dependant_field_type,
Expand All @@ -1167,6 +1171,7 @@ def update_dependencies_of_rows_updated(
update_collector,
field_cache,
path_to_starting_table,
dependency_context,
)
update_collector.apply_updates_and_get_updated_fields(
field_cache, skip_search_updates
Expand Down Expand Up @@ -1486,19 +1491,24 @@ def update_dependencies_of_rows_created(
)
)

for dependant_fields_group in all_dependent_fields_grouped_by_depth:
for depth, dependant_fields_group in enumerate(
all_dependent_fields_grouped_by_depth
):
dependency_context = DependencyContext(depth=depth)
for (
dependant_field,
dependant_field_type,
path_to_starting_table,
) in dependant_fields_group:
dependant_fields.append(dependant_field)

dependant_field_type.row_of_dependency_created(
dependant_field,
created_rows,
update_collector,
field_cache,
path_to_starting_table,
dependency_context,
)
update_collector.apply_updates_and_get_updated_fields(field_cache)
return fields, dependant_fields
Expand Down Expand Up @@ -2754,7 +2764,10 @@ def update_dependencies_of_rows_deleted(self, table, row, model):
)
)

for dependent_fields_level in all_dependent_fields_grouped_by_level:
for depth, dependent_fields_level in enumerate(
all_dependent_fields_grouped_by_level
):
dependency_context = DependencyContext(depth=depth)
for (
dependant_field,
dependant_field_type,
Expand All @@ -2768,6 +2781,7 @@ def update_dependencies_of_rows_deleted(self, table, row, model):
update_collector,
field_cache,
path_to_starting_table,
dependency_context,
)

update_collector.apply_updates_and_get_updated_fields(field_cache)
Expand Down Expand Up @@ -2910,7 +2924,10 @@ def force_delete_rows(
)
)

for dependent_fields_level in all_dependent_fields_grouped_by_level:
for depth, dependent_fields_level in enumerate(
all_dependent_fields_grouped_by_level
):
dependency_context = DependencyContext(depth=depth)
for (
table_id,
dependant_field,
Expand All @@ -2924,6 +2941,7 @@ def force_delete_rows(
update_collector,
field_cache,
path_to_starting_table,
dependency_context,
)
update_collector.apply_updates_and_get_updated_fields(field_cache)

Expand Down
5 changes: 4 additions & 1 deletion backend/src/baserow/test_utils/pytest_conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ def fake():
# This solution is taken from: https://bit.ly/3UJ90co
@pytest.fixture(scope="session")
def async_event_loop():
loop = asyncio.get_event_loop()
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
yield loop
loop.close()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from rest_framework.status import HTTP_200_OK, HTTP_204_NO_CONTENT

from baserow.contrib.database.fields.dependencies.update_collector import (
DependencyContext,
FieldUpdateCollector,
)
from baserow.contrib.database.fields.field_cache import FieldCache
Expand Down Expand Up @@ -864,23 +865,24 @@ def test_row_dependency_update_functions_do_one_row_updates_for_same_table(
field_cache = FieldCache()
field_cache.cache_model(table_model)

dependency_context = DependencyContext(depth=0)
formula_field_type.row_of_dependency_updated(
formula_field, row, update_collector, field_cache, None
formula_field, row, update_collector, field_cache, None, dependency_context
)
formula_field_type.row_of_dependency_updated(
formula_field, row, update_collector, field_cache, []
formula_field, row, update_collector, field_cache, [], dependency_context
)
formula_field_type.row_of_dependency_created(
formula_field, row, update_collector, field_cache, None
formula_field, row, update_collector, field_cache, None, dependency_context
)
formula_field_type.row_of_dependency_created(
formula_field, row, update_collector, field_cache, []
formula_field, row, update_collector, field_cache, [], dependency_context
)
formula_field_type.row_of_dependency_deleted(
formula_field, row, update_collector, field_cache, None
formula_field, row, update_collector, field_cache, None, dependency_context
)
formula_field_type.row_of_dependency_deleted(
formula_field, row, update_collector, field_cache, []
formula_field, row, update_collector, field_cache, [], dependency_context
)
# Does one update to update the last_system_or_user_row_update_on column
with django_assert_num_queries(1):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "feature",
"message": "AI field auto-update",
"domain": "database",
"issue_number": 4115,
"bullet_points": [],
"created_at": "2025-11-04"
}
24 changes: 1 addition & 23 deletions enterprise/backend/src/baserow_enterprise/api/assistant/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from django.http import StreamingHttpResponse

from baserow_premium.license.handler import LicenseHandler
from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from loguru import logger
Expand Down Expand Up @@ -39,7 +38,6 @@
HumanMessage,
UIContext,
)
from baserow_enterprise.features import ASSISTANT

from .errors import (
ERROR_ASSISTANT_CHAT_DOES_NOT_EXIST,
Expand All @@ -62,7 +60,6 @@ class AssistantChatsView(APIView):
operation_id="list_assistant_chats",
description=(
"List all AI assistant chats for the current user in the specified workspace."
"\n\nThis is a **advanced/enterprise** feature."
),
parameters=[
OpenApiParameter(
Expand Down Expand Up @@ -104,10 +101,6 @@ def get(self, request: Request, query_params) -> Response:
workspace_id = query_params["workspace_id"]
workspace = CoreHandler().get_workspace(workspace_id)

LicenseHandler.raise_if_user_doesnt_have_feature(
ASSISTANT, request.user, workspace
)

CoreHandler().check_permissions(
request.user,
ChatAssistantChatOperationType.type,
Expand All @@ -132,7 +125,6 @@ class AssistantChatView(APIView):
operation_id="send_message_to_assistant_chat",
description=(
"Send a message to the specified AI assistant chat and stream back the response.\n\n"
"This is an **advanced/enterprise** feature."
),
request=AssistantMessageRequestSerializer,
responses={
Expand All @@ -157,9 +149,6 @@ def post(self, request: Request, chat_uuid: str, data) -> StreamingHttpResponse:
ui_context = UIContext.from_validate_request(request, data["ui_context"])
workspace_id = ui_context.workspace.id
workspace = CoreHandler().get_workspace(workspace_id)
LicenseHandler.raise_if_user_doesnt_have_feature(
ASSISTANT, request.user, workspace
)
CoreHandler().check_permissions(
request.user,
ChatAssistantChatOperationType.type,
Expand Down Expand Up @@ -216,10 +205,7 @@ def _stream_assistant_message(self, message: AssistantMessageUnion) -> str:
@extend_schema(
tags=["AI Assistant"],
operation_id="list_assistant_chat_messages",
description=(
"List all messages in the specified AI assistant chat.\n\n"
"This is an **advanced/enterprise** feature."
),
description=("List all messages in the specified AI assistant chat.\n\n"),
responses={
200: AssistantChatMessagesSerializer,
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
Expand All @@ -239,9 +225,6 @@ def get(self, request: Request, chat_uuid: str) -> Response:
chat = handler.get_chat(request.user, chat_uuid)

workspace = chat.workspace
LicenseHandler.raise_if_user_doesnt_have_feature(
ASSISTANT, request.user, workspace
)
CoreHandler().check_permissions(
request.user,
ChatAssistantChatOperationType.type,
Expand All @@ -263,7 +246,6 @@ class AssistantChatMessageFeedbackView(APIView):
operation_id="submit_assistant_message_feedback",
description=(
"Provide sentiment and feedback for the given AI assistant chat message.\n\n"
"This is an **advanced/enterprise** feature."
),
responses={
200: None,
Expand All @@ -286,10 +268,6 @@ def put(self, request: Request, message_id: int, data) -> Response:

handler = AssistantHandler()
message = handler.get_chat_message_by_id(request.user, message_id)
LicenseHandler.raise_if_user_doesnt_have_feature(
ASSISTANT, request.user, message.chat.workspace
)

try:
prediction: AssistantChatPrediction = message.prediction
except AttributeError:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ def create_views(
table_id=table.id,
view_id=created_views[0]["id"],
view_name=created_views[0]["name"],
view_type=created_views[0]["type"],
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ class ViewNavigationType(BaseModel):
table_id: int
view_id: int
view_name: str
view_type: str

def to_localized_string(self):
return _("view %(view_name)s") % {"view_name": self.view_name}
Expand Down
1 change: 0 additions & 1 deletion enterprise/backend/src/baserow_enterprise/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
DATA_SYNC = "data_sync"
ADVANCED_WEBHOOKS = "advanced_webhooks"
FIELD_LEVEL_PERMISSIONS = "field_level_permissions"
ASSISTANT = "assistant"

BUILDER_SSO = "application_user_sso"
BUILDER_NO_BRANDING = "application_no_branding"
Expand Down
Loading
Loading