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
18 changes: 17 additions & 1 deletion backend/src/baserow/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

from baserow.core.context import clear_current_workspace_id, set_current_workspace_id
from baserow.core.storage import get_default_storage
from baserow.core.utils import split_comma_separated_string
from baserow.core.utils import (
list_to_comma_separated_string,
split_comma_separated_string,
)


class PrefetchedManyToManyListSerializer(serializers.ListSerializer):
Expand Down Expand Up @@ -135,6 +138,19 @@ def to_internal_value(self, data):
return record_ids


class CommaSeparatedValuesField(serializers.Field):
"""A serializer field that accepts a CSV string containing a list of values."""

def to_representation(self, value):
return list_to_comma_separated_string(value)

def to_internal_value(self, data):
try:
return split_comma_separated_string(data)
except ValueError as e:
raise serializers.ValidationError(str(e), code="invalid") from e


class FileURLSerializerMixin(serializers.Serializer):
url = serializers.SerializerMethodField()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(
use_sample_data=bool(self.simulate_until_node),
force_outputs=force_outputs,
event_payload=event_payload,
workspace=workflow.get_original().automation.workspace,
)

def clone(self, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from baserow.api.serializers import CommaSeparatedIntegerValuesField
from baserow.api.serializers import CommaSeparatedValuesField
from baserow.api.services.serializers import (
CreateServiceSerializer,
ServiceSerializer,
Expand Down Expand Up @@ -163,7 +163,7 @@ class MoveDataSourceSerializer(serializers.Serializer):


class GetRecordIdsSerializer(serializers.Serializer):
record_ids = CommaSeparatedIntegerValuesField()
record_ids = CommaSeparatedValuesField()


class DynamicMetadataSerializer(serializers.Serializer):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,9 @@ def get(self, request, data_source_id: int):
query = GetRecordIdsSerializer(data=request.query_params)

if query.is_valid(raise_exception=True):
record_ids = query.validated_data["record_ids"]
record_ids = service_type.prepare_record_ids(
query.validated_data["record_ids"]
)
record_names = service_type.get_record_names(
service, record_ids, dispatch_context
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class BuilderDispatchContext(DispatchContext):
"count",
"only_record_id",
"only_expose_public_allowed_properties",
"workspace",
]

def __init__(
Expand Down Expand Up @@ -79,6 +80,8 @@ def __init__(
only_expose_public_allowed_properties
)

kwargs["workspace"] = page.builder.get_workspace()

super().__init__(**kwargs)

# Early call to quickly trigger a validation error
Expand Down
5 changes: 4 additions & 1 deletion backend/src/baserow/contrib/builder/data_sources/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,8 +558,11 @@ def dispatch_data_source(
cloned_dispatch_context.add_call(data_source.id)

if data_source.id not in cache.setdefault("data_source_contents", {}):
service = data_source.service.specific
service.get_type().raise_if_deactivated(dispatch_context.workspace)

service_dispatch = self.service_handler.dispatch_service(
data_source.service.specific, cloned_dispatch_context
service, cloned_dispatch_context
)

# Cache the dispatch in the formula cache if we have formulas that need
Expand Down
13 changes: 3 additions & 10 deletions backend/src/baserow/contrib/builder/data_sources/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,6 @@ class DataSourceService:
def __init__(self):
self.handler = DataSourceHandler()

def _raise_if_service_type_deactivated(
self, service_type: ServiceType, page: Page
) -> None:
raise_if_deactivated = getattr(service_type, "raise_if_deactivated", None)
if raise_if_deactivated:
raise_if_deactivated(page.builder.workspace)

def get_data_source(self, user: AbstractUser, data_source_id: int) -> DataSource:
"""
Returns an data_source instance from the database. Also checks the user
Expand Down Expand Up @@ -166,7 +159,7 @@ def create_data_source(
# Verify the `service_type` is dispatch-able as DATA.
if not service_type.can_be_dispatched_as(DispatchTypes.DATA):
raise InvalidServiceTypeDispatchSource()
self._raise_if_service_type_deactivated(service_type, page)
service_type.raise_if_deactivated(page.builder.workspace)
prepared_values = service_type.prepare_values(kwargs, user)
else:
prepared_values = kwargs
Expand Down Expand Up @@ -255,8 +248,8 @@ def update_data_source(
service_type_for_preparation = new_service_type

if service_type_for_preparation:
self._raise_if_service_type_deactivated(
service_type_for_preparation, page or data_source.page
service_type_for_preparation.raise_if_deactivated(
(page or data_source.page).builder.workspace
)
service = data_source.service.specific if data_source.service_id else None
prepared_values = service_type_for_preparation.prepare_values(
Expand Down
16 changes: 10 additions & 6 deletions backend/src/baserow/contrib/builder/elements/element_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@
from baserow.core.formula.validator import (
ensure_array,
ensure_boolean,
ensure_integer,
ensure_numeric,
ensure_string_or_integer,
)
Expand Down Expand Up @@ -781,17 +780,22 @@ def is_valid(
service_type = service.get_type()

try:
record_ids = set(map(ensure_integer, ensure_array(value)))
raw_record_ids = (
ensure_array(value)
if element.multiple
else ([] if value is None or value == "" else [value])
)
record_ids = set(service_type.prepare_record_ids(raw_record_ids))
record_names = service_type.get_record_names(
service.specific,
record_ids,
dispatch_context,
)
available_record_ids = set(record_names.keys())
except ValidationError as err:
except (ValidationError, DRFValidationError) as err:
msg = (
"The value must be an array of integers, or convertible to an"
"array of integers"
"The value must be an array of valid record identifiers, or "
"convertible to an array of valid record identifiers"
)
raise TypeError(msg) from err

Expand All @@ -804,7 +808,7 @@ def is_valid(
msg = f"{value} is not a valid option"
raise ValueError(msg)
else:
record_id = value
record_id = next(iter(record_ids), None)

if not record_id:
if element.required:
Expand Down
2 changes: 1 addition & 1 deletion backend/src/baserow/contrib/builder/elements/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ def prepare_value_for_db(
else:
max_count = 20

if values["items_per_page"] > max_count:
if max_count is not None and values["items_per_page"] > max_count:
raise RequestBodyValidationException(
{
"items_per_page": [
Expand Down
10 changes: 8 additions & 2 deletions backend/src/baserow/contrib/dashboard/api/data_sources/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
DiscriminatorCustomFieldsMappingSerializer,
)
from baserow.contrib.dashboard.api.errors import ERROR_DASHBOARD_DOES_NOT_EXIST
from baserow.contrib.dashboard.application_types import DashboardApplicationType
from baserow.contrib.dashboard.data_sources.actions import (
UpdateDashboardDataSourceActionType,
)
Expand Down Expand Up @@ -183,6 +184,7 @@ def patch(self, request, data_source_id: int):
service_type_registry,
request.data,
base_serializer_class=UpdateDashboardDataSourceSerializer,
serializer_class_context={"application_type": DashboardApplicationType},
return_validated=True,
)

Expand Down Expand Up @@ -245,8 +247,12 @@ def post(self, request, data_source_id: int):
Call the given data source related service dispatch method.
"""

dispatch_context = DashboardDispatchContext(request)
response = DashboardDataSourceService().dispatch_data_source(
data_source_service = DashboardDataSourceService()
data_source = data_source_service.handler.get_data_source(data_source_id)
dispatch_context = DashboardDispatchContext(
request, data_source.dashboard.workspace
)
response = data_source_service.dispatch_data_source(
request.user, data_source_id, dispatch_context
)
return Response(response)
3 changes: 3 additions & 0 deletions backend/src/baserow/contrib/dashboard/application_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from baserow.contrib.integrations.local_baserow.integration_types import (
LocalBaserowIntegrationType,
)
from baserow.core.formula.registries import DataProviderTypeRegistry
from baserow.core.integrations.handler import IntegrationHandler
from baserow.core.integrations.registries import integration_type_registry
from baserow.core.models import Application, Workspace
Expand All @@ -27,6 +28,8 @@ class DashboardApplicationType(ApplicationType):
serializer_field_names = ["name", "description"]
allowed_fields = ["description"]
supports_integrations = True
# Dashboard supports runtime functions, but no data provider references.
data_provider_type_registry = DataProviderTypeRegistry()

def get_api_urls(self):
from .api import urls as api_urls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,32 @@

if TYPE_CHECKING:
from baserow.contrib.dashboard.widgets.models import Widget
from baserow.core.models import Workspace


class DashboardDispatchContext(DispatchContext):
own_properties = [
"request",
"widget",
"workspace",
]

def __init__(
self,
request: HttpRequest,
workspace: Optional["Workspace"] = None,
widget: Optional["Widget"] = None,
):
"""Create a context for dispatching a dashboard data source."""

# Before dashboard data sources needed their workspace directly, the second
# positional argument was the widget. Keep accepting that shape while callers
# move to passing the workspace.
if widget is None and workspace is not None and hasattr(workspace, "dashboard"):
widget = workspace
workspace = widget.dashboard.workspace

self.request = request
self.widget = widget

super().__init__()
super().__init__(workspace)
62 changes: 62 additions & 0 deletions backend/src/baserow/contrib/database/fields/field_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3057,6 +3057,22 @@ def get_response_serializer_field(self, instance, **kwargs):
},
)

def get_query_value_serializer(self, field: LinkRowField):
response_serializer = self.get_response_serializer_field(field)
related_model = field.link_row_table.get_model()

def serialize(value):
if value is None or value == "OTHER_VALUES":
return value

rows_by_id = {
row.id: row for row in related_model.objects.filter(id__in=value)
}
rows = [rows_by_id[row_id] for row_id in value if row_id in rows_by_id]
return response_serializer.to_representation(rows)

return serialize

def get_serializer_help_text(self, instance):
return (
"This field accepts an `array` containing the ids or the names of the "
Expand Down Expand Up @@ -4885,6 +4901,18 @@ def get_group_by_serializer_field(self, field, **kwargs):
}
)

def get_query_value_serializer(self, field: SingleSelectField):
select_options = {option.id: option for option in field.select_options.all()}
response_serializer = self.get_response_serializer_field(field)

def serialize(value):
if value is None or value == "OTHER_VALUES":
return value

return response_serializer.to_representation(select_options.get(value))

return serialize

def get_formula_reference_to_model_field(
self, model_field, db_column, already_in_subquery
):
Expand Down Expand Up @@ -5089,6 +5117,23 @@ def get_response_serializer_field(self, instance, **kwargs):
}
)

def get_query_value_serializer(self, field: MultipleSelectField):
select_options = {option.id: option for option in field.select_options.all()}
response_serializer = self.get_response_serializer_field(field)

def serialize(value):
if value is None or value == "OTHER_VALUES":
return value

options = [
select_options[option_id]
for option_id in value
if option_id in select_options
]
return response_serializer.to_representation(options)

return serialize

def enhance_queryset(self, queryset, field, name, **kwargs):
# It's important that this individual enhance_queryset method exists, even
# though the enhance queryset in bulk exists, because the link_row field can
Expand Down Expand Up @@ -7091,6 +7136,23 @@ def get_response_serializer_field(self, instance, **kwargs):
}
)

def get_query_value_serializer(self, field: MultipleCollaboratorsField):
response_serializer = self.get_response_serializer_field(field)

def serialize(value):
if value is None or value == "OTHER_VALUES":
return value

users_by_id = {
user.id: user for user in get_user_model().objects.filter(id__in=value)
}
users = [
users_by_id[user_id] for user_id in value if user_id in users_by_id
]
return response_serializer.to_representation(users)

return serialize

def serialize_to_input_value(self, field: Field, value: any) -> any:
return [{"id": u.id, "name": u.first_name} for u in value.all()]

Expand Down
11 changes: 11 additions & 0 deletions backend/src/baserow/contrib/database/fields/registries.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
NoReturn,
Expand Down Expand Up @@ -2278,6 +2279,16 @@ def get_distribution_group_by_value(self, field_name: str):

return field_name

def get_query_value_serializer(self, field: Field) -> Callable[[Any], Any]:
"""
Returns a serializer function for raw values produced by queries.

Field types can override this when the raw query value differs from the
public API representation of the field value.
"""

return lambda value: value

def to_runtime_formula_value(self, field, value):
"""
Transform the value to be usable in runtime formula land.
Expand Down
Loading
Loading