mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-11 07:51:20 +00:00
Merge branch '188-ability-to-select-subset-of-fields' into 'develop'
Resolve "Ability to select subset of fields" Closes #188 See merge request bramw/baserow!136
This commit is contained in:
commit
597f26da44
7 changed files with 217 additions and 6 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database
web-frontend/modules/database/pages
|
@ -118,7 +118,33 @@ class RowsView(APIView):
|
|||
'filters.\n'
|
||||
'`OR`: Indicates that the rows only have to match one of the '
|
||||
'filters.\n\n'
|
||||
'This works only if at two or more filters are provided.'
|
||||
'This works only if two or more filters are provided.'
|
||||
)
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='include',
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description=(
|
||||
'All the fields are included in the response by default. You can '
|
||||
'select a subset of fields by providing the include query '
|
||||
'parameter. If you for example provide the following GET '
|
||||
'parameter `include=field_1,field_2` then only the fields with'
|
||||
'id `1` and id `2` are going to be selected and included in the '
|
||||
'response. '
|
||||
)
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='exclude',
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description=(
|
||||
'All the fields are included in the response by default. You can '
|
||||
'select a subset of fields by providing the exclude query '
|
||||
'parameter. If you for example provide the following GET '
|
||||
'parameter `exclude=field_1,field_2` then the fields with id `1` '
|
||||
'and id `2` are going to be excluded from the selection and '
|
||||
'response.'
|
||||
)
|
||||
),
|
||||
],
|
||||
|
@ -171,11 +197,16 @@ class RowsView(APIView):
|
|||
|
||||
table = TableHandler().get_table(request.user, table_id)
|
||||
TokenHandler().check_table_permissions(request, 'read', table, False)
|
||||
|
||||
model = table.get_model()
|
||||
search = request.GET.get('search')
|
||||
order_by = request.GET.get('order_by')
|
||||
include = request.GET.get('include')
|
||||
exclude = request.GET.get('exclude')
|
||||
fields = RowHandler().get_include_exclude_fields(table, include, exclude)
|
||||
|
||||
model = table.get_model(
|
||||
fields=fields,
|
||||
field_ids=[] if fields else None
|
||||
)
|
||||
queryset = model.objects.all().enhance_by_fields()
|
||||
|
||||
if search:
|
||||
|
|
|
@ -3,11 +3,12 @@ from math import floor, ceil
|
|||
from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Max, F
|
||||
from django.db.models import Max, F, Q
|
||||
from django.db.models.fields.related import ManyToManyField
|
||||
from django.conf import settings
|
||||
|
||||
from baserow.core.exceptions import UserNotInGroupError
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
|
||||
from .exceptions import RowDoesNotExist
|
||||
|
||||
|
@ -56,6 +57,62 @@ class RowHandler:
|
|||
if str(key).isnumeric() or field_pattern.match(str(key))
|
||||
]
|
||||
|
||||
def extract_field_ids_from_string(self, value):
|
||||
"""
|
||||
Extracts the field ids from a string. Multiple ids can be separated by a comma.
|
||||
For example if you provide 'field_1,field_2' then [1, 2] is returned.
|
||||
|
||||
:param value: A string containing multiple ids separated by comma.
|
||||
:type value: str
|
||||
:return: A list containing the field ids as integers.
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
if not value:
|
||||
return []
|
||||
|
||||
return [
|
||||
int(re.sub("[^0-9]", "", str(v)))
|
||||
for v in value.split(',')
|
||||
if any(c.isdigit() for c in v)
|
||||
]
|
||||
|
||||
def get_include_exclude_fields(self, table, include=None, exclude=None):
|
||||
"""
|
||||
Returns a field queryset containing the requested fields based on the include
|
||||
and exclude parameter.
|
||||
|
||||
:param table: The table where to select the fields from. Field id's that are
|
||||
not in the table won't be included.
|
||||
:type table: Table
|
||||
:param include: The field ids that must be included. Only the provided ones
|
||||
are going to be in the returned queryset. Multiple can be provided
|
||||
separated by comma
|
||||
:type include: str
|
||||
:param exclude: The field ids that must be excluded. Only the ones that are not
|
||||
provided are going to be in the returned queryset. Multiple can be provided
|
||||
separated by comma.
|
||||
:type exclude: str
|
||||
:return: A Field's QuerySet containing the allowed fields based on the provided
|
||||
input.
|
||||
:rtype: QuerySet
|
||||
"""
|
||||
|
||||
queryset = Field.objects.filter(table=table)
|
||||
include_ids = self.extract_field_ids_from_string(include)
|
||||
exclude_ids = self.extract_field_ids_from_string(exclude)
|
||||
|
||||
if len(include_ids) == 0 and len(exclude_ids) == 0:
|
||||
return None
|
||||
|
||||
if len(include_ids) > 0:
|
||||
queryset = queryset.filter(id__in=include_ids)
|
||||
|
||||
if len(exclude_ids) > 0:
|
||||
queryset = queryset.filter(~Q(id__in=exclude_ids))
|
||||
|
||||
return queryset
|
||||
|
||||
def extract_manytomany_values(self, values, model):
|
||||
"""
|
||||
Extracts the ManyToMany values out of the values because they need to be
|
||||
|
|
|
@ -286,7 +286,7 @@ class Table(CreatedAndUpdatedOnMixin, OrderableMixin, models.Model):
|
|||
|
||||
# Create a combined list of fields that must be added and belong to the this
|
||||
# table.
|
||||
fields = fields + [field for field in fields_query]
|
||||
fields = list(fields) + [field for field in fields_query]
|
||||
|
||||
# If there are duplicate field names we have to store them in a list so we know
|
||||
# later which ones are duplicate.
|
||||
|
|
|
@ -20,6 +20,7 @@ def test_list_rows(api_client, data_fixture):
|
|||
table_2 = data_fixture.create_database_table()
|
||||
field_1 = data_fixture.create_text_field(name='Name', table=table, primary=True)
|
||||
field_2 = data_fixture.create_number_field(name='Price', table=table)
|
||||
field_3 = data_fixture.create_text_field()
|
||||
|
||||
token = TokenHandler().create_token(user, table.database.group, 'Good')
|
||||
wrong_token = TokenHandler().create_token(user, table.database.group, 'Wrong')
|
||||
|
@ -85,6 +86,29 @@ def test_list_rows(api_client, data_fixture):
|
|||
assert response_json['results'][0][f'field_{field_2.id}'] == 50
|
||||
assert response_json['results'][0]['order'] == '1.00000000000000000000'
|
||||
|
||||
url = reverse('api:database:rows:list', kwargs={'table_id': table.id})
|
||||
response = api_client.get(
|
||||
f'{url}?include=field_{field_1.id},field_{field_3.id}',
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {jwt_token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert f'field_{field_1.id}' in response_json['results'][0]
|
||||
assert f'field_{field_2.id}' not in response_json['results'][0]
|
||||
assert f'field_{field_3.id}' not in response_json['results'][0]
|
||||
|
||||
url = reverse('api:database:rows:list', kwargs={'table_id': table.id})
|
||||
response = api_client.get(
|
||||
f'{url}?exclude=field_{field_1.id}',
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {jwt_token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert f'field_{field_1.id}' not in response_json['results'][0]
|
||||
assert f'field_{field_2.id}' in response_json['results'][0]
|
||||
|
||||
url = reverse('api:database:rows:list', kwargs={'table_id': table.id})
|
||||
response = api_client.get(
|
||||
f'{url}?size=2&page=1',
|
||||
|
|
|
@ -21,6 +21,82 @@ def test_get_field_ids_from_dict():
|
|||
}) == [1, 2, 3]
|
||||
|
||||
|
||||
def test_extract_field_ids_from_string():
|
||||
handler = RowHandler()
|
||||
assert handler.extract_field_ids_from_string(None) == []
|
||||
assert handler.extract_field_ids_from_string('not,something') == []
|
||||
assert handler.extract_field_ids_from_string('field_1,field_2') == [1, 2]
|
||||
assert handler.extract_field_ids_from_string('field_22,test_8,999') == [22, 8, 999]
|
||||
assert handler.extract_field_ids_from_string('is,1,one') == [1]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_include_exclude_fields(data_fixture):
|
||||
table = data_fixture.create_database_table()
|
||||
table_2 = data_fixture.create_database_table()
|
||||
field_1 = data_fixture.create_text_field(table=table, order=1)
|
||||
field_2 = data_fixture.create_text_field(table=table, order=2)
|
||||
field_3 = data_fixture.create_text_field(table=table_2, order=3)
|
||||
|
||||
row_handler = RowHandler()
|
||||
|
||||
assert row_handler.get_include_exclude_fields(
|
||||
table,
|
||||
include=None,
|
||||
exclude=None
|
||||
) is None
|
||||
|
||||
assert row_handler.get_include_exclude_fields(
|
||||
table,
|
||||
include='',
|
||||
exclude=''
|
||||
) is None
|
||||
|
||||
fields = row_handler.get_include_exclude_fields(
|
||||
table,
|
||||
f'field_{field_1.id}'
|
||||
)
|
||||
assert len(fields) == 1
|
||||
assert fields[0].id == field_1.id
|
||||
|
||||
fields = row_handler.get_include_exclude_fields(
|
||||
table,
|
||||
f'field_{field_1.id},field_9999,field_{field_2.id}'
|
||||
)
|
||||
assert len(fields) == 2
|
||||
assert fields[0].id == field_1.id
|
||||
assert fields[1].id == field_2.id
|
||||
|
||||
fields = row_handler.get_include_exclude_fields(
|
||||
table,
|
||||
None,
|
||||
f'field_{field_1.id},field_9999'
|
||||
)
|
||||
assert len(fields) == 1
|
||||
assert fields[0].id == field_2.id
|
||||
|
||||
fields = row_handler.get_include_exclude_fields(
|
||||
table,
|
||||
f'field_{field_1.id},field_{field_2}',
|
||||
f'field_{field_1.id}'
|
||||
)
|
||||
assert len(fields) == 1
|
||||
assert fields[0].id == field_2.id
|
||||
|
||||
fields = row_handler.get_include_exclude_fields(
|
||||
table,
|
||||
f'field_{field_3.id}'
|
||||
)
|
||||
assert len(fields) == 0
|
||||
|
||||
fields = row_handler.get_include_exclude_fields(
|
||||
table,
|
||||
None,
|
||||
f'field_{field_3.id}'
|
||||
)
|
||||
assert len(fields) == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_extract_manytomany_values(data_fixture):
|
||||
row_handler = RowHandler()
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
fields and rows.
|
||||
* Made the file name editable.
|
||||
* Made the rows orderable and added the ability to insert a row at a given position.
|
||||
* Made it possible to include or exclude specific fields when listing rows via the API.
|
||||
|
||||
## Released (2020-12-01)
|
||||
|
||||
|
|
|
@ -363,7 +363,29 @@
|
|||
<br />
|
||||
<code class="api-docs__code">OR</code>: Indicates that the rows
|
||||
only have to match one of the filters. <br /><br />
|
||||
This works only if at two or more filters are provided.
|
||||
This works only if two or more filters are provided.
|
||||
</APIDocsParameter>
|
||||
<APIDocsParameter name="include" :optional="true" type="string">
|
||||
All the fields are included in the response by default. You can
|
||||
select a subset of fields by providing the include query
|
||||
parameter. If you for example provide the following GET
|
||||
parameter
|
||||
<code class="api-docs__code">include=field_1,field_2</code>
|
||||
then only the fields with id
|
||||
<code class="api-docs__code">1</code> and id
|
||||
<code class="api-docs__code">2</code> are going to be selected
|
||||
and included in the response.
|
||||
</APIDocsParameter>
|
||||
<APIDocsParameter name="exclude" :optional="true" type="string">
|
||||
All the fields are included in the response by default. You can
|
||||
select a subset of fields by providing the exclude query
|
||||
parameter. If you for example provide the following GET
|
||||
parameter
|
||||
<code class="api-docs__code">exclude=field_1,field_2</code>
|
||||
then the fields with id
|
||||
<code class="api-docs__code">1</code> and id
|
||||
<code class="api-docs__code">2</code> are going to be excluded
|
||||
from the selection and response.
|
||||
</APIDocsParameter>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Reference in a new issue