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
6 changes: 6 additions & 0 deletions backend/src/baserow/contrib/database/api/fields/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,9 @@
HTTP_400_BAD_REQUEST,
"Cannot use the view type.",
)
ERROR_INVALID_DEFAULT_VALUE_FUNCTION = (
"ERROR_INVALID_DEFAULT_VALUE_FUNCTION",
HTTP_400_BAD_REQUEST,
"The provided default value function `{e.unsupported_function}` is not supported "
"for the given field type `{e.field_type}`.",
)
5 changes: 5 additions & 0 deletions backend/src/baserow/contrib/database/api/views/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,8 @@
HTTP_400_BAD_REQUEST,
"This view type does not support listing rows.",
)
ERROR_VIEW_DOES_NOT_SUPPORT_DEFAULT_VALUES = (
"ERROR_VIEW_DOES_NOT_SUPPORT_DEFAULT_VALUES",
HTTP_400_BAD_REQUEST,
"This view type does not support setting default row values.",
)
50 changes: 48 additions & 2 deletions backend/src/baserow/contrib/database/api/views/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
OWNERSHIP_TYPE_COLLABORATIVE,
View,
ViewDecoration,
ViewDefaultValue,
ViewFilter,
ViewFilterGroup,
ViewGroupBy,
Expand Down Expand Up @@ -388,6 +389,24 @@ class Meta:
}


class ViewDefaultValueSerializer(serializers.ModelSerializer):
class Meta:
model = ViewDefaultValue
fields = (
"id",
"field",
"enabled",
"value",
"field_type",
"function",
)
extra_kwargs = {
"id": {"read_only": True},
"field_type": {"read_only": True},
"enabled": {"default": True},
}


class ViewSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
table = TableWithoutDataSyncSerializer()
Expand All @@ -400,6 +419,9 @@ class ViewSerializer(serializers.ModelSerializer):
decorations = ViewDecorationSerializer(
many=True, source="viewdecoration_set", required=False
)
default_row_values = ViewDefaultValueSerializer(
many=True, source="view_default_values", required=False
)
show_logo = serializers.BooleanField(required=False)
ownership_type = serializers.CharField()
owned_by_id = serializers.IntegerField(required=False)
Expand All @@ -419,6 +441,7 @@ class Meta:
"sortings",
"group_bys",
"decorations",
"default_row_values",
"filters_disabled",
"public_view_has_password",
"show_logo",
Expand All @@ -440,6 +463,7 @@ def __init__(self, instance=None, *args, **kwargs):
context["include_sortings"] = kwargs.pop("sortings", False)
context["include_decorations"] = kwargs.pop("decorations", False)
context["include_group_bys"] = kwargs.pop("group_bys", False)
context["include_default_row_values"] = kwargs.pop("default_row_values", False)
enhance_objects_by_view_ownership = kwargs.pop(
"enhance_objects_by_view_ownership", True
)
Expand All @@ -449,14 +473,33 @@ def __init__(self, instance=None, *args, **kwargs):
# makes sure that the user only receives data about the view that they are
# permitted to see, according to the ownership type.
if enhance_objects_by_view_ownership and "user" in context:
# Build a set of field names that will actually appear in the
# response so that ownership types can skip unnecessary work (and
# queries) for fields that are not included.
includes = set()
if context.get("include_filters"):
includes.add("filters")
if context.get("include_sortings"):
includes.add("sortings")
if context.get("include_decorations"):
includes.add("decorations")
if context.get("include_group_bys"):
includes.add("group_bys")
if context.get("include_default_row_values"):
includes.add("default_row_values")

if isinstance(instance, list):
instance = view_ownership_type_registry.prepare_views_of_different_types_for_user(
context["user"], instance
context["user"],
instance,
includes=includes,
)
else:
instance = (
view_ownership_type_registry.prepare_views_of_different_types_for_user(
context["user"], [instance]
context["user"],
[instance],
includes=includes,
)
)[0]
super().__init__(instance, *args, **kwargs)
Expand All @@ -480,6 +523,9 @@ def to_representation(self, instance):
if not self.context["include_group_bys"]:
self.fields.pop("group_bys", None)

if not self.context.get("include_default_row_values"):
self.fields.pop("default_row_values", None)

return super().to_representation(instance)

@extend_schema_field(OpenApiTypes.STR)
Expand Down
6 changes: 6 additions & 0 deletions backend/src/baserow/contrib/database/api/views/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
RotateViewSlugView,
ViewDecorationsView,
ViewDecorationView,
ViewDefaultValuesView,
ViewFieldOptionsView,
ViewFilterGroupsView,
ViewFilterGroupView,
Expand Down Expand Up @@ -89,6 +90,11 @@
ViewDecorationsView.as_view(),
name="list_decorations",
),
re_path(
r"(?P<view_id>[0-9]+)/default-values/$",
ViewDefaultValuesView.as_view(),
name="default_values",
),
re_path(
r"(?P<view_id>[0-9]+)/field-options/$",
ViewFieldOptionsView.as_view(),
Expand Down
126 changes: 121 additions & 5 deletions backend/src/baserow/contrib/database/api/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import ObjectDoesNotExist

Expand All @@ -23,6 +24,7 @@
validate_query_parameters,
)
from baserow.api.errors import ERROR_USER_NOT_IN_GROUP
from baserow.api.exceptions import RequestBodyValidationException
from baserow.api.pagination import PageNumberPagination
from baserow.api.schemas import (
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
Expand All @@ -41,6 +43,7 @@
from baserow.contrib.database.api.fields.errors import (
ERROR_FIELD_DOES_NOT_EXIST,
ERROR_FIELD_NOT_IN_TABLE,
ERROR_INVALID_DEFAULT_VALUE_FUNCTION,
)
from baserow.contrib.database.api.fields.serializers import LinkRowValueSerializer
from baserow.contrib.database.api.rows.errors import ERROR_ROW_DOES_NOT_EXIST
Expand All @@ -54,11 +57,13 @@
CreateViewGroupBySerializer,
PublicViewInfoSerializer,
UpdateViewGroupBySerializer,
ViewDefaultValueSerializer,
ViewGroupBySerializer,
)
from baserow.contrib.database.fields.exceptions import (
FieldDoesNotExist,
FieldNotInTable,
InvalidDefaultValueFunction,
)
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.models import Field, LinkRowField
Expand All @@ -83,6 +88,7 @@
RotateViewSlugActionType,
UpdateDecorationActionType,
UpdateViewActionType,
UpdateViewDefaultValuesActionType,
UpdateViewFieldOptionsActionType,
UpdateViewFilterActionType,
UpdateViewFilterGroupActionType,
Expand All @@ -97,6 +103,7 @@
ViewDecorationDoesNotExist,
ViewDecorationNotSupported,
ViewDoesNotExist,
ViewDoesNotSupportDefaultValues,
ViewDoesNotSupportFieldOptions,
ViewDoesNotSupportListingRows,
ViewFilterDoesNotExist,
Expand Down Expand Up @@ -144,6 +151,7 @@
ERROR_VIEW_DECORATION_NOT_SUPPORTED,
ERROR_VIEW_DECORATION_VALUE_PROVIDER_NOT_COMPATIBLE,
ERROR_VIEW_DOES_NOT_EXIST,
ERROR_VIEW_DOES_NOT_SUPPORT_DEFAULT_VALUES,
ERROR_VIEW_DOES_NOT_SUPPORT_FIELD_OPTIONS,
ERROR_VIEW_DOES_NOT_SUPPORT_LISTING_ROWS,
ERROR_VIEW_FILTER_DOES_NOT_EXIST,
Expand Down Expand Up @@ -245,7 +253,7 @@ def get_permissions(self):
description=(
"A comma separated list of extra attributes to include on each "
"view in the response. The supported attributes are `filters`, "
"`sortings` and `decorations`. "
"`sortings`, `decorations`, `group_bys` and `default_row_values`. "
"For example `include=filters,sortings` will add the "
"attributes `filters` and `sortings` to every returned view, "
"containing a list of the views filters and sortings respectively."
Expand Down Expand Up @@ -278,9 +286,19 @@ def get_permissions(self):
}
)
@validate_query_parameters(ListQueryParamatersSerializer)
@allowed_includes("filters", "sortings", "decorations", "group_bys")
@allowed_includes(
"filters", "sortings", "decorations", "group_bys", "default_row_values"
)
def get(
self, request, table_id, query_params, filters, sortings, decorations, group_bys
self,
request,
table_id,
query_params,
filters,
sortings,
decorations,
group_bys,
default_row_values,
):
"""
Responds with a list of serialized views that belong to the table if the user
Expand All @@ -303,6 +321,7 @@ def get(
sortings,
decorations,
group_bys,
default_row_values,
query_params["limit"],
)

Expand All @@ -320,6 +339,7 @@ def get(
sortings=sortings,
decorations=decorations,
group_bys=group_bys,
default_row_values=default_row_values,
many=True,
).data
return Response(serialized_views)
Expand Down Expand Up @@ -466,8 +486,19 @@ class ViewView(APIView):
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
}
)
@allowed_includes("filters", "sortings", "decorations", "group_bys")
def get(self, request, view_id, filters, sortings, decorations, group_bys):
@allowed_includes(
"filters", "sortings", "decorations", "group_bys", "default_row_values"
)
def get(
self,
request,
view_id,
filters,
sortings,
decorations,
group_bys,
default_row_values,
):
"""Selects a single view and responds with a serialized version."""

view = ViewHandler().get_view_as_user(request.user, view_id)
Expand All @@ -479,6 +510,7 @@ def get(self, request, view_id, filters, sortings, decorations, group_bys):
sortings=sortings,
decorations=decorations,
group_bys=group_bys,
default_row_values=default_row_values,
context={"user": request.user},
)
return Response(serializer.data)
Expand Down Expand Up @@ -676,6 +708,7 @@ def post(self, request, view_id):
sortings=True,
decorations=True,
group_bys=True,
default_row_values=True,
context={"user": request.user},
)
return Response(serializer.data)
Expand Down Expand Up @@ -2455,3 +2488,86 @@ def get(self, request: Request, slug: str, row_id: int) -> Response:
row._meta.model, RowSerializer, is_response=True, field_ids=field_ids
)
return Response(serializer_class(row).data)


class ViewDefaultValuesView(APIView):
permission_classes = (IsAuthenticated,)

@extend_schema(
parameters=[
OpenApiParameter(
name="view_id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="Updates the default row values for the view with "
"the given id.",
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
],
tags=["Database table views"],
operation_id="update_view_default_values",
description=(
"Updates the default row values for the specified view. Accepts a list of "
"default value objects, each specifying the field, whether the default is "
"enabled, an optional raw value, and an optional function name (e.g. "
"'now') for dynamic defaults."
),
request=ViewDefaultValueSerializer(many=True),
responses={
200: ViewDefaultValueSerializer(many=True),
400: get_error_schema(
[
"ERROR_USER_NOT_IN_GROUP",
"ERROR_VIEW_DOES_NOT_SUPPORT_DEFAULT_VALUES",
"ERROR_INVALID_DEFAULT_VALUE_FUNCTION",
"ERROR_FIELD_NOT_IN_TABLE",
]
),
404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]),
},
)
@map_exceptions(
{
ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST,
ViewDoesNotSupportDefaultValues: ERROR_VIEW_DOES_NOT_SUPPORT_DEFAULT_VALUES,
InvalidDefaultValueFunction: ERROR_INVALID_DEFAULT_VALUE_FUNCTION,
FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE,
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
}
)
@transaction.atomic
def patch(self, request, view_id):
"""Updates the default row values for the given view."""

items = validate_data(ViewDefaultValueSerializer, request.data, many=True)

handler = ViewHandler()
view = handler.get_view(view_id).specific

# Validate field values through the row serializer to ensure they are valid
# for the respective field types.
table = view.table
model = table.get_model()
validation_serializer = get_row_serializer_class(model)

raw_values = {}
for item in items:
field_id = item.get("field")
if field_id and item.get("value") is not None:
raw_values[f"field_{field_id}"] = item["value"]

if raw_values:
validate_data(validation_serializer, raw_values)

try:
records = action_type_registry.get(
UpdateViewDefaultValuesActionType.type
).do(
user=request.user,
view=view,
items=items,
)
except ValidationError as e:
raise RequestBodyValidationException(detail=e.message)

return Response(ViewDefaultValueSerializer(records, many=True).data)
1 change: 1 addition & 0 deletions backend/src/baserow/contrib/database/application_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ def export_serialized(
"view_set__viewsort_set",
"view_set__viewgroupby_set",
"view_set__viewdecoration_set",
"view_set__view_default_values",
"data_sync__synced_properties",
Prefetch(
"field_rules", queryset=specific_queryset(FieldRule.objects.all())
Expand Down
Loading
Loading