diff --git a/backend/src/baserow/contrib/database/api/rows/fields.py b/backend/src/baserow/contrib/database/api/rows/fields.py new file mode 100644 index 000000000..840ebfd0c --- /dev/null +++ b/backend/src/baserow/contrib/database/api/rows/fields.py @@ -0,0 +1,8 @@ +from rest_framework import serializers + +from baserow.contrib.database.api.utils import extract_user_field_names_from_params + + +class UserFieldNamesField(serializers.BooleanField): + def to_internal_value(self, data): + return extract_user_field_names_from_params({"user_field_names": data}) diff --git a/backend/src/baserow/contrib/database/api/rows/serializers.py b/backend/src/baserow/contrib/database/api/rows/serializers.py index 9c3109227..8a2136847 100644 --- a/backend/src/baserow/contrib/database/api/rows/serializers.py +++ b/backend/src/baserow/contrib/database/api/rows/serializers.py @@ -9,6 +9,7 @@ from rest_framework import serializers from baserow.api.search.serializers import SearchQueryParamSerializer from baserow.api.utils import get_serializer_class +from baserow.contrib.database.api.rows.fields import UserFieldNamesField from baserow.contrib.database.fields.registries import field_type_registry from baserow.contrib.database.rows.models import RowHistory from baserow.contrib.database.rows.registries import row_metadata_registry @@ -241,8 +242,9 @@ def get_example_row_serializer_class(example_type="get", user_field_names=False) optional_user_field_names_info = "" if user_field_names: optional_user_field_names_info = ( - " If the GET parameter `user_field_names` is provided then the key will " - "instead be the actual name of the field." + " If the GET parameter user_field_names is provided and its value is " + "one of the following: `y`, `yes`, `true`, `t`, `on`, `1`, or empty, " + "then the key will instead be the actual name of the field." ) for i, field_type in enumerate(field_types): @@ -334,6 +336,12 @@ def remap_serialized_row_to_user_field_names( return new_row +class UserFieldNamesSerializer(serializers.Serializer): + user_field_names = UserFieldNamesField( + required=False, default=False, allow_null=True + ) + + class MoveRowQueryParamsSerializer(serializers.Serializer): before_id = serializers.IntegerField(required=False) @@ -346,8 +354,9 @@ class BatchCreateRowsQueryParamsSerializer(serializers.Serializer): before = serializers.IntegerField(required=False) -class ListRowsQueryParamsSerializer(SearchQueryParamSerializer): - user_field_names = serializers.BooleanField(required=False, default=False) +class ListRowsQueryParamsSerializer( + SearchQueryParamSerializer, UserFieldNamesSerializer +): order_by = serializers.CharField(required=False) include = serializers.CharField(required=False) exclude = serializers.CharField(required=False) @@ -395,8 +404,9 @@ def get_example_batch_rows_serializer_class(example_type="get", user_field_names return class_object -class GetRowAdjacentSerializer(SearchQueryParamSerializer, serializers.Serializer): - user_field_names = serializers.BooleanField(required=False, default=False) +class GetRowAdjacentSerializer( + SearchQueryParamSerializer, UserFieldNamesSerializer, serializers.Serializer +): previous = serializers.BooleanField(required=False, default=False) view_id = serializers.IntegerField(required=False) diff --git a/backend/src/baserow/contrib/database/api/rows/views.py b/backend/src/baserow/contrib/database/api/rows/views.py index 3d817171c..8690d1578 100644 --- a/backend/src/baserow/contrib/database/api/rows/views.py +++ b/backend/src/baserow/contrib/database/api/rows/views.py @@ -47,7 +47,10 @@ from baserow.contrib.database.api.rows.serializers import GetRowAdjacentSerializ 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.errors import ERROR_NO_PERMISSION_TO_TABLE -from baserow.contrib.database.api.utils import get_include_exclude_fields +from baserow.contrib.database.api.utils import ( + extract_user_field_names_from_params, + get_include_exclude_fields, +) from baserow.contrib.database.api.views.errors import ( ERROR_VIEW_DOES_NOT_EXIST, ERROR_VIEW_FILTER_TYPE_DOES_NOT_EXIST, @@ -259,9 +262,11 @@ class RowsView(APIView): location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, description=( - "A flag query parameter which if provided the returned json " - "will use the user specified field names instead of internal " - "Baserow field names (field_123 etc). " + "A flag query parameter that, if provided with one of the " + "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an " + "empty value, will cause the returned JSON to use the " + "user-specified field names instead of the internal Baserow " + "field names (e.g., field_123)." ), ), OpenApiParameter( @@ -418,9 +423,11 @@ class RowsView(APIView): location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, description=( - "A flag query parameter which if provided this endpoint will " - "expect and return the user specified field names instead of " - "internal Baserow field names (field_123 etc)." + "A flag query parameter that, if provided with one of the " + "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an " + "empty value, will cause this endpoint to expect and return the " + "user-specified field names instead of the internal Baserow " + "field names (e.g., field_123)." ), ), CLIENT_SESSION_ID_SCHEMA_PARAMETER, @@ -490,7 +497,8 @@ class RowsView(APIView): context=table, ) - user_field_names = "user_field_names" in request.GET + user_field_names = extract_user_field_names_from_params(request.GET) + model = table.get_model() validation_serializer = get_row_serializer_class( @@ -658,9 +666,11 @@ class RowView(APIView): location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, description=( - "A flag query parameter which if provided the returned json " - "will use the user specified field names instead of internal " - "Baserow field names (field_123 etc). " + "A flag query parameter that, if provided with one of the " + "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an " + "empty value, will cause the returned JSON to use the " + "user-specified field names instead of the internal Baserow " + "field names (e.g., field_123)." ), ), ], @@ -707,7 +717,7 @@ class RowView(APIView): table = TableHandler().get_table(table_id) TokenHandler().check_table_permissions(request, "read", table, False) - user_field_names = "user_field_names" in request.GET + user_field_names = extract_user_field_names_from_params(request.GET) model = table.get_model() row = RowHandler().get_row(request.user, table, row_id, model) serializer_class = get_row_serializer_class( @@ -736,9 +746,11 @@ class RowView(APIView): location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, description=( - "A flag query parameter which if provided this endpoint will " - "expect and return the user specified field names instead of " - "internal Baserow field names (field_123 etc)." + "A flag query parameter that, if provided with one of the " + "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an " + "empty value, will cause this endpoint to expect and return the " + "user-specified field names instead of the internal Baserow " + "field names (e.g., field_123)." ), ), CLIENT_SESSION_ID_SCHEMA_PARAMETER, @@ -802,7 +814,7 @@ class RowView(APIView): table = TableHandler().get_table(table_id) TokenHandler().check_table_permissions(request, "update", table, False) - user_field_names = "user_field_names" in request.GET + user_field_names = extract_user_field_names_from_params(request.GET) field_ids, field_names = None, None if user_field_names: @@ -923,9 +935,11 @@ class RowMoveView(APIView): location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, description=( - "A flag query parameter which if provided the returned json " - "will use the user specified field names instead of internal " - "Baserow field names (field_123 etc). " + "A flag query parameter that, if provided with one of the " + "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an " + "empty value, will cause the returned JSON to use the " + "user-specified field names instead of the internal Baserow " + "field names (e.g., field_123)." ), ), CLIENT_SESSION_ID_SCHEMA_PARAMETER, @@ -967,7 +981,7 @@ class RowMoveView(APIView): TokenHandler().check_table_permissions(request, "update", table, False) - user_field_names = "user_field_names" in request.GET + user_field_names = extract_user_field_names_from_params(request.GET) model = table.get_model() @@ -1015,9 +1029,11 @@ class BatchRowsView(APIView): location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, description=( - "A flag query parameter which if provided this endpoint will " - "expect and return the user specified field names instead of " - "internal Baserow field names (field_123 etc)." + "A flag query parameter that, if provided with one of the " + "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an " + "empty value, will cause this endpoint to expect and return the " + "user-specified field names instead of the internal Baserow " + "field names (e.g., field_123)." ), ), CLIENT_SESSION_ID_SCHEMA_PARAMETER, @@ -1083,7 +1099,7 @@ class BatchRowsView(APIView): TokenHandler().check_table_permissions(request, "create", table, False) model = table.get_model() - user_field_names = "user_field_names" in request.GET + user_field_names = extract_user_field_names_from_params(request.GET) before_id = query_params.get("before") before_row = ( RowHandler().get_row(request.user, table, before_id, model) @@ -1130,9 +1146,11 @@ class BatchRowsView(APIView): location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, description=( - "A flag query parameter which if provided this endpoint will " - "expect and return the user specified field names instead of " - "internal Baserow field names (field_123 etc)." + "A flag query parameter that, if provided with one of the " + "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an " + "empty value, will cause this endpoint to expect and return the " + "user-specified field names instead of the internal Baserow " + "field names (e.g., field_123)." ), ), CLIENT_SESSION_ID_SCHEMA_PARAMETER, @@ -1198,7 +1216,7 @@ class BatchRowsView(APIView): TokenHandler().check_table_permissions(request, "update", table, False) model = table.get_model() - user_field_names = "user_field_names" in request.GET + user_field_names = extract_user_field_names_from_params(request.GET) row_validation_serializer = get_row_serializer_class( model, @@ -1326,9 +1344,11 @@ class RowAdjacentView(APIView): location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, description=( - "A flag query parameter which if provided the returned json " - "will use the user specified field names instead of internal " - "Baserow field names (field_123 etc). " + "A flag query parameter that, if provided with one of the " + "following values: `y`, `yes`, `true`, `t`, `on`, `1`, or an " + "empty value, will cause the returned JSON to use the " + "user-specified field names instead of the internal Baserow " + "field names (e.g., field_123)." ), ), OpenApiParameter( diff --git a/backend/src/baserow/contrib/database/api/utils.py b/backend/src/baserow/contrib/database/api/utils.py index 387b98acb..21823e564 100644 --- a/backend/src/baserow/contrib/database/api/utils.py +++ b/backend/src/baserow/contrib/database/api/utils.py @@ -1,3 +1,4 @@ +from baserow.config.settings.utils import str_to_bool from baserow.contrib.database.fields.models import Field from baserow.contrib.database.fields.utils import get_field_id_from_field_key from baserow.core.utils import split_comma_separated_string @@ -115,3 +116,20 @@ def extract_field_ids_from_string(value): ids = [get_field_id_from_field_key(v, False) for v in value.split(",")] return [_id for _id in ids if _id is not None] + + +def extract_user_field_names_from_params(query_params): + """ + Extracts the user_field_names parameter from the query_params and returns + boolean value + """ + + value = query_params.get("user_field_names", False) + + if value is False: + return False + + if value is None or value == "": + return True + + return str_to_bool(value) diff --git a/backend/tests/baserow/contrib/database/rows/test_rows_handler.py b/backend/tests/baserow/contrib/database/rows/test_rows_handler.py index 7404946e5..b671dc078 100644 --- a/backend/tests/baserow/contrib/database/rows/test_rows_handler.py +++ b/backend/tests/baserow/contrib/database/rows/test_rows_handler.py @@ -11,6 +11,7 @@ from pyinstrument import Profiler from baserow.contrib.database.api.utils import ( extract_field_ids_from_string, + extract_user_field_names_from_params, get_include_exclude_fields, ) from baserow.contrib.database.rows.exceptions import RowDoesNotExist @@ -39,6 +40,14 @@ def test_extract_field_ids_from_string(): assert extract_field_ids_from_string("is,1,one") == [1] +def test_extract_user_field_names_from_params(): + assert extract_user_field_names_from_params({}) is False + assert extract_user_field_names_from_params({"user_field_names": None}) is True + assert extract_user_field_names_from_params({"user_field_names": ""}) is True + assert extract_user_field_names_from_params({"user_field_names": "true"}) is True + assert extract_user_field_names_from_params({"user_field_names": "false"}) is False + + @pytest.mark.django_db def test_get_include_exclude_fields(data_fixture): table = data_fixture.create_database_table() diff --git a/changelog/entries/unreleased/bug/2784_properly_handle_user_field_names_in_api_calls.json b/changelog/entries/unreleased/bug/2784_properly_handle_user_field_names_in_api_calls.json new file mode 100644 index 000000000..c055e430d --- /dev/null +++ b/changelog/entries/unreleased/bug/2784_properly_handle_user_field_names_in_api_calls.json @@ -0,0 +1,7 @@ +{ + "type": "bug", + "message": "Properly handle user_field_names in api calls", + "issue_number": 2784, + "bullet_points": [], + "created_at": "2024-07-11" +} \ No newline at end of file diff --git a/web-frontend/locales/en.json b/web-frontend/locales/en.json index 16badc309..e6e02ceae 100644 --- a/web-frontend/locales/en.json +++ b/web-frontend/locales/en.json @@ -408,7 +408,7 @@ "queryParameters": "Query parameters", "pathParameters": "Path parameters", "requestBodySchema": "Request body schema", - "userFieldNamesDescription": "When any value is provided for the `user_field_names` GET param then field names returned by this endpoint will be the actual names of the fields.\n\n If the `user_field_names` GET param is not provided, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.", + "userFieldNamesDescription": "When the `user_field_names` GET parameter is provided and its value is one of the following: `y`, `yes`, `true`, `t`, `on`, `1`, or empty string, the field names returned by this endpoint will be the actual names of the fields.\n\nIf the `user_field_names` GET parameter is not provided, or if it does not match any of the above values, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.", "singleRow": "Single", "batchRows": "Batch", "fileUploads": "File uploads" diff --git a/web-frontend/modules/database/components/docs/sections/APIDocsTableGetRow.vue b/web-frontend/modules/database/components/docs/sections/APIDocsTableGetRow.vue index 5ab3d289e..6d527f3f5 100644 --- a/web-frontend/modules/database/components/docs/sections/APIDocsTableGetRow.vue +++ b/web-frontend/modules/database/components/docs/sections/APIDocsTableGetRow.vue @@ -23,7 +23,10 @@ </h4> <ul class="api-docs__parameters"> <APIDocsParameter name="user_field_names" :optional="true" type="any"> - <MarkdownIt :content="$t('apiDocs.userFieldNamesDescription')" /> + <MarkdownIt + class="api-docs__content" + :content="$t('apiDocs.userFieldNamesDescription')" + /> </APIDocsParameter> </ul> </div> diff --git a/web-frontend/modules/database/locales/en.json b/web-frontend/modules/database/locales/en.json index fc68b658a..07ffcbe55 100644 --- a/web-frontend/modules/database/locales/en.json +++ b/web-frontend/modules/database/locales/en.json @@ -141,7 +141,7 @@ "description": "To list rows in the *{name}* table a `GET` request has to be made to the *{name}* endpoint. The response is paginated and by default the first page is returned. The correct page can be fetched by providing the `page` and `size` query parameters.", "page": "Defines which page of rows should be returned.", "size": "Defines how many rows should be returned per page.", - "userFieldNames": "When any value is provided for the `user_field_names` GET param then field names returned by this endpoint will be the actual names of the fields.\n\n If the `user_field_names` GET param is not provided, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.\n\n Additionally when `user_field_names` is set then the behaviour of the other GET parameters `order_by`, `include` and `exclude` changes. They instead expect comma separated lists of the actual field names instead.", + "userFieldNames": "When the `user_field_names` GET parameter is provided and its value is one of the following: `y`, `yes`, `true`, `t`, `on`, `1`, or empty string, the field names returned by this endpoint will be the actual names of the fields.\n\nIf the `user_field_names` GET parameter is not provided, or if it does not match any of the above values, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.\n\n Additionally when `user_field_names` is set then the behaviour of the other GET parameters `order_by`, `include` and `exclude` changes. They instead expect comma separated lists of the actual field names instead.", "search": "If provided only rows with data that matches the search query are going to be returned.", "orderBy": "Optionally the rows can be ordered by fields separated by comma. By default or if prepended with a '+' a field is ordered in ascending (A-Z) order, but by prepending the field with a '-' it can be ordered descending (Z-A).\n\n #### With `user_field_names`:\n\n `order_by` should be a comma separated list of the field names to order by. For example if you provide the following GET parameter `order_by=My Field,-My Field 2` the rows will ordered by the field called `My Field` in ascending order. If some fields have the same value, that subset will be ordered by the field called `My Field 2` in descending order.\n\n Ensure fields with names starting with a `+` or `-` are explicitly prepended with another `+` or `-`. E.g `+-Name`.\n\n The name of fields containing commas must be surrounded by quotes: `\"Name ,\"`. If the field names contain quotes, then they must be escaped using the `\\` character. Eg: `Name \\\"`. \n\n#### Without `user_field_names`:\n\n `order_by` should be a comma separated list of `field_` followed by the id of the field to order by. For example if you provide the following GET parameter `order_by=field_1,-field_2` the rows will ordered by `field_1` in ascending order. If some fields have the same value, that subset will be ordered by `field_2` in descending order.", "filters": "Rows can optionally be filtered using the same view filters that are available for the views. This parameter accepts a JSON serialized string containing the filter tree to apply to this view. The filter tree is a nested structure containing the filters that need to be applied. \n\n#### With `user_field_names`:\n\nAn example of a valid filter tree is the following: `{\"filter_type\": \"AND\", \"filters\": [{\"field\": \"Name\", \"type\": \"equal\", \"value\": \"test\"}]}`.\n\n#### Without `user_field_names`:\n\nFor example, if you optionally provide the following GET parameter: `{\"filter_type\": \"AND\", \"filters\": [{\"field\": 1, \"type\": \"equal\", \"value\": \"test\"}]}`\n\nPlease note that if this parameter is provided, all other `filter__{field}__{filter}` will be ignored, as well as the filter_type parameter.",