1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 06:15:36 +00:00

Merge branch '2727-filter-lookups' into 'develop'

Allow filtering text-based lookups

Closes 

See merge request 
This commit is contained in:
Petr Stribny 2024-07-16 15:04:21 +00:00
commit f931be08a1
17 changed files with 2253 additions and 16 deletions

View file

@ -385,6 +385,28 @@ class DatabaseConfig(AppConfig):
view_filter_type_registry.register(UserIsViewFilterType())
view_filter_type_registry.register(UserIsNotViewFilterType())
from .views.array_view_filters import (
HasEmptyValueViewFilterType,
HasNotEmptyValueViewFilterType,
HasNotValueContainsViewFilterType,
HasNotValueContainsWordViewFilterType,
HasNotValueEqualViewFilterType,
HasValueContainsViewFilterType,
HasValueContainsWordViewFilterType,
HasValueEqualViewFilterType,
HasValueLengthIsLowerThanViewFilterType,
)
view_filter_type_registry.register(HasValueEqualViewFilterType())
view_filter_type_registry.register(HasNotValueEqualViewFilterType())
view_filter_type_registry.register(HasValueContainsViewFilterType())
view_filter_type_registry.register(HasNotValueContainsViewFilterType())
view_filter_type_registry.register(HasValueContainsWordViewFilterType())
view_filter_type_registry.register(HasNotValueContainsWordViewFilterType())
view_filter_type_registry.register(HasValueLengthIsLowerThanViewFilterType())
view_filter_type_registry.register(HasEmptyValueViewFilterType())
view_filter_type_registry.register(HasNotEmptyValueViewFilterType())
from .views.view_aggregations import (
AverageViewAggregationType,
DecileViewAggregationType,

View file

@ -77,6 +77,14 @@ from baserow.contrib.database.api.views.errors import (
)
from baserow.contrib.database.db.functions import RandomUUID
from baserow.contrib.database.export_serialized import DatabaseExportSerializedStructure
from baserow.contrib.database.fields.filter_support import (
FilterNotSupportedException,
HasValueContainsFilterSupport,
HasValueContainsWordFilterSupport,
HasValueEmptyFilterSupport,
HasValueFilterSupport,
HasValueLengthIsLowerThanFilterSupport,
)
from baserow.contrib.database.formula import (
BASEROW_FORMULA_TYPE_ALLOWED_FIELDS,
BaserowExpression,
@ -149,6 +157,7 @@ from .expressions import extract_jsonb_array_values_to_single_string
from .field_cache import FieldCache
from .field_filters import (
AnnotatedQ,
OptionallyAnnotatedQ,
contains_filter,
contains_word_filter,
filename_contains_filter,
@ -4308,7 +4317,14 @@ class PhoneNumberFieldType(CollationSortMixin, CharFieldMatchingRegexFieldType):
return collate_expression(Value(value))
class FormulaFieldType(ReadOnlyFieldType):
class FormulaFieldType(
HasValueEmptyFilterSupport,
HasValueFilterSupport,
HasValueContainsFilterSupport,
HasValueContainsWordFilterSupport,
HasValueLengthIsLowerThanFilterSupport,
ReadOnlyFieldType,
):
type = "formula"
model_class = FormulaField
@ -4467,6 +4483,83 @@ class FormulaFieldType(ReadOnlyFieldType):
rich_value=rich_value,
)
def get_in_array_empty_query(self, field_name, model_field, field: FormulaField):
(
field_instance,
field_type,
) = self._get_field_instance_and_type_from_formula_field(field)
if not isinstance(field_type, HasValueEmptyFilterSupport):
raise FilterNotSupportedException()
return field_type.get_in_array_empty_query(
field_name, model_field, field_instance
)
def get_in_array_is_query(
self,
field_name: str,
value: str,
model_field: models.Field,
field: FormulaField,
) -> Q | OptionallyAnnotatedQ:
(
field_instance,
field_type,
) = self._get_field_instance_and_type_from_formula_field(field)
if not isinstance(field_type, HasValueFilterSupport):
raise FilterNotSupportedException()
return field_type.get_in_array_is_query(
field_name, value, model_field, field_instance
)
def get_in_array_contains_query(
self, field_name, value, model_field, field: FormulaField
):
(
field_instance,
field_type,
) = self._get_field_instance_and_type_from_formula_field(field)
if not isinstance(field_type, HasValueContainsFilterSupport):
raise FilterNotSupportedException()
return field_type.get_in_array_contains_query(
field_name, value, model_field, field_instance
)
def get_in_array_contains_word_query(
self, field_name, value, model_field, field: FormulaField
):
(
field_instance,
field_type,
) = self._get_field_instance_and_type_from_formula_field(field)
if not isinstance(field_type, HasValueContainsWordFilterSupport):
raise FilterNotSupportedException()
return field_type.get_in_array_contains_word_query(
field_name, value, model_field, field_instance
)
def get_in_array_length_is_lower_than_query(
self, field_name, value, model_field, field: FormulaField
):
(
field_instance,
field_type,
) = self._get_field_instance_and_type_from_formula_field(field)
if not isinstance(field_type, HasValueLengthIsLowerThanFilterSupport):
raise FilterNotSupportedException()
return field_type.get_in_array_length_is_lower_than_query(
field_name, value, model_field, field_instance
)
def contains_query(self, field_name, value, model_field, field: FormulaField):
(
field_instance,

View file

@ -0,0 +1,150 @@
import re
import typing
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.db.models import BooleanField, F, Q, Value
from baserow.contrib.database.fields.field_filters import (
AnnotatedQ,
OptionallyAnnotatedQ,
)
from baserow.contrib.database.formula.expression_generator.django_expressions import (
JSONArrayContainsValueExpr,
JSONArrayContainsValueLengthLowerThanExpr,
JSONArrayContainsValueSimilarToExpr,
)
if typing.TYPE_CHECKING:
from baserow.contrib.database.fields.models import Field
class FilterNotSupportedException(Exception):
pass
class HasValueEmptyFilterSupport:
def get_in_array_empty_query(
self, field_name: str, model_field: models.Field, field: "Field"
) -> OptionallyAnnotatedQ:
"""
Specifies a Q expression to filter empty values contained in an array.
:param field_name: The name of the field.
:param model_field: The field's actual django field model instance.
:param field: The related field's instance.
:return: A Q or AnnotatedQ filter given value.
"""
return Q(**{f"{field_name}__contains": Value([{"value": ""}], JSONField())})
class HasValueFilterSupport:
def get_in_array_is_query(
self, field_name: str, value: str, model_field: models.Field, field: "Field"
) -> OptionallyAnnotatedQ:
"""
Specifies a Q expression to filter exact values contained in an array.
:param field_name: The name of the field.
:param value: The value to check if it is contained in array.
:param model_field: The field's actual django field model instance.
:param field: The related field's instance.
:return: A Q or AnnotatedQ filter given value.
"""
if not value:
return Q()
return Q(**{f"{field_name}__contains": Value([{"value": value}], JSONField())})
class HasValueContainsFilterSupport:
def get_in_array_contains_query(
self, field_name: str, value: str, model_field: models.Field, field: "Field"
) -> OptionallyAnnotatedQ:
"""
Specifies a Q expression to filter values in an array that contain a
specific value.
:param field_name: The name of the field.
:param value: The value to check if it is contained in array.
:param model_field: The field's actual django field model instance.
:param field: The related field's instance.
:return: A Q or AnnotatedQ filter given value.
"""
if not value:
return Q()
annotation_query = JSONArrayContainsValueExpr(
F(field_name), Value(f"%{value}%"), output_field=BooleanField()
)
hashed_value = hash(value)
return AnnotatedQ(
annotation={
f"{field_name}_has_value_contains_{hashed_value}": annotation_query
},
q={f"{field_name}_has_value_contains_{hashed_value}": True},
)
class HasValueContainsWordFilterSupport:
def get_in_array_contains_word_query(
self, field_name: str, value: str, model_field: models.Field, field: "Field"
) -> OptionallyAnnotatedQ:
"""
Specifies a Q expression to filter values in an array that contain a
specific word.
:param field_name: The name of the field.
:param value: The value to check if it is contained in array.
:param model_field: The field's actual django field model instance.
:param field: The related field's instance.
:return: A Q or AnnotatedQ filter given value.
"""
value = value.strip()
if not value:
return Q()
value = re.escape(value.upper())
annotation_query = JSONArrayContainsValueSimilarToExpr(
F(field_name), Value(f"%\\m{value}\\M%"), output_field=BooleanField()
)
hashed_value = hash(value)
return AnnotatedQ(
annotation={
f"{field_name}_has_value_contains_word_{hashed_value}": annotation_query
},
q={f"{field_name}_has_value_contains_word_{hashed_value}": True},
)
class HasValueLengthIsLowerThanFilterSupport:
def get_in_array_length_is_lower_than_query(
self, field_name: str, value: str, model_field: models.Field, field: "Field"
) -> OptionallyAnnotatedQ:
"""
Specifies a Q expression to filter values in an array that has lower
than length.
:param field_name: The name of the field.
:param value: The value representing the length to use for the check.
:param model_field: The field's actual django field model instance.
:param field: The related field's instance.
:return: A Q or AnnotatedQ filter given value.
"""
value = value.strip()
if not value:
return Q()
converted_value = int(value)
annotation_query = JSONArrayContainsValueLengthLowerThanExpr(
F(field_name), Value(converted_value), output_field=BooleanField()
)
hashed_value = hash(value)
return AnnotatedQ(
annotation={
f"{field_name}_has_value_length_is_lower_than_{hashed_value}": annotation_query
},
q={f"{field_name}_has_value_length_is_lower_than_{hashed_value}": True},
)

View file

@ -116,18 +116,12 @@ class JSONArray(Func):
)
class FileNameContainsExpr(Expression):
# fmt: off
template = (
f"""
EXISTS(
SELECT attached_files ->> 'visible_name'
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as attached_files
WHERE UPPER(attached_files ->> 'visible_name') LIKE UPPER(%(value)s)
)
""" # nosec B608
)
# fmt: on
class BaserowFilterExpression(Expression):
"""
Baserow expression that works with field_name and value
to provide expressions for filters. To use, subclass and
define the template.
"""
def __init__(self, field_name: F, value: Value, output_field: Field):
super().__init__(output_field=output_field)
@ -159,3 +153,59 @@ class FileNameContainsExpr(Expression):
"value": sql_value,
}
return template % data, params_value
class FileNameContainsExpr(BaserowFilterExpression):
# fmt: off
template = (
f"""
EXISTS(
SELECT attached_files ->> 'visible_name'
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as attached_files
WHERE UPPER(attached_files ->> 'visible_name') LIKE UPPER(%(value)s)
)
""" # nosec B608
)
# fmt: on
class JSONArrayContainsValueExpr(BaserowFilterExpression):
# fmt: off
template = (
f"""
EXISTS(
SELECT filtered_field ->> 'value'
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
WHERE UPPER(filtered_field ->> 'value') LIKE UPPER(%(value)s)
)
""" # nosec B608
)
# fmt: on
class JSONArrayContainsValueSimilarToExpr(BaserowFilterExpression):
# fmt: off
template = (
f"""
EXISTS(
SELECT filtered_field ->> 'value'
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
WHERE UPPER(filtered_field ->> 'value') SIMILAR TO %(value)s
)
""" # nosec B608 %(value)s
)
# fmt: on
class JSONArrayContainsValueLengthLowerThanExpr(BaserowFilterExpression):
# fmt: off
template = (
f"""
EXISTS(
SELECT filtered_field ->> 'value'
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
WHERE LENGTH(filtered_field ->> 'value') < %(value)s
)
""" # nosec B608 %(value)s
)
# fmt: on

View file

@ -20,6 +20,14 @@ from baserow.contrib.database.fields.expressions import (
json_extract_path,
)
from baserow.contrib.database.fields.field_sortings import OptionallyAnnotatedOrderBy
from baserow.contrib.database.fields.filter_support import (
FilterNotSupportedException,
HasValueContainsFilterSupport,
HasValueContainsWordFilterSupport,
HasValueEmptyFilterSupport,
HasValueFilterSupport,
HasValueLengthIsLowerThanFilterSupport,
)
from baserow.contrib.database.fields.mixins import get_date_time_format
from baserow.contrib.database.fields.utils.duration import (
D_H_M_S,
@ -95,6 +103,11 @@ class BaserowFormulaBaseTextType(BaserowFormulaTypeHasEmptyBaserowExpression):
class BaserowFormulaTextType(
HasValueEmptyFilterSupport,
HasValueFilterSupport,
HasValueContainsFilterSupport,
HasValueContainsWordFilterSupport,
HasValueLengthIsLowerThanFilterSupport,
BaserowFormulaBaseTextType,
BaserowFormulaTypeHasEmptyBaserowExpression,
BaserowFormulaValidType,
@ -961,7 +974,14 @@ class BaserowFormulaSingleFileType(BaserowJSONBObjectBaseType):
)
class BaserowFormulaArrayType(BaserowFormulaValidType):
class BaserowFormulaArrayType(
HasValueEmptyFilterSupport,
HasValueFilterSupport,
HasValueContainsFilterSupport,
HasValueContainsWordFilterSupport,
HasValueLengthIsLowerThanFilterSupport,
BaserowFormulaValidType,
):
type = "array"
user_overridable_formatting_option_fields = [
"array_formula_type",
@ -1123,6 +1143,46 @@ class BaserowFormulaArrayType(BaserowFormulaValidType):
def contains_query(self, field_name, value, model_field, field):
return Q()
def get_in_array_is_query(self, field_name, value, model_field, field):
if not isinstance(self.sub_type, HasValueFilterSupport):
raise FilterNotSupportedException()
return self.sub_type.get_in_array_is_query(
field_name, value, model_field, field
)
def get_in_array_empty_query(self, field_name, model_field, field):
if not isinstance(self.sub_type, HasValueEmptyFilterSupport):
raise FilterNotSupportedException()
return self.sub_type.get_in_array_empty_query(field_name, model_field, field)
def get_in_array_contains_query(self, field_name, value, model_field, field):
if not isinstance(self.sub_type, HasValueContainsFilterSupport):
raise FilterNotSupportedException()
return self.sub_type.get_in_array_contains_query(
field_name, value, model_field, field
)
def get_in_array_contains_word_query(self, field_name, value, model_field, field):
if not isinstance(self.sub_type, HasValueContainsWordFilterSupport):
raise FilterNotSupportedException()
return self.sub_type.get_in_array_contains_word_query(
field_name, value, model_field, field
)
def get_in_array_length_is_lower_than_query(
self, field_name, value, model_field, field
):
if not isinstance(self.sub_type, HasValueLengthIsLowerThanFilterSupport):
raise FilterNotSupportedException()
return self.sub_type.get_in_array_length_is_lower_than_query(
field_name, value, model_field, field
)
def get_alter_column_prepare_old_value(self, connection, from_field, to_field):
return "p_in = '';"

View file

@ -0,0 +1,172 @@
from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
from baserow.contrib.database.fields.field_types import FormulaFieldType
from baserow.contrib.database.fields.filter_support import (
FilterNotSupportedException,
HasValueContainsFilterSupport,
HasValueContainsWordFilterSupport,
HasValueEmptyFilterSupport,
HasValueFilterSupport,
HasValueLengthIsLowerThanFilterSupport,
)
from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.formula import BaserowFormulaTextType
from .registries import ViewFilterType
from .view_filters import NotViewFilterTypeMixin
class HasEmptyValueViewFilterType(ViewFilterType):
"""
The filter can be used to check for empty condition for
items in an array.
"""
type = "has_empty_value"
compatible_field_types = [
FormulaFieldType.compatible_with_formula_types(
FormulaFieldType.array_of(BaserowFormulaTextType.type),
),
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
try:
field_type = field_type_registry.get_by_model(field)
if not isinstance(field_type, HasValueEmptyFilterSupport):
raise FilterNotSupportedException()
return field_type.get_in_array_empty_query(field_name, model_field, field)
except Exception:
return self.default_filter_on_exception()
class HasNotEmptyValueViewFilterType(
NotViewFilterTypeMixin, HasEmptyValueViewFilterType
):
type = "has_not_empty_value"
class HasValueEqualViewFilterType(ViewFilterType):
"""
The filter can be used to check for "is" condition for
items in an array.
"""
type = "has_value_equal"
compatible_field_types = [
FormulaFieldType.compatible_with_formula_types(
FormulaFieldType.array_of(BaserowFormulaTextType.type),
),
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
try:
field_type = field_type_registry.get_by_model(field)
if not isinstance(field_type, HasValueFilterSupport):
raise FilterNotSupportedException()
return field_type.get_in_array_is_query(
field_name, value, model_field, field
)
except Exception:
return self.default_filter_on_exception()
class HasNotValueEqualViewFilterType(
NotViewFilterTypeMixin, HasValueEqualViewFilterType
):
type = "has_not_value_equal"
class HasValueContainsViewFilterType(ViewFilterType):
"""
The filter can be used to check for "contains" condition for
items in an array.
"""
type = "has_value_contains"
compatible_field_types = [
FormulaFieldType.compatible_with_formula_types(
FormulaFieldType.array_of(BaserowFormulaTextType.type),
),
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
try:
field_type = field_type_registry.get_by_model(field)
if not isinstance(field_type, HasValueContainsFilterSupport):
raise FilterNotSupportedException()
return field_type.get_in_array_contains_query(
field_name, value, model_field, field
)
except Exception:
return self.default_filter_on_exception()
class HasNotValueContainsViewFilterType(
NotViewFilterTypeMixin, HasValueContainsViewFilterType
):
type = "has_not_value_contains"
class HasValueContainsWordViewFilterType(ViewFilterType):
"""
The filter can be used to check for "contains word" condition
for items in an array.
"""
type = "has_value_contains_word"
compatible_field_types = [
FormulaFieldType.compatible_with_formula_types(
FormulaFieldType.array_of(BaserowFormulaTextType.type),
),
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
try:
field_type = field_type_registry.get_by_model(field)
if not isinstance(field_type, HasValueContainsWordFilterSupport):
raise FilterNotSupportedException()
return field_type.get_in_array_contains_word_query(
field_name, value, model_field, field
)
except Exception:
return self.default_filter_on_exception()
class HasNotValueContainsWordViewFilterType(
NotViewFilterTypeMixin, HasValueContainsWordViewFilterType
):
type = "has_not_value_contains_word"
class HasValueLengthIsLowerThanViewFilterType(ViewFilterType):
"""
The filter can be used to check for "length is lower than" condition
for items in an array.
"""
type = "has_value_length_is_lower_than"
compatible_field_types = [
FormulaFieldType.compatible_with_formula_types(
FormulaFieldType.array_of(BaserowFormulaTextType.type),
),
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
try:
field_type = field_type_registry.get_by_model(field)
if not isinstance(field_type, HasValueLengthIsLowerThanFilterSupport):
raise FilterNotSupportedException()
return field_type.get_in_array_length_is_lower_than_query(
field_name, value, model_field, field
)
except Exception:
return self.default_filter_on_exception()

View file

@ -0,0 +1,754 @@
from dataclasses import dataclass
from django.contrib.auth.models import AbstractUser
import pytest
from baserow.contrib.database.fields.models import Field, LinkRowField, LookupField
from baserow.contrib.database.rows.handler import RowHandler
from baserow.contrib.database.table.models import GeneratedTableModel, Table
from baserow.contrib.database.views.handler import ViewHandler
from baserow.contrib.database.views.models import GridView
@dataclass
class ArrayFiltersSetup:
user: AbstractUser
table: Table
model: GeneratedTableModel
other_table_model: GeneratedTableModel
grid_view: GridView
link_row_field: LinkRowField
lookup_field: LookupField
target_field: Field
row_handler: RowHandler
view_handler: ViewHandler
def text_field_factory(data_fixture, table, user):
return data_fixture.create_text_field(name="target", user=user, table=table)
def long_text_field_factory(data_fixture, table, user):
return data_fixture.create_long_text_field(name="target", user=user, table=table)
def setup(data_fixture, target_field_factory):
user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user)
table = data_fixture.create_database_table(user=user, database=database)
other_table = data_fixture.create_database_table(user=user, database=database)
target_field = target_field_factory(data_fixture, other_table, user)
link_row_field = data_fixture.create_link_row_field(
name="link", table=table, link_row_table=other_table
)
lookup_field = data_fixture.create_lookup_field(
table=table,
through_field=link_row_field,
target_field=target_field,
through_field_name=link_row_field.name,
target_field_name=target_field.name,
setup_dependencies=False,
)
grid_view = data_fixture.create_grid_view(table=table)
view_handler = ViewHandler()
row_handler = RowHandler()
model = table.get_model()
other_table_model = other_table.get_model()
return ArrayFiltersSetup(
user=user,
table=table,
other_table_model=other_table_model,
target_field=target_field,
row_handler=row_handler,
grid_view=grid_view,
link_row_field=link_row_field,
lookup_field=lookup_field,
view_handler=view_handler,
model=model,
)
@pytest.mark.parametrize(
"target_field_factory", [text_field_factory, long_text_field_factory]
)
@pytest.mark.django_db
def test_has_empty_value_filter_text_field_types(data_fixture, target_field_factory):
test_setup = setup(data_fixture, target_field_factory)
other_row_A = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "A"}
)
other_row_B = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "B"}
)
other_row_empty = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": ""}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [
other_row_A.id,
other_row_empty.id,
]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": []},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_B.id]},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_empty_value",
value="",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 1
assert row_1.id in ids
@pytest.mark.parametrize(
"target_field_factory", [text_field_factory, long_text_field_factory]
)
@pytest.mark.django_db
def test_has_not_empty_value_filter_text_field_types(
data_fixture, target_field_factory
):
test_setup = setup(data_fixture, target_field_factory)
other_row_A = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "A"}
)
other_row_B = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "B"}
)
other_row_empty = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": ""}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [
other_row_A.id,
other_row_empty.id,
]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": []},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_B.id]},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_not_empty_value",
value="",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_2.id in ids
assert row_3.id in ids
@pytest.mark.parametrize(
"target_field_factory", [text_field_factory, long_text_field_factory]
)
@pytest.mark.django_db
def test_has_value_equal_filter_text_field_types(data_fixture, target_field_factory):
test_setup = setup(data_fixture, target_field_factory)
other_row_A = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "A"}
)
other_row_B = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "B"}
)
other_row_C = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "C"}
)
other_row_a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "a"}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_A.id, other_row_B.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_a.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_B.id, other_row_a.id]
},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_value_equal",
value="A",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 1
assert row_1.id in ids
view_filter.value = "a"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_2.id in ids
assert row_3.id in ids
view_filter.value = "C"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 0
view_filter.value = ""
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 3
@pytest.mark.parametrize(
"target_field_factory", [text_field_factory, long_text_field_factory]
)
@pytest.mark.django_db
def test_has_not_value_equal_filter_text_field_types(
data_fixture, target_field_factory
):
test_setup = setup(data_fixture, target_field_factory)
other_row_A = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "A"}
)
other_row_B = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "B"}
)
other_row_C = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "C"}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_A.id, other_row_B.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": []},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_B.id]},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_not_value_equal",
value="A",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_2.id in ids
assert row_3.id in ids
view_filter.value = "a"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 3
view_filter.value = "C"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 3
view_filter.value = ""
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 3
@pytest.mark.parametrize(
"target_field_factory", [text_field_factory, long_text_field_factory]
)
@pytest.mark.django_db
def test_has_value_contains_filter_text_field_types(data_fixture, target_field_factory):
test_setup = setup(data_fixture, target_field_factory)
other_row_John_Smith = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "John Smith"}
)
other_row_Anna_Smith = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "Anna Smith"}
)
other_row_John_Wick = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "John Wick"}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_John_Smith.id]},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": []},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_Anna_Smith.id]},
)
row_4 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_John_Wick.id]},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_value_contains",
value="smith",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_3.id in ids
view_filter.value = "john"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_4.id in ids
view_filter.value = ""
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 4
@pytest.mark.parametrize(
"target_field_factory", [text_field_factory, long_text_field_factory]
)
@pytest.mark.django_db
def test_has_not_value_contains_filter_text_field_types(
data_fixture, target_field_factory
):
test_setup = setup(data_fixture, target_field_factory)
other_row_John_Smith = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "John Smith"}
)
other_row_Anna_Smith = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "Anna Smith"}
)
other_row_John_Wick = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "John Wick"}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_John_Smith.id]},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": []},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_Anna_Smith.id]},
)
row_4 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_John_Wick.id]},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_not_value_contains",
value="smith",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_2.id in ids
assert row_4.id in ids
view_filter.value = "john"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_2.id in ids
assert row_3.id in ids
view_filter.value = ""
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 4
@pytest.mark.parametrize(
"target_field_factory", [text_field_factory, long_text_field_factory]
)
@pytest.mark.django_db
def test_has_value_contains_word_filter_text_field_types(
data_fixture, target_field_factory
):
test_setup = setup(data_fixture, target_field_factory)
other_row_1 = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "This is a sentence."}
)
other_row_2 = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "Another Sentence."}
)
other_row_3 = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": ""}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_1.id, other_row_3.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_3.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_2.id]},
)
row_4 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": []},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_value_contains_word",
value="sentence",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_3.id in ids
view_filter.value = "Sentence"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_3.id in ids
view_filter.value = ""
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 4
@pytest.mark.parametrize(
"target_field_factory", [text_field_factory, long_text_field_factory]
)
@pytest.mark.django_db
def test_has_not_value_contains_word_filter_text_field_types(
data_fixture, target_field_factory
):
test_setup = setup(data_fixture, target_field_factory)
other_row_1 = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "This is a sentence."}
)
other_row_2 = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "Another Sentence."}
)
other_row_3 = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": ""}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_1.id, other_row_3.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_3.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_2.id]},
)
row_4 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": []},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_not_value_contains_word",
value="sentence",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_2.id in ids
assert row_4.id in ids
view_filter.value = "Sentence"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_2.id in ids
assert row_4.id in ids
view_filter.value = ""
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 4
@pytest.mark.parametrize(
"target_field_factory", [text_field_factory, long_text_field_factory]
)
@pytest.mark.django_db
def test_has_value_length_is_lower_than_text_field_types(
data_fixture, target_field_factory
):
test_setup = setup(data_fixture, target_field_factory)
other_row_10a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "aaaaaaaaaa"}
)
other_row_5a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "aaaaa"}
)
other_row_0a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": ""}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_10a.id]},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_0a.id, other_row_10a.id]
},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_5a.id]},
)
row_4 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": []},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_value_length_is_lower_than",
value="10",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_2.id in ids
assert row_3.id in ids
view_filter.value = "5"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 1
assert row_2.id in ids
view_filter.value = "11"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 3
assert row_1.id in ids
assert row_2.id in ids
assert row_3.id in ids
view_filter.value = ""
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 4

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Add array filters for formula arrays based on text",
"issue_number": 2727,
"bullet_points": [],
"created_at": "2024-07-10"
}

View file

@ -174,6 +174,15 @@
"password": "A write-only field that holds a hashed password. The value will be `null` if not set, or `true` if it has been set. It accepts a string to set it."
},
"viewFilter": {
"hasEmptyValue": "has empty value",
"hasNotEmptyValue": "doesn't have empty value",
"hasValueEqual": "has value equal",
"hasNotValueEqual": "doesn't have value equal",
"hasValueContains": "has value contains",
"hasNotValueContains": "doesn't have value contains",
"hasValueContainsWord": "has value contains word",
"hasNotValueContainsWord": "doesn't have value contains word",
"hasValueLengthIsLowerThan": "has value length is lower than",
"contains": "contains",
"containsNot": "doesn't contain",
"containsWord": "contains word",

View file

@ -0,0 +1,210 @@
import ViewFilterTypeText from '@baserow/modules/database/components/view/ViewFilterTypeText'
import ViewFilterTypeNumber from '@baserow/modules/database/components/view/ViewFilterTypeNumber'
import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
import { ViewFilterType } from '@baserow/modules/database/viewFilters'
export class HasEmptyValueViewFilterType extends ViewFilterType {
static getType() {
return 'has_empty_value'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasEmptyValue')
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
}
matches(cellValue, filterValue, field, fieldType) {
return fieldType.getHasEmptyValueFilterFunction(field)(cellValue)
}
}
export class HasNotEmptyValueViewFilterType extends ViewFilterType {
static getType() {
return 'has_not_empty_value'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasNotEmptyValue')
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
}
matches(cellValue, filterValue, field, fieldType) {
return !fieldType.getHasEmptyValueFilterFunction(field)(cellValue)
}
}
export class HasValueEqualViewFilterType extends ViewFilterType {
static getType() {
return 'has_value_equal'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasValueEqual')
}
getInputComponent(field) {
return ViewFilterTypeText
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
}
matches(cellValue, filterValue, field, fieldType) {
return fieldType.hasValueEqualFilter(cellValue, filterValue, field)
}
}
export class HasNotValueEqualViewFilterType extends ViewFilterType {
static getType() {
return 'has_not_value_equal'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasNotValueEqual')
}
getInputComponent(field) {
return ViewFilterTypeText
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
}
matches(cellValue, filterValue, field, fieldType) {
return fieldType.hasNotValueEqualFilter(cellValue, filterValue, field)
}
}
export class HasValueContainsViewFilterType extends ViewFilterType {
static getType() {
return 'has_value_contains'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasValueContains')
}
getInputComponent(field) {
return ViewFilterTypeText
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
}
matches(cellValue, filterValue, field, fieldType) {
return fieldType.hasValueContainsFilter(cellValue, filterValue, field)
}
}
export class HasNotValueContainsViewFilterType extends ViewFilterType {
static getType() {
return 'has_not_value_contains'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasNotValueContains')
}
getInputComponent(field) {
return ViewFilterTypeText
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
}
matches(cellValue, filterValue, field, fieldType) {
return fieldType.hasNotValueContainsFilter(cellValue, filterValue, field)
}
}
export class HasValueContainsWordViewFilterType extends ViewFilterType {
static getType() {
return 'has_value_contains_word'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasValueContainsWord')
}
getInputComponent(field) {
return ViewFilterTypeText
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
}
matches(cellValue, filterValue, field, fieldType) {
return fieldType.hasValueContainsWordFilter(cellValue, filterValue, field)
}
}
export class HasNotValueContainsWordViewFilterType extends ViewFilterType {
static getType() {
return 'has_not_value_contains_word'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasNotValueContainsWord')
}
getInputComponent(field) {
return ViewFilterTypeText
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
}
matches(cellValue, filterValue, field, fieldType) {
return fieldType.hasNotValueContainsWordFilter(
cellValue,
filterValue,
field
)
}
}
export class HasValueLengthIsLowerThanViewFilterType extends ViewFilterType {
static getType() {
return 'has_value_length_is_lower_than'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasValueLengthIsLowerThan')
}
getInputComponent(field) {
return ViewFilterTypeNumber
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(text)')]
}
matches(cellValue, filterValue, field, fieldType) {
return fieldType.getHasValueLengthIsLowerThanFilterFunction(field)(
cellValue,
filterValue
)
}
}

View file

@ -0,0 +1,82 @@
import {
genericHasValueEqualFilter,
genericHasValueContainsFilter,
genericHasValueContainsWordFilter,
genericHasEmptyValueFilter,
genericHasValueLengthLowerThanFilter,
} from '@baserow/modules/database/utils/fieldFilters'
export function fieldSupportsFilter(fieldType, filterMixin) {
for (const [key, value] of Object.entries(filterMixin)) {
/* eslint no-prototype-builtins: "off" */
if (!fieldType.prototype.hasOwnProperty(key)) {
fieldType.prototype[key] = value
}
}
}
export const hasEmptyValueFilterMixin = {
getHasEmptyValueFilterFunction(field) {
return genericHasEmptyValueFilter
},
}
export const hasValueEqualFilterMixin = {
getHasValueEqualFilterFunction(field) {
return genericHasValueEqualFilter
},
hasValueEqualFilter(cellValue, filterValue, field) {
return (
filterValue === '' ||
this.getHasValueEqualFilterFunction(field)(cellValue, filterValue)
)
},
hasNotValueEqualFilter(cellValue, filterValue, field) {
return (
filterValue === '' ||
!this.getHasValueEqualFilterFunction(field)(cellValue, filterValue)
)
},
}
export const hasValueContainsFilterMixin = {
getHasValueContainsFilterFunction(field) {
return genericHasValueContainsFilter
},
hasValueContainsFilter(cellValue, filterValue, field) {
return (
filterValue === '' ||
this.getHasValueContainsFilterFunction(field)(cellValue, filterValue)
)
},
hasNotValueContainsFilter(cellValue, filterValue, field) {
return (
filterValue === '' ||
!this.getHasValueContainsFilterFunction(field)(cellValue, filterValue)
)
},
}
export const hasValueContainsWordFilterMixin = {
getHasValueContainsWordFilterFunction(field) {
return genericHasValueContainsWordFilter
},
hasValueContainsWordFilter(cellValue, filterValue, field) {
return (
filterValue === '' ||
this.getHasValueContainsWordFilterFunction(field)(cellValue, filterValue)
)
},
hasNotValueContainsWordFilter(cellValue, filterValue, field) {
return (
filterValue === '' ||
!this.getHasValueContainsWordFilterFunction(field)(cellValue, filterValue)
)
},
}
export const hasValueLengthIsLowerThanFilterMixin = {
getHasValueLengthIsLowerThanFilterFunction(field) {
return genericHasValueLengthLowerThanFilter
},
}

View file

@ -15,7 +15,14 @@ import {
isValidEmail,
isValidURL,
} from '@baserow/modules/core/utils/string'
import {
fieldSupportsFilter,
hasEmptyValueFilterMixin,
hasValueContainsFilterMixin,
hasValueEqualFilterMixin,
hasValueContainsWordFilterMixin,
hasValueLengthIsLowerThanFilterMixin,
} from '@baserow/modules/database/fieldFilterCompatibility'
import moment from '@baserow/modules/core/moment'
import guessFormat from 'moment-guess'
import { Registerable } from '@baserow/modules/core/registry'
@ -135,6 +142,12 @@ import FormViewFieldMultipleLinkRow from '@baserow/modules/database/components/v
import FormViewFieldMultipleSelectCheckboxes from '@baserow/modules/database/components/view/form/FormViewFieldMultipleSelectCheckboxes'
import FormViewFieldSingleSelectRadios from '@baserow/modules/database/components/view/form/FormViewFieldSingleSelectRadios'
import {
BaserowFormulaArrayType,
BaserowFormulaCharType,
BaserowFormulaTextType,
} from '@baserow/modules/database/formula/formulaTypes'
import { trueValues } from '@baserow/modules/core/utils/constants'
import {
getDateMomentFormat,
@ -3658,6 +3671,31 @@ export class FormulaFieldType extends FieldType {
const subType = this.app.$registry.get('formula_type', field.formula_type)
return subType.canRepresentFiles(field)
}
getHasEmptyValueFilterFunction(field) {
const subType = this.app.$registry.get('formula_type', field.formula_type)
return subType.getHasEmptyValueFilterFunction(field)
}
getHasValueEqualFilterFunction(field) {
const subType = this.app.$registry.get('formula_type', field.formula_type)
return subType.getHasValueEqualFilterFunction(field)
}
getHasValueContainsFilterFunction(field) {
const subType = this.app.$registry.get('formula_type', field.formula_type)
return subType.getHasValueContainsFilterFunction(field)
}
getHasValueContainsWordFilterFunction(field) {
const subType = this.app.$registry.get('formula_type', field.formula_type)
return subType.getHasValueContainsWordFilterFunction(field)
}
getHasValueLengthIsLowerThanFilterFunction(field) {
const subType = this.app.$registry.get('formula_type', field.formula_type)
return subType.getHasValueLengthIsLowerThanFilterFunction(field)
}
}
export class CountFieldType extends FormulaFieldType {
@ -4202,3 +4240,37 @@ export class PasswordFieldType extends FieldType {
return RowHistoryFieldPassword
}
}
fieldSupportsFilter(FormulaFieldType, hasEmptyValueFilterMixin)
fieldSupportsFilter(BaserowFormulaArrayType, hasEmptyValueFilterMixin)
fieldSupportsFilter(BaserowFormulaTextType, hasEmptyValueFilterMixin)
fieldSupportsFilter(BaserowFormulaCharType, hasEmptyValueFilterMixin)
fieldSupportsFilter(FormulaFieldType, hasValueEqualFilterMixin)
fieldSupportsFilter(BaserowFormulaArrayType, hasValueEqualFilterMixin)
fieldSupportsFilter(BaserowFormulaTextType, hasValueEqualFilterMixin)
fieldSupportsFilter(BaserowFormulaCharType, hasValueEqualFilterMixin)
fieldSupportsFilter(FormulaFieldType, hasValueContainsFilterMixin)
fieldSupportsFilter(BaserowFormulaArrayType, hasValueContainsFilterMixin)
fieldSupportsFilter(BaserowFormulaTextType, hasValueContainsFilterMixin)
fieldSupportsFilter(BaserowFormulaCharType, hasValueContainsFilterMixin)
fieldSupportsFilter(FormulaFieldType, hasValueContainsWordFilterMixin)
fieldSupportsFilter(BaserowFormulaArrayType, hasValueContainsWordFilterMixin)
fieldSupportsFilter(BaserowFormulaTextType, hasValueContainsWordFilterMixin)
fieldSupportsFilter(BaserowFormulaCharType, hasValueContainsWordFilterMixin)
fieldSupportsFilter(FormulaFieldType, hasValueLengthIsLowerThanFilterMixin)
fieldSupportsFilter(
BaserowFormulaArrayType,
hasValueLengthIsLowerThanFilterMixin
)
fieldSupportsFilter(
BaserowFormulaTextType,
hasValueLengthIsLowerThanFilterMixin
)
fieldSupportsFilter(
BaserowFormulaCharType,
hasValueLengthIsLowerThanFilterMixin
)

View file

@ -666,6 +666,46 @@ export class BaserowFormulaArrayType extends BaserowFormulaTypeDefinition {
canGroupByInView() {
return false
}
getHasEmptyValueFilterFunction(field) {
const subType = this.app.$registry.get(
'formula_type',
field.array_formula_type
)
return subType.getHasEmptyValueFilterFunction(field)
}
getHasValueEqualFilterFunction(field) {
const subType = this.app.$registry.get(
'formula_type',
field.array_formula_type
)
return subType.getHasValueEqualFilterFunction(field)
}
getHasValueContainsFilterFunction(field) {
const subType = this.app.$registry.get(
'formula_type',
field.array_formula_type
)
return subType.getHasValueContainsFilterFunction(field)
}
getHasValueContainsWordFilterFunction(field) {
const subType = this.app.$registry.get(
'formula_type',
field.array_formula_type
)
return subType.getHasValueContainsWordFilterFunction(field)
}
getHasValueLengthIsLowerThanFilterFunction(field) {
const subType = this.app.$registry.get(
'formula_type',
field.array_formula_type
)
return subType.getHasValueLengthIsLowerThanFilterFunction(field)
}
}
export class BaserowFormulaFileType extends BaserowFormulaTypeDefinition {

View file

@ -95,6 +95,17 @@ import {
DateAfterOrEqualViewFilterType,
DateEqualsDayOfMonthViewFilterType,
} from '@baserow/modules/database/viewFilters'
import {
HasValueEqualViewFilterType,
HasEmptyValueViewFilterType,
HasNotEmptyValueViewFilterType,
HasNotValueEqualViewFilterType,
HasValueContainsViewFilterType,
HasNotValueContainsViewFilterType,
HasValueContainsWordViewFilterType,
HasNotValueContainsWordViewFilterType,
HasValueLengthIsLowerThanViewFilterType,
} from '@baserow/modules/database/arrayViewFilters'
import {
CSVImporterType,
PasteImporterType,
@ -427,6 +438,36 @@ export default (context) => {
new DateAfterDaysAgoViewFilterType(context)
)
// END
app.$registry.register('viewFilter', new HasEmptyValueViewFilterType(context))
app.$registry.register(
'viewFilter',
new HasNotEmptyValueViewFilterType(context)
)
app.$registry.register('viewFilter', new HasValueEqualViewFilterType(context))
app.$registry.register(
'viewFilter',
new HasNotValueEqualViewFilterType(context)
)
app.$registry.register(
'viewFilter',
new HasValueContainsViewFilterType(context)
)
app.$registry.register(
'viewFilter',
new HasNotValueContainsViewFilterType(context)
)
app.$registry.register(
'viewFilter',
new HasValueContainsWordViewFilterType(context)
)
app.$registry.register(
'viewFilter',
new HasNotValueContainsWordViewFilterType(context)
)
app.$registry.register(
'viewFilter',
new HasValueLengthIsLowerThanViewFilterType(context)
)
app.$registry.register('viewFilter', new ContainsViewFilterType(context))
app.$registry.register('viewFilter', new ContainsNotViewFilterType(context))
app.$registry.register('viewFilter', new ContainsWordViewFilterType(context))

View file

@ -49,3 +49,91 @@ export function genericContainsWordFilter(
filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
return humanReadableRowValue.match(new RegExp(`\\b${filterValue}\\b`))
}
export function genericHasEmptyValueFilter(cellValue, filterValue) {
if (!Array.isArray(cellValue)) {
return false
}
for (let i = 0; i < cellValue.length; i++) {
const value = cellValue[i].value
if (value === '') {
return true
}
}
return false
}
export function genericHasValueEqualFilter(cellValue, filterValue) {
if (!Array.isArray(cellValue)) {
return false
}
for (let i = 0; i < cellValue.length; i++) {
const value = cellValue[i].value
if (value === filterValue) {
return true
}
}
return false
}
export function genericHasValueContainsFilter(cellValue, filterValue) {
if (!Array.isArray(cellValue)) {
return false
}
filterValue = filterValue.toString().toLowerCase().trim()
for (let i = 0; i < cellValue.length; i++) {
const value = cellValue[i].value.toString().toLowerCase().trim()
if (value.includes(filterValue)) {
return true
}
}
return false
}
export function genericHasValueContainsWordFilter(cellValue, filterValue) {
if (!Array.isArray(cellValue)) {
return false
}
filterValue = filterValue.toString().toLowerCase().trim()
filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
for (let i = 0; i < cellValue.length; i++) {
if (cellValue[i].value == null) {
continue
}
const value = cellValue[i].value.toString().toLowerCase().trim()
if (value.match(new RegExp(`\\b${filterValue}\\b`))) {
return true
}
}
return false
}
export function genericHasValueLengthLowerThanFilter(cellValue, filterValue) {
if (!Array.isArray(cellValue)) {
return false
}
for (let i = 0; i < cellValue.length; i++) {
if (cellValue[i].value == null) {
continue
}
const valueLength = cellValue[i].value.toString().length
if (valueLength < filterValue) {
return true
}
}
return false
}

View file

@ -197,7 +197,6 @@ export class EqualViewFilterType extends ViewFilterType {
'uuid',
'autonumber',
'duration',
FormulaFieldType.compatibleWithFormulaTypes('text', 'char', 'number'),
]
}

View file

@ -0,0 +1,388 @@
import { TestApp } from '@baserow/test/helpers/testApp'
import {
HasValueEqualViewFilterType,
HasNotValueEqualViewFilterType,
HasValueContainsViewFilterType,
HasNotValueContainsViewFilterType,
HasValueContainsWordViewFilterType,
HasNotValueContainsWordViewFilterType,
HasEmptyValueViewFilterType,
HasNotEmptyValueViewFilterType,
HasValueLengthIsLowerThanViewFilterType,
} from '@baserow/modules/database/arrayViewFilters'
import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
describe('Text-based array view filters', () => {
let testApp = null
beforeAll(() => {
testApp = new TestApp()
})
afterEach(() => {
testApp.afterEach()
})
const hasTextValueEqualCases = [
{
cellValue: [],
filterValue: 'A',
expected: false,
},
{
cellValue: [{ value: 'B' }, { value: 'A' }],
filterValue: 'A',
expected: true,
},
{
cellValue: [{ value: 'a' }],
filterValue: 'A',
expected: false,
},
{
cellValue: [{ value: 'Aa' }],
filterValue: 'A',
expected: false,
},
]
const hasValueEqualSupportedFields = [
{
TestFieldType: FormulaFieldType,
formula_type: 'array',
array_formula_type: 'text',
},
]
describe.each(hasValueEqualSupportedFields)(
'HasValueEqualViewFilterType %j',
(field) => {
test.each(hasTextValueEqualCases)(
'filter matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasValueEqualViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(testValues.expected)
}
)
}
)
describe.each(hasValueEqualSupportedFields)(
'HasNotValueEqualViewFilterType %j',
(field) => {
test.each(hasTextValueEqualCases)(
'filter not matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasNotValueEqualViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(!testValues.expected)
}
)
}
)
const hasValueContainsCases = [
{
cellValue: [],
filterValue: 'A',
expected: false,
},
{
cellValue: [{ value: 'B' }, { value: 'Aa' }],
filterValue: 'A',
expected: true,
},
{
cellValue: [{ value: 't a t' }],
filterValue: 'A',
expected: true,
},
{
cellValue: [{ value: 'C' }],
filterValue: 'A',
expected: false,
},
]
const hasValueContainsSupportedFields = [
{
TestFieldType: FormulaFieldType,
formula_type: 'array',
array_formula_type: 'text',
},
]
describe.each(hasValueContainsSupportedFields)(
'HasValueContainsViewFilterType %j',
(field) => {
test.each(hasValueContainsCases)(
'filter matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasValueContainsViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(testValues.expected)
}
)
}
)
describe.each(hasValueContainsSupportedFields)(
'HasNotValueContainsViewFilterType %j',
(field) => {
test.each(hasValueContainsCases)(
'filter not matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasNotValueContainsViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(!testValues.expected)
}
)
}
)
const hasValueContainsWordCases = [
{
cellValue: [],
filterValue: 'Word',
expected: false,
},
{
cellValue: [{ value: '...Word...' }, { value: 'Some sentence' }],
filterValue: 'Word',
expected: true,
},
{
cellValue: [{ value: 'Word' }],
filterValue: 'ord',
expected: false,
},
{
cellValue: [{ value: 'Some word in a sentence.' }],
filterValue: 'Word',
expected: true,
},
{
cellValue: [{ value: 'Some Word in a sentence.' }],
filterValue: 'word',
expected: true,
},
]
const hasValueContainsWordSupportedFields = [
{
TestFieldType: FormulaFieldType,
formula_type: 'array',
array_formula_type: 'text',
},
]
describe.each(hasValueContainsWordSupportedFields)(
'HasValueContainsWordViewFilterType %j',
(field) => {
test.each(hasValueContainsWordCases)(
'filter matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasValueContainsWordViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(testValues.expected)
}
)
}
)
describe.each(hasValueContainsWordSupportedFields)(
'HasNotValueContainsWordViewFilterType %j',
(field) => {
test.each(hasValueContainsWordCases)(
'filter not matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasNotValueContainsWordViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(!testValues.expected)
}
)
}
)
const hasEmptyValueCases = [
{
cellValue: [],
expected: false,
},
{
cellValue: [{ value: 'B' }, { value: '' }],
expected: true,
},
{
cellValue: [{ value: '' }],
expected: true,
},
{
cellValue: [{ value: 'C' }],
expected: false,
},
]
const hasEmptyValueSupportedFields = [
{
TestFieldType: FormulaFieldType,
formula_type: 'array',
array_formula_type: 'text',
},
]
describe.each(hasEmptyValueSupportedFields)(
'HasEmptyValueViewFilterType %j',
(field) => {
test.each(hasEmptyValueCases)(
'filter matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasEmptyValueViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(testValues.expected)
}
)
}
)
describe.each(hasEmptyValueSupportedFields)(
'HasNotEmptyValueViewFilterType %j',
(field) => {
test.each(hasEmptyValueCases)(
'filter not matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasNotEmptyValueViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(!testValues.expected)
}
)
}
)
const hasLengthLowerThanValueCases = [
{
cellValue: [],
filterValue: '1',
expected: false,
},
{
cellValue: [{ value: 'aaaaa' }, { value: 'aaaaaaaaaa' }],
filterValue: '6',
expected: true,
},
{
cellValue: [{ value: 'aaaaa' }],
filterValue: '5',
expected: false,
},
{
cellValue: [{ value: '' }],
filterValue: '1',
expected: true,
},
]
const hasLengthLowerThanSupportedFields = [
{
TestFieldType: FormulaFieldType,
formula_type: 'array',
array_formula_type: 'text',
},
]
describe.each(hasLengthLowerThanSupportedFields)(
'HasValueLengthIsLowerThanViewFilterType %j',
(field) => {
test.each(hasLengthLowerThanValueCases)(
'filter matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasValueLengthIsLowerThanViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(testValues.expected)
}
)
}
)
})