mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-13 16:49:07 +00:00
Resolve "The notification mode settings is not shown correctly if the row is not in the view"
This commit is contained in:
parent
f381d42848
commit
bdf7d3e8c7
27 changed files with 361 additions and 138 deletions
backend
src/baserow/contrib/database
api
tokens
tests/baserow/contrib/database/api
changelog/entries/unreleased/bug
premium
backend
src/baserow_premium
api/views
row_comments
tests/baserow_premium_tests/row_comments
web-frontend/modules/baserow_premium/store
web-frontend/modules/database
|
@ -13,7 +13,10 @@ from baserow.api.utils import get_serializer_class
|
||||||
from baserow.contrib.database.api.rows.fields import UserFieldNamesField
|
from baserow.contrib.database.api.rows.fields import UserFieldNamesField
|
||||||
from baserow.contrib.database.fields.registries import field_type_registry
|
from baserow.contrib.database.fields.registries import field_type_registry
|
||||||
from baserow.contrib.database.rows.models import RowHistory
|
from baserow.contrib.database.rows.models import RowHistory
|
||||||
from baserow.contrib.database.rows.registries import row_metadata_registry
|
from baserow.contrib.database.rows.registries import (
|
||||||
|
RowMetadataType,
|
||||||
|
row_metadata_registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RowSerializer(serializers.ModelSerializer):
|
class RowSerializer(serializers.ModelSerializer):
|
||||||
|
@ -173,18 +176,18 @@ class BatchDeleteRowsSerializer(serializers.Serializer):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_example_row_serializer_class(example_type="get", user_field_names=False):
|
def get_example_row_serializer_class(
|
||||||
|
example_type: str = "get",
|
||||||
|
user_field_names: bool = False,
|
||||||
|
) -> serializers.Serializer:
|
||||||
"""
|
"""
|
||||||
Generates a serializer containing a field for each field type. It is only used for
|
Generates a serializer containing a field for each field type. It is only used for
|
||||||
example purposes in the openapi documentation.
|
example purposes in the openapi documentation.
|
||||||
|
|
||||||
:param example_type: Sets various parameters. Can be get, post, patch.
|
:param example_type: Sets various parameters. Can be get, post, patch.
|
||||||
:type example_type: str
|
|
||||||
:param user_field_names: Whether this example serializer help text should indicate
|
:param user_field_names: Whether this example serializer help text should indicate
|
||||||
the fields names can be switched using the `user_field_names` GET parameter.
|
the fields names can be switched using the `user_field_names` GET parameter.
|
||||||
:type user_field_names: bool
|
|
||||||
:return: Generated serializer containing a field for each field type.
|
:return: Generated serializer containing a field for each field type.
|
||||||
:rtype: Serializer
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
|
@ -192,24 +195,28 @@ def get_example_row_serializer_class(example_type="get", user_field_names=False)
|
||||||
"class_name": "ExampleRowResponseSerializer",
|
"class_name": "ExampleRowResponseSerializer",
|
||||||
"add_id": True,
|
"add_id": True,
|
||||||
"add_order": True,
|
"add_order": True,
|
||||||
|
"add_metadata": True,
|
||||||
"read_only_fields": True,
|
"read_only_fields": True,
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"class_name": "ExampleRowRequestSerializer",
|
"class_name": "ExampleRowRequestSerializer",
|
||||||
"add_id": False,
|
"add_id": False,
|
||||||
"add_order": False,
|
"add_order": False,
|
||||||
|
"add_metadata": False,
|
||||||
"read_only_fields": False,
|
"read_only_fields": False,
|
||||||
},
|
},
|
||||||
"patch": {
|
"patch": {
|
||||||
"class_name": "ExampleUpdateRowRequestSerializer",
|
"class_name": "ExampleUpdateRowRequestSerializer",
|
||||||
"add_id": False,
|
"add_id": False,
|
||||||
"add_order": False,
|
"add_order": False,
|
||||||
|
"add_metadata": False,
|
||||||
"read_only_fields": False,
|
"read_only_fields": False,
|
||||||
},
|
},
|
||||||
"patch_batch": {
|
"patch_batch": {
|
||||||
"class_name": "ExampleBatchUpdateRowRequestSerializer",
|
"class_name": "ExampleBatchUpdateRowRequestSerializer",
|
||||||
"add_id": True,
|
"add_id": True,
|
||||||
"add_order": False,
|
"add_order": False,
|
||||||
|
"add_metadata": False,
|
||||||
"read_only_fields": False,
|
"read_only_fields": False,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -217,6 +224,7 @@ def get_example_row_serializer_class(example_type="get", user_field_names=False)
|
||||||
class_name = config[example_type]["class_name"]
|
class_name = config[example_type]["class_name"]
|
||||||
add_id = config[example_type]["add_id"]
|
add_id = config[example_type]["add_id"]
|
||||||
add_order = config[example_type]["add_order"]
|
add_order = config[example_type]["add_order"]
|
||||||
|
add_metadata = config[example_type]["add_metadata"]
|
||||||
add_readonly_fields = config[example_type]["read_only_fields"]
|
add_readonly_fields = config[example_type]["read_only_fields"]
|
||||||
is_response_example = add_readonly_fields
|
is_response_example = add_readonly_fields
|
||||||
|
|
||||||
|
@ -245,6 +253,16 @@ def get_example_row_serializer_class(example_type="get", user_field_names=False)
|
||||||
"last.",
|
"last.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if add_metadata:
|
||||||
|
metadata_serializer = get_example_row_metadata_serializer()
|
||||||
|
fields["metadata"] = metadata_serializer(
|
||||||
|
required=False,
|
||||||
|
help_text=(
|
||||||
|
"Additional metadata for the row, if `include=metadata' "
|
||||||
|
"is provided as query parameter."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
field_types = field_type_registry.registry.values()
|
field_types = field_type_registry.registry.values()
|
||||||
|
|
||||||
if len(field_types) == 0:
|
if len(field_types) == 0:
|
||||||
|
@ -285,30 +303,37 @@ def get_example_row_serializer_class(example_type="get", user_field_names=False)
|
||||||
return class_object
|
return class_object
|
||||||
|
|
||||||
|
|
||||||
def get_example_row_metadata_field_serializer():
|
def get_example_row_metadata_serializer() -> serializers.Serializer:
|
||||||
"""
|
"""
|
||||||
Generates a serializer containing a field for each row metadata type which
|
Generates a serializer containing a field for each row metadata type which
|
||||||
represents the metadata for a single row.
|
represents the metadata for a single row.
|
||||||
It is only used for example purposes in the openapi documentation.
|
:return: A serializer containing a field for each row metadata type.
|
||||||
|
|
||||||
:return: Generated serializer for a single rows metadata
|
|
||||||
:rtype: Serializer
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
metadata_types = row_metadata_registry.get_all()
|
metadata_types: List[RowMetadataType] = row_metadata_registry.get_all()
|
||||||
|
|
||||||
if len(metadata_types) == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
for metadata_type in metadata_types:
|
for metadata_type in metadata_types:
|
||||||
fields[metadata_type.type] = metadata_type.get_example_serializer_field()
|
fields[metadata_type.type] = metadata_type.get_example_serializer_field()
|
||||||
|
|
||||||
per_row_serializer = type(
|
return type("RowMetadataSerializer", (serializers.Serializer,), fields)
|
||||||
"RowMetadataSerializer", (serializers.Serializer,), fields
|
|
||||||
)()
|
|
||||||
|
def get_example_multiple_rows_metadata_serializer() -> serializers.Serializer:
|
||||||
|
"""
|
||||||
|
Generates a serializer containing a field for each row metadata type which
|
||||||
|
represents the metadata for a single row. The single row serializer is then
|
||||||
|
nested in a dictionary where the key is the row id and the value is the single row
|
||||||
|
metadata serializer.
|
||||||
|
It is only used for example purposes in the openapi documentation.
|
||||||
|
|
||||||
|
:return: A serializer containing a dictionary of row id to row metadata.
|
||||||
|
"""
|
||||||
|
|
||||||
|
per_row_serializer = get_example_row_metadata_serializer()
|
||||||
|
|
||||||
return serializers.DictField(
|
return serializers.DictField(
|
||||||
child=per_row_serializer,
|
child=per_row_serializer(),
|
||||||
required=False,
|
required=False,
|
||||||
help_text="An object keyed by row id with a value being an object containing "
|
help_text="An object keyed by row id with a value being an object containing "
|
||||||
"additional metadata about that row. A row might not have metadata and will "
|
"additional metadata about that row. A row might not have metadata and will "
|
||||||
|
|
|
@ -14,6 +14,7 @@ from rest_framework.status import HTTP_204_NO_CONTENT
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from baserow.api.decorators import (
|
from baserow.api.decorators import (
|
||||||
|
allowed_includes,
|
||||||
map_exceptions,
|
map_exceptions,
|
||||||
require_request_data_type,
|
require_request_data_type,
|
||||||
validate_body,
|
validate_body,
|
||||||
|
@ -51,7 +52,10 @@ from baserow.contrib.database.api.rows.exceptions import InvalidJoinParameterExc
|
||||||
from baserow.contrib.database.api.rows.serializers import GetRowAdjacentSerializer
|
from baserow.contrib.database.api.rows.serializers import GetRowAdjacentSerializer
|
||||||
from baserow.contrib.database.api.tables.errors import ERROR_TABLE_DOES_NOT_EXIST
|
from baserow.contrib.database.api.tables.errors import ERROR_TABLE_DOES_NOT_EXIST
|
||||||
from baserow.contrib.database.api.tokens.authentications import TokenAuthentication
|
from baserow.contrib.database.api.tokens.authentications import TokenAuthentication
|
||||||
from baserow.contrib.database.api.tokens.errors import ERROR_NO_PERMISSION_TO_TABLE
|
from baserow.contrib.database.api.tokens.errors import (
|
||||||
|
ERROR_CANNOT_INCLUDE_ROW_METADATA,
|
||||||
|
ERROR_NO_PERMISSION_TO_TABLE,
|
||||||
|
)
|
||||||
from baserow.contrib.database.api.utils import (
|
from baserow.contrib.database.api.utils import (
|
||||||
extract_link_row_joins_from_request,
|
extract_link_row_joins_from_request,
|
||||||
extract_send_webhook_events_from_params,
|
extract_send_webhook_events_from_params,
|
||||||
|
@ -63,6 +67,7 @@ from baserow.contrib.database.api.views.errors import (
|
||||||
ERROR_VIEW_FILTER_TYPE_DOES_NOT_EXIST,
|
ERROR_VIEW_FILTER_TYPE_DOES_NOT_EXIST,
|
||||||
ERROR_VIEW_FILTER_TYPE_UNSUPPORTED_FIELD,
|
ERROR_VIEW_FILTER_TYPE_UNSUPPORTED_FIELD,
|
||||||
)
|
)
|
||||||
|
from baserow.contrib.database.api.views.utils import serialize_single_row_metadata
|
||||||
from baserow.contrib.database.fields.exceptions import (
|
from baserow.contrib.database.fields.exceptions import (
|
||||||
FieldDoesNotExist,
|
FieldDoesNotExist,
|
||||||
FilterFieldNotFound,
|
FilterFieldNotFound,
|
||||||
|
@ -100,7 +105,10 @@ from baserow.contrib.database.table.operations import (
|
||||||
ListRowNamesDatabaseTableOperationType,
|
ListRowNamesDatabaseTableOperationType,
|
||||||
ListRowsDatabaseTableOperationType,
|
ListRowsDatabaseTableOperationType,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.tokens.exceptions import NoPermissionToTable
|
from baserow.contrib.database.tokens.exceptions import (
|
||||||
|
NoPermissionToTable,
|
||||||
|
TokenCannotIncludeRowMetadata,
|
||||||
|
)
|
||||||
from baserow.contrib.database.tokens.handler import TokenHandler
|
from baserow.contrib.database.tokens.handler import TokenHandler
|
||||||
from baserow.contrib.database.views.exceptions import (
|
from baserow.contrib.database.views.exceptions import (
|
||||||
ViewDoesNotExist,
|
ViewDoesNotExist,
|
||||||
|
@ -702,6 +710,16 @@ class RowView(APIView):
|
||||||
"field names (e.g., field_123)."
|
"field names (e.g., field_123)."
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="include",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
description=(
|
||||||
|
"Optionally include row's `metadata` in the response. "
|
||||||
|
"The `metadata` object includes extra row specific data like the "
|
||||||
|
"'row_comments_notification_mode' settings, if available."
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
tags=["Database table rows"],
|
tags=["Database table rows"],
|
||||||
operation_id="get_database_table_row",
|
operation_id="get_database_table_row",
|
||||||
|
@ -735,9 +753,11 @@ class RowView(APIView):
|
||||||
TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST,
|
TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST,
|
||||||
RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST,
|
RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST,
|
||||||
NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE,
|
NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE,
|
||||||
|
TokenCannotIncludeRowMetadata: ERROR_CANNOT_INCLUDE_ROW_METADATA,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def get(self, request, table_id, row_id):
|
@allowed_includes("metadata")
|
||||||
|
def get(self, request, table_id, row_id, metadata):
|
||||||
"""
|
"""
|
||||||
Responds with a serializer version of the row related to the provided row_id
|
Responds with a serializer version of the row related to the provided row_id
|
||||||
and table_id.
|
and table_id.
|
||||||
|
@ -745,7 +765,13 @@ class RowView(APIView):
|
||||||
|
|
||||||
table = TableHandler().get_table(table_id)
|
table = TableHandler().get_table(table_id)
|
||||||
|
|
||||||
TokenHandler().check_table_permissions(request, "read", table, False)
|
token_handler = TokenHandler()
|
||||||
|
db_token = token_handler.get_token_from_request(request)
|
||||||
|
if db_token is not None:
|
||||||
|
if metadata:
|
||||||
|
raise TokenCannotIncludeRowMetadata()
|
||||||
|
token_handler.check_table_permissions(db_token, "read", table)
|
||||||
|
|
||||||
user_field_names = extract_user_field_names_from_params(request.GET)
|
user_field_names = extract_user_field_names_from_params(request.GET)
|
||||||
model = table.get_model()
|
model = table.get_model()
|
||||||
row = RowHandler().get_row(request.user, table, row_id, model)
|
row = RowHandler().get_row(request.user, table, row_id, model)
|
||||||
|
@ -753,10 +779,15 @@ class RowView(APIView):
|
||||||
model, RowSerializer, is_response=True, user_field_names=user_field_names
|
model, RowSerializer, is_response=True, user_field_names=user_field_names
|
||||||
)
|
)
|
||||||
serializer = serializer_class(row)
|
serializer = serializer_class(row)
|
||||||
|
response_data = serializer.data
|
||||||
|
|
||||||
|
if metadata:
|
||||||
|
row_metadata = serialize_single_row_metadata(request.user, row)
|
||||||
|
response_data["metadata"] = row_metadata
|
||||||
|
|
||||||
rows_loaded.send(sender=self, table=table)
|
rows_loaded.send(sender=self, table=table)
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(response_data)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
parameters=[
|
parameters=[
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
from rest_framework.status import HTTP_401_UNAUTHORIZED, HTTP_404_NOT_FOUND
|
from rest_framework.status import (
|
||||||
|
HTTP_400_BAD_REQUEST,
|
||||||
|
HTTP_401_UNAUTHORIZED,
|
||||||
|
HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
ERROR_TOKEN_DOES_NOT_EXIST = (
|
ERROR_TOKEN_DOES_NOT_EXIST = (
|
||||||
"ERROR_TOKEN_DOES_NOT_EXIST",
|
"ERROR_TOKEN_DOES_NOT_EXIST",
|
||||||
|
@ -10,3 +14,9 @@ ERROR_NO_PERMISSION_TO_TABLE = (
|
||||||
HTTP_401_UNAUTHORIZED,
|
HTTP_401_UNAUTHORIZED,
|
||||||
"The token does not have permissions to the table.",
|
"The token does not have permissions to the table.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ERROR_CANNOT_INCLUDE_ROW_METADATA = (
|
||||||
|
"ERROR_CANNOT_INCLUDE_ROW_METADATA",
|
||||||
|
HTTP_400_BAD_REQUEST,
|
||||||
|
"The token cannot include row metadata.",
|
||||||
|
)
|
||||||
|
|
|
@ -29,7 +29,7 @@ from baserow.contrib.database.api.fields.errors import (
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.api.rows.serializers import (
|
from baserow.contrib.database.api.rows.serializers import (
|
||||||
RowSerializer,
|
RowSerializer,
|
||||||
get_example_row_metadata_field_serializer,
|
get_example_multiple_rows_metadata_serializer,
|
||||||
get_example_row_serializer_class,
|
get_example_row_serializer_class,
|
||||||
get_row_serializer_class,
|
get_row_serializer_class,
|
||||||
)
|
)
|
||||||
|
@ -150,7 +150,7 @@ class GalleryViewView(APIView):
|
||||||
serializer_class=GalleryViewFieldOptionsSerializer,
|
serializer_class=GalleryViewFieldOptionsSerializer,
|
||||||
required=False,
|
required=False,
|
||||||
),
|
),
|
||||||
"row_metadata": get_example_row_metadata_field_serializer(),
|
"row_metadata": get_example_multiple_rows_metadata_serializer(),
|
||||||
},
|
},
|
||||||
serializer_name="PaginationSerializerWithGalleryViewFieldOptions",
|
serializer_name="PaginationSerializerWithGalleryViewFieldOptions",
|
||||||
),
|
),
|
||||||
|
|
|
@ -39,7 +39,7 @@ from baserow.contrib.database.api.fields.errors import (
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.api.rows.serializers import (
|
from baserow.contrib.database.api.rows.serializers import (
|
||||||
RowSerializer,
|
RowSerializer,
|
||||||
get_example_row_metadata_field_serializer,
|
get_example_multiple_rows_metadata_serializer,
|
||||||
get_example_row_serializer_class,
|
get_example_row_serializer_class,
|
||||||
get_row_serializer_class,
|
get_row_serializer_class,
|
||||||
)
|
)
|
||||||
|
@ -172,7 +172,7 @@ class GridViewView(APIView):
|
||||||
"field_options": FieldOptionsField(
|
"field_options": FieldOptionsField(
|
||||||
serializer_class=GridViewFieldOptionsSerializer, required=False
|
serializer_class=GridViewFieldOptionsSerializer, required=False
|
||||||
),
|
),
|
||||||
"row_metadata": get_example_row_metadata_field_serializer(),
|
"row_metadata": get_example_multiple_rows_metadata_serializer(),
|
||||||
},
|
},
|
||||||
serializer_name="PaginationSerializerWithGridViewFieldOptions",
|
serializer_name="PaginationSerializerWithGridViewFieldOptions",
|
||||||
),
|
),
|
||||||
|
|
|
@ -221,6 +221,22 @@ def serialize_rows_metadata(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_single_row_metadata(
|
||||||
|
user: AbstractUser, row: GeneratedTableModel
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Serializes the metadata for the provided row.
|
||||||
|
|
||||||
|
:param user: The user to serialize the metadata for.
|
||||||
|
:param row: The row to serialize the metadata for.
|
||||||
|
:return: The serialized metadata for the provided rows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return row_metadata_registry.generate_and_merge_metadata_for_row(
|
||||||
|
user, row.baserow_table, row.id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def serialize_group_by_fields_metadata(
|
def serialize_group_by_fields_metadata(
|
||||||
queryset: QuerySet[GeneratedTableModel],
|
queryset: QuerySet[GeneratedTableModel],
|
||||||
group_by_fields: List[Field],
|
group_by_fields: List[Field],
|
||||||
|
|
|
@ -16,3 +16,9 @@ class NoPermissionToTable(Exception):
|
||||||
"""
|
"""
|
||||||
Raised when a token does not have permissions to perform an operation for a table.
|
Raised when a token does not have permissions to perform an operation for a table.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TokenCannotIncludeRowMetadata(Exception):
|
||||||
|
"""
|
||||||
|
Raised if requested to include a row's metadata via token.
|
||||||
|
"""
|
||||||
|
|
|
@ -395,8 +395,41 @@ class TokenHandler:
|
||||||
# At least one must be True
|
# At least one must be True
|
||||||
return any([v is True for v in token_permission.values()])
|
return any([v is True for v in token_permission.values()])
|
||||||
|
|
||||||
|
def get_token_from_request(self, request: Request) -> Token | None:
|
||||||
|
"""
|
||||||
|
Extracts the token from the request. If the token is not found then None is
|
||||||
|
returned.
|
||||||
|
|
||||||
|
:param request: The request from which the token must be extracted.
|
||||||
|
:return: The extracted token or None if it could not be found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return getattr(request, "user_token", None)
|
||||||
|
|
||||||
|
def raise_table_permission_error(self, table: Table, type_name: str | list[str]):
|
||||||
|
"""
|
||||||
|
Raises an exception indicating that the provided token does not have permission
|
||||||
|
to the provided table. Used to raise a consistent exception when the token does
|
||||||
|
not have permission to the table.
|
||||||
|
|
||||||
|
:param table: The table object to check the permissions for.
|
||||||
|
:param type_name: The CRUD operation, create, read, update or delete to check
|
||||||
|
the permissions for. Can be a list if you want to check at least one of the
|
||||||
|
listed operation.
|
||||||
|
:raises NoPermissionToTable: Raised when the token does not have permissions to
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NoPermissionToTable(
|
||||||
|
f"The provided token does not have {type_name} "
|
||||||
|
f"permissions to table {table.id}."
|
||||||
|
)
|
||||||
|
|
||||||
def check_table_permissions(
|
def check_table_permissions(
|
||||||
self, request_or_token, type_name, table, force_check=False
|
self,
|
||||||
|
request_or_token: Request | Token,
|
||||||
|
type_name: str | list[str],
|
||||||
|
table: Table,
|
||||||
|
force_check=False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Instead of returning True or False, this method will raise an exception if the
|
Instead of returning True or False, this method will raise an exception if the
|
||||||
|
@ -404,52 +437,35 @@ class TokenHandler:
|
||||||
|
|
||||||
:param request_or_token: If a request is provided then the token will be
|
:param request_or_token: If a request is provided then the token will be
|
||||||
extracted from the request. Otherwise a token object is expected.
|
extracted from the request. Otherwise a token object is expected.
|
||||||
:type request_or_token: Request or Token
|
|
||||||
:param type_name: The CRUD operation, create, read, update or delete to check
|
:param type_name: The CRUD operation, create, read, update or delete to check
|
||||||
the permissions for. Can be a list if you want to check at least one of the
|
the permissions for. Can be a list if you want to check at least one of the
|
||||||
listed operation.
|
listed operation.
|
||||||
:type type_name: str | list
|
|
||||||
:param table: The table object to check the permissions for.
|
:param table: The table object to check the permissions for.
|
||||||
:type table: Table
|
|
||||||
:param force_check: Indicates if a NoPermissionToTable exception must be raised
|
:param force_check: Indicates if a NoPermissionToTable exception must be raised
|
||||||
when the token could not be extracted from the request. This can be
|
when the token could not be extracted from the request. This can be
|
||||||
useful if a view accepts multiple types of authentication.
|
useful if a view accepts multiple types of authentication.
|
||||||
:type force_check: bool
|
|
||||||
:raises ValueError: when neither a Token or HttpRequest is provided.
|
:raises ValueError: when neither a Token or HttpRequest is provided.
|
||||||
:raises NoPermissionToTable: when the token does not have permissions to the
|
:raises NoPermissionToTable: when the token does not have permissions to the
|
||||||
table.
|
table.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
token = None
|
token = None
|
||||||
|
|
||||||
if not isinstance(request_or_token, Request) and not isinstance(
|
|
||||||
request_or_token, Token
|
|
||||||
):
|
|
||||||
raise ValueError(
|
|
||||||
"The provided instance should be a HttpRequest or Token " "object."
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(request_or_token, Request) and hasattr(
|
|
||||||
request_or_token, "user_token"
|
|
||||||
):
|
|
||||||
token = request_or_token.user_token
|
|
||||||
|
|
||||||
if isinstance(request_or_token, Token):
|
if isinstance(request_or_token, Token):
|
||||||
token = request_or_token
|
token = request_or_token
|
||||||
|
elif isinstance(request_or_token, Request):
|
||||||
if not token and not force_check:
|
token = self.get_token_from_request(request_or_token)
|
||||||
return
|
else:
|
||||||
|
raise ValueError(
|
||||||
if (
|
"The provided instance should be a HttpRequest or Token object."
|
||||||
not token
|
|
||||||
and force_check
|
|
||||||
or not TokenHandler().has_table_permission(token, type_name, table)
|
|
||||||
):
|
|
||||||
raise NoPermissionToTable(
|
|
||||||
f"The provided token does not have {type_name} "
|
|
||||||
f"permissions to table {table.id}."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
should_check_permissions = token is not None or force_check
|
||||||
|
has_table_permissions = token is not None and self.has_table_permission(
|
||||||
|
token, type_name, table
|
||||||
|
)
|
||||||
|
if should_check_permissions and not has_table_permissions:
|
||||||
|
self.raise_table_permission_error(table, type_name)
|
||||||
|
|
||||||
def delete_token(self, user, token):
|
def delete_token(self, user, token):
|
||||||
"""
|
"""
|
||||||
Deletes an existing token
|
Deletes an existing token
|
||||||
|
|
|
@ -233,7 +233,7 @@ def test_get_example_row_serializer_class():
|
||||||
num_readonly_fields = len(
|
num_readonly_fields = len(
|
||||||
[ftype for ftype in field_type_registry.registry.values() if ftype.read_only]
|
[ftype for ftype in field_type_registry.registry.values() if ftype.read_only]
|
||||||
)
|
)
|
||||||
num_extra_response_fields = 2 # id + order
|
num_extra_response_fields = 3 # id + order + metadata
|
||||||
num_difference = num_readonly_fields + num_extra_response_fields
|
num_difference = num_readonly_fields + num_extra_response_fields
|
||||||
|
|
||||||
assert num_request_fields == num_response_fields - num_difference
|
assert num_request_fields == num_response_fields - num_difference
|
||||||
|
|
|
@ -677,3 +677,31 @@ def test_check_token(api_client, data_fixture):
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
assert response.json() == {"token": "OK"}
|
assert response.json() == {"token": "OK"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cannot_get_row_metadata_from_token(api_client, data_fixture):
|
||||||
|
user = data_fixture.create_user()
|
||||||
|
workspace = data_fixture.create_workspace(user=user)
|
||||||
|
token = data_fixture.create_token(user=user, workspace=workspace)
|
||||||
|
|
||||||
|
database = data_fixture.create_database_application(workspace=workspace)
|
||||||
|
table = data_fixture.create_database_table(user=user, database=database)
|
||||||
|
row = table.get_model().objects.create()
|
||||||
|
|
||||||
|
def get_row_from_api_with_metadata():
|
||||||
|
return api_client.get(
|
||||||
|
reverse(
|
||||||
|
"api:database:rows:item",
|
||||||
|
kwargs={"table_id": table.id, "row_id": row.id},
|
||||||
|
)
|
||||||
|
+ "?include=metadata",
|
||||||
|
HTTP_AUTHORIZATION=f"Token {token.key}",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = get_row_from_api_with_metadata()
|
||||||
|
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||||
|
assert response.json() == {
|
||||||
|
"error": "ERROR_CANNOT_INCLUDE_ROW_METADATA",
|
||||||
|
"detail": "The token cannot include row metadata.",
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"type": "bug",
|
||||||
|
"message": "Fixed an issue where the notification mode settings were not displayed correctly if the row was not in the view.",
|
||||||
|
"issue_number": 3411,
|
||||||
|
"bullet_points": [],
|
||||||
|
"created_at": "2025-02-05"
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ from baserow_premium.views.models import CalendarViewFieldOptions
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from baserow.contrib.database.api.rows.serializers import (
|
from baserow.contrib.database.api.rows.serializers import (
|
||||||
get_example_row_metadata_field_serializer,
|
get_example_multiple_rows_metadata_serializer,
|
||||||
get_example_row_serializer_class,
|
get_example_row_serializer_class,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.search.handler import ALL_SEARCH_MODES
|
from baserow.contrib.database.search.handler import ALL_SEARCH_MODES
|
||||||
|
@ -75,6 +75,6 @@ def get_calendar_view_example_response_serializer():
|
||||||
"field_options": serializers.ListSerializer(
|
"field_options": serializers.ListSerializer(
|
||||||
child=CalendarViewFieldOptionsSerializer()
|
child=CalendarViewFieldOptionsSerializer()
|
||||||
),
|
),
|
||||||
"row_metadata": get_example_row_metadata_field_serializer(),
|
"row_metadata": get_example_multiple_rows_metadata_serializer(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from baserow_premium.views.models import KanbanViewFieldOptions
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from baserow.contrib.database.api.rows.serializers import (
|
from baserow.contrib.database.api.rows.serializers import (
|
||||||
get_example_row_metadata_field_serializer,
|
get_example_multiple_rows_metadata_serializer,
|
||||||
get_example_row_serializer_class,
|
get_example_row_serializer_class,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,6 +37,6 @@ def get_kanban_view_example_response_serializer():
|
||||||
"field_options": serializers.ListSerializer(
|
"field_options": serializers.ListSerializer(
|
||||||
child=KanbanViewFieldOptionsSerializer()
|
child=KanbanViewFieldOptionsSerializer()
|
||||||
),
|
),
|
||||||
"row_metadata": get_example_row_metadata_field_serializer(),
|
"row_metadata": get_example_multiple_rows_metadata_serializer(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -44,6 +44,7 @@ from baserow.contrib.database.api.fields.errors import (
|
||||||
ERROR_ORDER_BY_FIELD_NOT_POSSIBLE,
|
ERROR_ORDER_BY_FIELD_NOT_POSSIBLE,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.api.rows.serializers import (
|
from baserow.contrib.database.api.rows.serializers import (
|
||||||
|
get_example_multiple_rows_metadata_serializer,
|
||||||
get_example_row_serializer_class,
|
get_example_row_serializer_class,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.api.utils import get_include_exclude_field_ids
|
from baserow.contrib.database.api.utils import get_include_exclude_field_ids
|
||||||
|
@ -56,6 +57,7 @@ from baserow.contrib.database.api.views.serializers import FieldOptionsField
|
||||||
from baserow.contrib.database.api.views.utils import (
|
from baserow.contrib.database.api.views.utils import (
|
||||||
get_public_view_authorization_token,
|
get_public_view_authorization_token,
|
||||||
paginate_and_serialize_queryset,
|
paginate_and_serialize_queryset,
|
||||||
|
serialize_rows_metadata,
|
||||||
serialize_view_field_options,
|
serialize_view_field_options,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.fields.exceptions import (
|
from baserow.contrib.database.fields.exceptions import (
|
||||||
|
@ -105,12 +107,12 @@ class TimelineViewView(APIView):
|
||||||
location=OpenApiParameter.QUERY,
|
location=OpenApiParameter.QUERY,
|
||||||
type=OpenApiTypes.STR,
|
type=OpenApiTypes.STR,
|
||||||
description=(
|
description=(
|
||||||
"A comma separated list allowing the values of "
|
"A comma separated list allowing the values of `field_options` and "
|
||||||
"`field_options` which will add the object/objects with the "
|
"`row_metadata` which will add the object/objects with the same "
|
||||||
"same "
|
|
||||||
"name to the response if included. The `field_options` object "
|
"name to the response if included. The `field_options` object "
|
||||||
"contains user defined view settings for each field. For "
|
"contains user defined view settings for each field. For example "
|
||||||
"example the field's width is included in here."
|
"the field's width is included in here. The `row_metadata` object"
|
||||||
|
" includes extra row specific data on a per row basis."
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ONLY_COUNT_API_PARAM,
|
ONLY_COUNT_API_PARAM,
|
||||||
|
@ -152,6 +154,7 @@ class TimelineViewView(APIView):
|
||||||
serializer_class=TimelineViewFieldOptionsSerializer,
|
serializer_class=TimelineViewFieldOptionsSerializer,
|
||||||
required=False,
|
required=False,
|
||||||
),
|
),
|
||||||
|
"row_metadata": get_example_multiple_rows_metadata_serializer(),
|
||||||
},
|
},
|
||||||
serializer_name="PaginationSerializerWithTimelineViewFieldOptions",
|
serializer_name="PaginationSerializerWithTimelineViewFieldOptions",
|
||||||
),
|
),
|
||||||
|
@ -185,9 +188,9 @@ class TimelineViewView(APIView):
|
||||||
IncompatibleField: ERROR_INCOMPATIBLE_FIELD,
|
IncompatibleField: ERROR_INCOMPATIBLE_FIELD,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@allowed_includes("field_options")
|
@allowed_includes("field_options", "row_metadata")
|
||||||
@validate_query_parameters(SearchQueryParamSerializer, return_validated=True)
|
@validate_query_parameters(SearchQueryParamSerializer, return_validated=True)
|
||||||
def get(self, request, view_id, field_options, query_params):
|
def get(self, request, view_id, field_options, row_metadata, query_params):
|
||||||
"""
|
"""
|
||||||
Lists all the rows of a timeline view, paginated either by a page or
|
Lists all the rows of a timeline view, paginated either by a page or
|
||||||
offset/limit. If the limit get parameter is provided the limit/offset pagination
|
offset/limit. If the limit get parameter is provided the limit/offset pagination
|
||||||
|
@ -238,11 +241,18 @@ class TimelineViewView(APIView):
|
||||||
if "count" in request.GET:
|
if "count" in request.GET:
|
||||||
return Response({"count": queryset.count()})
|
return Response({"count": queryset.count()})
|
||||||
|
|
||||||
response, _, _ = paginate_and_serialize_queryset(queryset, request, field_ids)
|
response, page, _ = paginate_and_serialize_queryset(
|
||||||
|
queryset, request, field_ids
|
||||||
|
)
|
||||||
|
|
||||||
if field_options:
|
if field_options:
|
||||||
response.data.update(**serialize_view_field_options(view, model))
|
response.data.update(**serialize_view_field_options(view, model))
|
||||||
|
|
||||||
|
if row_metadata:
|
||||||
|
response.data.update(
|
||||||
|
row_metadata=serialize_rows_metadata(request.user, view, page)
|
||||||
|
)
|
||||||
|
|
||||||
view_loaded.send(
|
view_loaded.send(
|
||||||
sender=self,
|
sender=self,
|
||||||
table=view.table,
|
table=view.table,
|
||||||
|
|
|
@ -63,6 +63,9 @@ ALL_ROW_COMMENT_NOTIFICATION_MODES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
ROW_COMMENT_NOTIFICATION_DEFAULT_MODE = RowCommentsNotificationModes.MODE_ONLY_MENTIONS
|
||||||
|
|
||||||
|
|
||||||
class RowCommentsNotificationMode(CreatedAndUpdatedOnMixin, models.Model):
|
class RowCommentsNotificationMode(CreatedAndUpdatedOnMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
A many to many relationship between users and table rows to keep track of
|
A many to many relationship between users and table rows to keep track of
|
||||||
|
@ -91,7 +94,7 @@ class RowCommentsNotificationMode(CreatedAndUpdatedOnMixin, models.Model):
|
||||||
(RowCommentsNotificationModes.MODE_ALL_COMMENTS, "All comments"),
|
(RowCommentsNotificationModes.MODE_ALL_COMMENTS, "All comments"),
|
||||||
(RowCommentsNotificationModes.MODE_ONLY_MENTIONS, "Only mentions"),
|
(RowCommentsNotificationModes.MODE_ONLY_MENTIONS, "Only mentions"),
|
||||||
),
|
),
|
||||||
default=RowCommentsNotificationModes.MODE_ONLY_MENTIONS,
|
default=ROW_COMMENT_NOTIFICATION_DEFAULT_MODE,
|
||||||
help_text="The notification mode for this user and row.",
|
help_text="The notification mode for this user and row.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ from django.db.models import Count
|
||||||
|
|
||||||
from baserow_premium.row_comments.models import (
|
from baserow_premium.row_comments.models import (
|
||||||
ALL_ROW_COMMENT_NOTIFICATION_MODES,
|
ALL_ROW_COMMENT_NOTIFICATION_MODES,
|
||||||
|
ROW_COMMENT_NOTIFICATION_DEFAULT_MODE,
|
||||||
RowComment,
|
RowComment,
|
||||||
RowCommentsNotificationMode,
|
RowCommentsNotificationMode,
|
||||||
RowCommentsNotificationModes,
|
|
||||||
)
|
)
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import Field
|
from rest_framework.fields import Field
|
||||||
|
@ -38,7 +38,7 @@ class RowCommentCountMetadataType(RowMetadataType):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RowCommentsNotificationModeMetadataType(RowCommentCountMetadataType):
|
class RowCommentsNotificationModeMetadataType(RowMetadataType):
|
||||||
type = "row_comments_notification_mode"
|
type = "row_comments_notification_mode"
|
||||||
|
|
||||||
def generate_metadata_for_rows(
|
def generate_metadata_for_rows(
|
||||||
|
@ -61,7 +61,7 @@ class RowCommentsNotificationModeMetadataType(RowCommentCountMetadataType):
|
||||||
table=table,
|
table=table,
|
||||||
row_id__in=row_ids,
|
row_id__in=row_ids,
|
||||||
)
|
)
|
||||||
.exclude(mode=RowCommentsNotificationModes.MODE_ONLY_MENTIONS)
|
.exclude(mode=ROW_COMMENT_NOTIFICATION_DEFAULT_MODE)
|
||||||
.values("row_id", "mode")
|
.values("row_id", "mode")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -648,3 +648,44 @@ def test_row_comment_notification_type_can_be_rendered_as_email(
|
||||||
)
|
)
|
||||||
== "Test comment"
|
== "Test comment"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(DEBUG=True)
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_can_get_row_comment_notification_mode_from_row_metadata(
|
||||||
|
api_client, premium_data_fixture
|
||||||
|
):
|
||||||
|
user, token = premium_data_fixture.create_user_and_token(
|
||||||
|
has_active_premium_license=True
|
||||||
|
)
|
||||||
|
table = premium_data_fixture.create_database_table(user=user)
|
||||||
|
row = table.get_model().objects.create()
|
||||||
|
|
||||||
|
def get_row_from_api_with_metadata():
|
||||||
|
return api_client.get(
|
||||||
|
reverse(
|
||||||
|
"api:database:rows:item",
|
||||||
|
kwargs={"table_id": table.id, "row_id": row.id},
|
||||||
|
)
|
||||||
|
+ "?include=metadata",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = get_row_from_api_with_metadata()
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
assert response.json()["metadata"] == {}
|
||||||
|
|
||||||
|
response = api_client.put(
|
||||||
|
reverse(
|
||||||
|
"api:premium:row_comments:notification_mode",
|
||||||
|
kwargs={"row_id": row.id, "table_id": table.id},
|
||||||
|
),
|
||||||
|
{"mode": "all"},
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_204_NO_CONTENT
|
||||||
|
|
||||||
|
response = get_row_from_api_with_metadata()
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
assert response.json()["metadata"]["row_comments_notification_mode"] == "all"
|
||||||
|
|
|
@ -326,13 +326,22 @@ export const actions = {
|
||||||
/**
|
/**
|
||||||
* Forcefully update the notification mode for a comments on a row.
|
* Forcefully update the notification mode for a comments on a row.
|
||||||
*/
|
*/
|
||||||
async forceUpdateNotificationMode({ commit }, { tableId, rowId, mode }) {
|
async forceUpdateNotificationMode({ dispatch }, { tableId, rowId, mode }) {
|
||||||
|
const updateFunction = () => mode.toString()
|
||||||
|
const rowMetadataType = 'row_comments_notification_mode'
|
||||||
await updateRowMetadataInViews(
|
await updateRowMetadataInViews(
|
||||||
this,
|
this,
|
||||||
tableId,
|
tableId,
|
||||||
rowId,
|
rowId,
|
||||||
'row_comments_notification_mode',
|
rowMetadataType,
|
||||||
() => mode.toString()
|
updateFunction
|
||||||
|
)
|
||||||
|
// Let's also make sure the local copy of the row edit modal is updated in case the
|
||||||
|
// row is not in any view buffer.
|
||||||
|
dispatch(
|
||||||
|
'rowModal/updateRowMetadata',
|
||||||
|
{ rowId, rowMetadataType, updateFunction },
|
||||||
|
{ root: true }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,14 @@ import {
|
||||||
extractRowReadOnlyValues,
|
extractRowReadOnlyValues,
|
||||||
prepareNewOldAndUpdateRequestValues,
|
prepareNewOldAndUpdateRequestValues,
|
||||||
prepareRowForRequest,
|
prepareRowForRequest,
|
||||||
|
updateRowMetadataType,
|
||||||
|
getRowMetadata,
|
||||||
} from '@baserow/modules/database/utils/row'
|
} from '@baserow/modules/database/utils/row'
|
||||||
import { getDefaultSearchModeFromEnv } from '@baserow/modules/database/utils/search'
|
import { getDefaultSearchModeFromEnv } from '@baserow/modules/database/utils/search'
|
||||||
|
|
||||||
export function populateRow(row, metadata = {}) {
|
export function populateRow(row, metadata = {}) {
|
||||||
row._ = {
|
row._ = {
|
||||||
metadata,
|
metadata: getRowMetadata(row, metadata),
|
||||||
// Whether the row should be displayed based on the current activeSearchTerm term.
|
// Whether the row should be displayed based on the current activeSearchTerm term.
|
||||||
matchSearch: true,
|
matchSearch: true,
|
||||||
// Contains the specific field ids which match the activeSearchTerm term.
|
// Contains the specific field ids which match the activeSearchTerm term.
|
||||||
|
@ -212,18 +214,7 @@ export const mutations = {
|
||||||
Object.assign(row, values)
|
Object.assign(row, values)
|
||||||
},
|
},
|
||||||
UPDATE_ROW_METADATA(state, { row, rowMetadataType, updateFunction }) {
|
UPDATE_ROW_METADATA(state, { row, rowMetadataType, updateFunction }) {
|
||||||
const currentValue = row._.metadata[rowMetadataType]
|
updateRowMetadataType(row, rowMetadataType, updateFunction)
|
||||||
const newValue = updateFunction(currentValue)
|
|
||||||
|
|
||||||
if (
|
|
||||||
!Object.prototype.hasOwnProperty.call(row._.metadata, rowMetadataType)
|
|
||||||
) {
|
|
||||||
const metaDataCopy = clone(row._.metadata)
|
|
||||||
metaDataCopy[rowMetadataType] = newValue
|
|
||||||
Vue.set(row._, 'metadata', metaDataCopy)
|
|
||||||
} else {
|
|
||||||
Vue.set(row._.metadata, rowMetadataType, newValue)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
SET_ADHOC_FILTERING(state, adhocFiltering) {
|
SET_ADHOC_FILTERING(state, adhocFiltering) {
|
||||||
state.adhocFiltering = adhocFiltering
|
state.adhocFiltering = adhocFiltering
|
||||||
|
|
|
@ -17,11 +17,13 @@ import {
|
||||||
extractRowReadOnlyValues,
|
extractRowReadOnlyValues,
|
||||||
prepareNewOldAndUpdateRequestValues,
|
prepareNewOldAndUpdateRequestValues,
|
||||||
prepareRowForRequest,
|
prepareRowForRequest,
|
||||||
|
updateRowMetadataType,
|
||||||
|
getRowMetadata,
|
||||||
} from '@baserow/modules/database/utils/row'
|
} from '@baserow/modules/database/utils/row'
|
||||||
|
|
||||||
export function populateRow(row, metadata = {}) {
|
export function populateRow(row, metadata = {}) {
|
||||||
row._ = {
|
row._ = {
|
||||||
metadata,
|
metadata: getRowMetadata(row, metadata),
|
||||||
dragging: false,
|
dragging: false,
|
||||||
}
|
}
|
||||||
return row
|
return row
|
||||||
|
@ -180,18 +182,7 @@ export const mutations = {
|
||||||
Object.assign(row, values)
|
Object.assign(row, values)
|
||||||
},
|
},
|
||||||
UPDATE_ROW_METADATA(state, { row, rowMetadataType, updateFunction }) {
|
UPDATE_ROW_METADATA(state, { row, rowMetadataType, updateFunction }) {
|
||||||
const currentValue = row._.metadata[rowMetadataType]
|
updateRowMetadataType(row, rowMetadataType, updateFunction)
|
||||||
const newValue = updateFunction(currentValue)
|
|
||||||
|
|
||||||
if (
|
|
||||||
!Object.prototype.hasOwnProperty.call(row._.metadata, rowMetadataType)
|
|
||||||
) {
|
|
||||||
const metaDataCopy = clone(row._.metadata)
|
|
||||||
metaDataCopy[rowMetadataType] = newValue
|
|
||||||
Vue.set(row._, 'metadata', metaDataCopy)
|
|
||||||
} else {
|
|
||||||
Vue.set(row._.metadata, rowMetadataType, newValue)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
SET_ADHOC_FILTERING(state, adhocFiltering) {
|
SET_ADHOC_FILTERING(state, adhocFiltering) {
|
||||||
state.adhocFiltering = adhocFiltering
|
state.adhocFiltering = adhocFiltering
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import bufferedRows from '@baserow/modules/database/store/view/bufferedRows'
|
import bufferedRows from '@baserow/modules/database/store/view/bufferedRows'
|
||||||
import TimelineService from '@baserow_premium/services/views/timeline'
|
import TimelineService from '@baserow_premium/services/views/timeline'
|
||||||
|
import { getRowMetadata } from '@baserow/modules/database/utils/row'
|
||||||
|
|
||||||
export function populateRow(row, metadata = {}) {
|
export function populateRow(row, metadata = {}) {
|
||||||
row._ = {
|
row._ = {
|
||||||
metadata,
|
metadata: getRowMetadata(row, metadata),
|
||||||
}
|
}
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
@ -32,7 +33,6 @@ export const actions = {
|
||||||
fields,
|
fields,
|
||||||
initialRowArguments: {
|
initialRowArguments: {
|
||||||
includeFieldOptions: true,
|
includeFieldOptions: true,
|
||||||
includeRowMetadata: false,
|
|
||||||
},
|
},
|
||||||
adhocFiltering,
|
adhocFiltering,
|
||||||
adhocSorting,
|
adhocSorting,
|
||||||
|
|
|
@ -7,8 +7,13 @@ const groupGetNameCalls = callGrouper(GRACE_DELAY)
|
||||||
|
|
||||||
export default (client) => {
|
export default (client) => {
|
||||||
return {
|
return {
|
||||||
get(tableId, rowId) {
|
get(tableId, rowId, includeMetadata = true) {
|
||||||
return client.get(`/database/rows/table/${tableId}/${rowId}/`)
|
const searchParams = {}
|
||||||
|
if (includeMetadata) {
|
||||||
|
searchParams.include = 'metadata'
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams(searchParams).toString()
|
||||||
|
return client.get(`/database/rows/table/${tableId}/${rowId}/?${params}`)
|
||||||
},
|
},
|
||||||
fetchAll({
|
fetchAll({
|
||||||
tableId,
|
tableId,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { updateRowMetadataType } from '@baserow/modules/database/utils/row'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This store exists to always keep a copy of the row that's being edited via the
|
* This store exists to always keep a copy of the row that's being edited via the
|
||||||
* row edit modal. It sometimes happen that row from the original source, where it was
|
* row edit modal. It sometimes happen that row from the original source, where it was
|
||||||
|
@ -53,6 +55,13 @@ export const mutations = {
|
||||||
UPDATE_ROW(state, { componentId, row }) {
|
UPDATE_ROW(state, { componentId, row }) {
|
||||||
Object.assign(state.rows[componentId].row, row)
|
Object.assign(state.rows[componentId].row, row)
|
||||||
},
|
},
|
||||||
|
UPDATE_ROW_METADATA(state, { rowId, rowMetadataType, updateFunction }) {
|
||||||
|
Object.values(state.rows)
|
||||||
|
.filter((data) => data.row.id === rowId)
|
||||||
|
.forEach((data) =>
|
||||||
|
updateRowMetadataType(data.row, rowMetadataType, updateFunction)
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
@ -102,6 +111,14 @@ export const actions = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* If a row is open in the modal but it's not present in the buffer, we need to
|
||||||
|
* manually update the metadata of the row. This is used for example to update the
|
||||||
|
* notification_mode setting of a row.
|
||||||
|
*/
|
||||||
|
updateRowMetadata({ commit }, { rowId, rowMetadataType, updateFunction }) {
|
||||||
|
commit('UPDATE_ROW_METADATA', { rowId, rowMetadataType, updateFunction })
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
|
|
@ -16,6 +16,8 @@ import {
|
||||||
extractRowReadOnlyValues,
|
extractRowReadOnlyValues,
|
||||||
prepareNewOldAndUpdateRequestValues,
|
prepareNewOldAndUpdateRequestValues,
|
||||||
prepareRowForRequest,
|
prepareRowForRequest,
|
||||||
|
updateRowMetadataType,
|
||||||
|
getRowMetadata,
|
||||||
} from '@baserow/modules/database/utils/row'
|
} from '@baserow/modules/database/utils/row'
|
||||||
import { getDefaultSearchModeFromEnv } from '@baserow/modules/database/utils/search'
|
import { getDefaultSearchModeFromEnv } from '@baserow/modules/database/utils/search'
|
||||||
import fieldOptionsStoreFactory from '@baserow/modules/database/store/view/fieldOptions'
|
import fieldOptionsStoreFactory from '@baserow/modules/database/store/view/fieldOptions'
|
||||||
|
@ -59,13 +61,16 @@ export default ({ service, customPopulateRow, fieldOptions }) => {
|
||||||
|
|
||||||
const populateRow = (row, metadata = {}) => {
|
const populateRow = (row, metadata = {}) => {
|
||||||
if (customPopulateRow) {
|
if (customPopulateRow) {
|
||||||
customPopulateRow(row)
|
customPopulateRow(row, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the metadata to the row so that it can be used in the front-end.
|
||||||
if (row._ == null) {
|
if (row._ == null) {
|
||||||
row._ = {
|
row._ = {
|
||||||
metadata,
|
metadata: getRowMetadata(row, metadata),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matching rows for front-end only search is not yet properly
|
// Matching rows for front-end only search is not yet properly
|
||||||
// supported and tested in this store mixin. Only server-side search
|
// supported and tested in this store mixin. Only server-side search
|
||||||
// implementation is finished.
|
// implementation is finished.
|
||||||
|
@ -270,18 +275,7 @@ export default ({ service, customPopulateRow, fieldOptions }) => {
|
||||||
row._.matchSearch = matchSearch
|
row._.matchSearch = matchSearch
|
||||||
},
|
},
|
||||||
UPDATE_ROW_METADATA(state, { row, rowMetadataType, updateFunction }) {
|
UPDATE_ROW_METADATA(state, { row, rowMetadataType, updateFunction }) {
|
||||||
const currentValue = row._.metadata[rowMetadataType]
|
updateRowMetadataType(row, rowMetadataType, updateFunction)
|
||||||
const newValue = updateFunction(currentValue)
|
|
||||||
|
|
||||||
if (
|
|
||||||
!Object.prototype.hasOwnProperty.call(row._.metadata, rowMetadataType)
|
|
||||||
) {
|
|
||||||
const metaDataCopy = clone(row._.metadata)
|
|
||||||
metaDataCopy[rowMetadataType] = newValue
|
|
||||||
Vue.set(row._, 'metadata', metaDataCopy)
|
|
||||||
} else {
|
|
||||||
Vue.set(row._.metadata, rowMetadataType, newValue)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
SET_ADHOC_FILTERING(state, adhocFiltering) {
|
SET_ADHOC_FILTERING(state, adhocFiltering) {
|
||||||
state.adhocFiltering = adhocFiltering
|
state.adhocFiltering = adhocFiltering
|
||||||
|
@ -507,7 +501,8 @@ export default ({ service, customPopulateRow, fieldOptions }) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
data.results.forEach((row, index) => {
|
data.results.forEach((row, index) => {
|
||||||
rows[rangeToFetch.offset + index] = populateRow(row)
|
const metadata = extractRowMetadata(data, row.id)
|
||||||
|
rows[rangeToFetch.offset + index] = populateRow(row, metadata)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (includeFieldOptions) {
|
if (includeFieldOptions) {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import bufferedRows from '@baserow/modules/database/store/view/bufferedRows'
|
import bufferedRows from '@baserow/modules/database/store/view/bufferedRows'
|
||||||
import GalleryService from '@baserow/modules/database/services/view/gallery'
|
import GalleryService from '@baserow/modules/database/services/view/gallery'
|
||||||
|
import { getRowMetadata } from '@baserow/modules/database/utils/row'
|
||||||
|
|
||||||
export function populateRow(row, metadata = {}) {
|
export function populateRow(row, metadata = {}) {
|
||||||
row._ = {
|
row._ = {
|
||||||
metadata,
|
metadata: getRowMetadata(row, metadata),
|
||||||
dragging: false,
|
dragging: false,
|
||||||
}
|
}
|
||||||
return row
|
return row
|
||||||
|
|
|
@ -23,6 +23,8 @@ import {
|
||||||
prepareRowForRequest,
|
prepareRowForRequest,
|
||||||
prepareNewOldAndUpdateRequestValues,
|
prepareNewOldAndUpdateRequestValues,
|
||||||
extractRowReadOnlyValues,
|
extractRowReadOnlyValues,
|
||||||
|
updateRowMetadataType,
|
||||||
|
getRowMetadata,
|
||||||
} from '@baserow/modules/database/utils/row'
|
} from '@baserow/modules/database/utils/row'
|
||||||
import { getDefaultSearchModeFromEnv } from '@baserow/modules/database/utils/search'
|
import { getDefaultSearchModeFromEnv } from '@baserow/modules/database/utils/search'
|
||||||
import { fieldValuesAreEqualInObjects } from '@baserow/modules/database/utils/groupBy'
|
import { fieldValuesAreEqualInObjects } from '@baserow/modules/database/utils/groupBy'
|
||||||
|
@ -32,7 +34,7 @@ const ORDER_STEP_BEFORE = '0.00000000000000000001'
|
||||||
|
|
||||||
export function populateRow(row, metadata = {}) {
|
export function populateRow(row, metadata = {}) {
|
||||||
row._ = {
|
row._ = {
|
||||||
metadata,
|
metadata: getRowMetadata(row, metadata),
|
||||||
persistentId: uuid(),
|
persistentId: uuid(),
|
||||||
loading: false,
|
loading: false,
|
||||||
hover: false,
|
hover: false,
|
||||||
|
@ -50,6 +52,7 @@ export function populateRow(row, metadata = {}) {
|
||||||
selected: false,
|
selected: false,
|
||||||
selectedFieldId: -1,
|
selectedFieldId: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,18 +442,7 @@ export const mutations = {
|
||||||
row[`field_${field.id}`] = value
|
row[`field_${field.id}`] = value
|
||||||
},
|
},
|
||||||
UPDATE_ROW_METADATA(state, { row, rowMetadataType, updateFunction }) {
|
UPDATE_ROW_METADATA(state, { row, rowMetadataType, updateFunction }) {
|
||||||
const currentValue = row._.metadata[rowMetadataType]
|
updateRowMetadataType(row, rowMetadataType, updateFunction)
|
||||||
const newValue = updateFunction(currentValue)
|
|
||||||
|
|
||||||
if (
|
|
||||||
!Object.prototype.hasOwnProperty.call(row._.metadata, rowMetadataType)
|
|
||||||
) {
|
|
||||||
const metaDataCopy = clone(row._.metadata)
|
|
||||||
metaDataCopy[rowMetadataType] = newValue
|
|
||||||
Vue.set(row._, 'metadata', metaDataCopy)
|
|
||||||
} else {
|
|
||||||
Vue.set(row._.metadata, rowMetadataType, newValue)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
FINALIZE_ROWS_IN_BUFFER(state, { oldRows, newRows, fields }) {
|
FINALIZE_ROWS_IN_BUFFER(state, { oldRows, newRows, fields }) {
|
||||||
const stateRowsCopy = { ...state.rows }
|
const stateRowsCopy = { ...state.rows }
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import { clone } from '@baserow/modules/core/utils/object'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes a row to make sure that the values are according to what the API expects.
|
* Serializes a row to make sure that the values are according to what the API expects.
|
||||||
*
|
*
|
||||||
|
@ -103,3 +106,29 @@ export function extractRowReadOnlyValues(row, allFields, registry) {
|
||||||
})
|
})
|
||||||
return readOnlyValues
|
return readOnlyValues
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call the given updateFunction with the current value of the row metadata type and
|
||||||
|
* set the new value. If the row metadata type does not exist yet, it will be
|
||||||
|
* created.
|
||||||
|
*/
|
||||||
|
export function updateRowMetadataType(row, rowMetadataType, updateFunction) {
|
||||||
|
const currentValue = row._.metadata[rowMetadataType]
|
||||||
|
const newValue = updateFunction(currentValue)
|
||||||
|
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(row._.metadata, rowMetadataType)) {
|
||||||
|
const metaDataCopy = clone(row._.metadata)
|
||||||
|
metaDataCopy[rowMetadataType] = newValue
|
||||||
|
Vue.set(row._, 'metadata', metaDataCopy)
|
||||||
|
} else {
|
||||||
|
Vue.set(row._.metadata, rowMetadataType, newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the metadata of a row. If the metadata does not exist yet, it will be created
|
||||||
|
* as an empty object.
|
||||||
|
*/
|
||||||
|
export function getRowMetadata(row, metadata = {}) {
|
||||||
|
return { ...metadata, ...(row.metadata || {}) }
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue