1
0
Fork 0
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 

See merge request 
This commit is contained in:
Bram Wiepjes 2020-12-21 19:43:54 +00:00
commit 597f26da44
7 changed files with 217 additions and 6 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database
changelog.md
web-frontend/modules/database/pages

View file

@ -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:

View file

@ -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

View file

@ -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.

View file

@ -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',

View file

@ -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()

View file

@ -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)

View file

@ -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>