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 #2727 See merge request baserow/baserow!2472
This commit is contained in:
commit
f931be08a1
17 changed files with 2253 additions and 16 deletions
backend
changelog/entries/unreleased/feature
web-frontend
locales
modules/database
test/unit/database
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
150
backend/src/baserow/contrib/database/fields/filter_support.py
Normal file
150
backend/src/baserow/contrib/database/fields/filter_support.py
Normal 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},
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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 = '';"
|
||||
|
||||
|
|
172
backend/src/baserow/contrib/database/views/array_view_filters.py
Normal file
172
backend/src/baserow/contrib/database/views/array_view_filters.py
Normal 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()
|
|
@ -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
|
|
@ -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"
|
||||
}
|
|
@ -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",
|
||||
|
|
210
web-frontend/modules/database/arrayViewFilters.js
Normal file
210
web-frontend/modules/database/arrayViewFilters.js
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
82
web-frontend/modules/database/fieldFilterCompatibility.js
Normal file
82
web-frontend/modules/database/fieldFilterCompatibility.js
Normal 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
|
||||
},
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -197,7 +197,6 @@ export class EqualViewFilterType extends ViewFilterType {
|
|||
'uuid',
|
||||
'autonumber',
|
||||
'duration',
|
||||
FormulaFieldType.compatibleWithFormulaTypes('text', 'char', 'number'),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
388
web-frontend/test/unit/database/arrayViewFiltersMatch.spec.js
Normal file
388
web-frontend/test/unit/database/arrayViewFiltersMatch.spec.js
Normal 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
Loading…
Add table
Reference in a new issue