mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-13 08:41:46 +00:00
#3112 number lookup field filters
This commit is contained in:
parent
aa20310a0e
commit
244d6050bd
27 changed files with 2175 additions and 391 deletions
backend
changelog/entries/unreleased/feature
web-frontend
modules/database
arrayFilterMixins.jsarrayViewFilters.js
components/view
fieldTypes.jsformula
locales
mixins
plugin.jsutils
test/unit/database
|
@ -453,10 +453,18 @@ class DatabaseConfig(AppConfig):
|
||||||
HasNotValueContainsViewFilterType,
|
HasNotValueContainsViewFilterType,
|
||||||
HasNotValueContainsWordViewFilterType,
|
HasNotValueContainsWordViewFilterType,
|
||||||
HasNotValueEqualViewFilterType,
|
HasNotValueEqualViewFilterType,
|
||||||
|
HasNotValueHigherOrEqualTHanFilterType,
|
||||||
|
HasNotValueHigherThanFilterType,
|
||||||
|
HasNotValueLowerOrEqualTHanFilterType,
|
||||||
|
HasNotValueLowerThanFilterType,
|
||||||
HasValueContainsViewFilterType,
|
HasValueContainsViewFilterType,
|
||||||
HasValueContainsWordViewFilterType,
|
HasValueContainsWordViewFilterType,
|
||||||
HasValueEqualViewFilterType,
|
HasValueEqualViewFilterType,
|
||||||
|
HasValueHigherOrEqualThanFilter,
|
||||||
HasValueLengthIsLowerThanViewFilterType,
|
HasValueLengthIsLowerThanViewFilterType,
|
||||||
|
HasValueLowerOrEqualThanFilter,
|
||||||
|
HasValueLowerThanFilter,
|
||||||
|
hasValueComparableToFilter,
|
||||||
)
|
)
|
||||||
|
|
||||||
view_filter_type_registry.register(HasValueEqualViewFilterType())
|
view_filter_type_registry.register(HasValueEqualViewFilterType())
|
||||||
|
@ -471,6 +479,14 @@ class DatabaseConfig(AppConfig):
|
||||||
view_filter_type_registry.register(HasNotEmptyValueViewFilterType())
|
view_filter_type_registry.register(HasNotEmptyValueViewFilterType())
|
||||||
view_filter_type_registry.register(HasAnySelectOptionEqualViewFilterType())
|
view_filter_type_registry.register(HasAnySelectOptionEqualViewFilterType())
|
||||||
view_filter_type_registry.register(HasNoneSelectOptionEqualViewFilterType())
|
view_filter_type_registry.register(HasNoneSelectOptionEqualViewFilterType())
|
||||||
|
view_filter_type_registry.register(HasValueLowerThanFilter())
|
||||||
|
view_filter_type_registry.register(HasValueLowerOrEqualThanFilter())
|
||||||
|
view_filter_type_registry.register(hasValueComparableToFilter())
|
||||||
|
view_filter_type_registry.register(HasValueHigherOrEqualThanFilter())
|
||||||
|
view_filter_type_registry.register(HasNotValueHigherOrEqualTHanFilterType())
|
||||||
|
view_filter_type_registry.register(HasNotValueHigherThanFilterType())
|
||||||
|
view_filter_type_registry.register(HasNotValueLowerOrEqualTHanFilterType())
|
||||||
|
view_filter_type_registry.register(HasNotValueLowerThanFilterType())
|
||||||
|
|
||||||
from .views.view_aggregations import (
|
from .views.view_aggregations import (
|
||||||
AverageViewAggregationType,
|
AverageViewAggregationType,
|
||||||
|
|
|
@ -94,7 +94,7 @@ from baserow.contrib.database.api.views.errors import (
|
||||||
from baserow.contrib.database.db.functions import RandomUUID
|
from baserow.contrib.database.db.functions import RandomUUID
|
||||||
from baserow.contrib.database.export_serialized import DatabaseExportSerializedStructure
|
from baserow.contrib.database.export_serialized import DatabaseExportSerializedStructure
|
||||||
from baserow.contrib.database.fields.filter_support.formula import (
|
from baserow.contrib.database.fields.filter_support.formula import (
|
||||||
FormulaArrayFilterSupport,
|
FormulaFieldTypeArrayFilterSupport,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.formula import (
|
from baserow.contrib.database.formula import (
|
||||||
BASEROW_FORMULA_TYPE_ALLOWED_FIELDS,
|
BASEROW_FORMULA_TYPE_ALLOWED_FIELDS,
|
||||||
|
@ -144,6 +144,7 @@ from baserow.core.user_files.handler import UserFileHandler
|
||||||
from baserow.core.utils import list_to_comma_separated_string
|
from baserow.core.utils import list_to_comma_separated_string
|
||||||
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
|
BASEROW_BOOLEAN_FIELD_FALSE_VALUES,
|
||||||
BASEROW_BOOLEAN_FIELD_TRUE_VALUES,
|
BASEROW_BOOLEAN_FIELD_TRUE_VALUES,
|
||||||
UPSERT_OPTION_DICT_KEY,
|
UPSERT_OPTION_DICT_KEY,
|
||||||
DeleteFieldStrategyEnum,
|
DeleteFieldStrategyEnum,
|
||||||
|
@ -783,6 +784,21 @@ class NumberFieldType(FieldType):
|
||||||
"number_separator": field.number_separator,
|
"number_separator": field.number_separator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def prepare_filter_value(self, field, model_field, value):
|
||||||
|
"""
|
||||||
|
Verify if it's a valid and finite decimal value, but the filter value doesn't
|
||||||
|
need to respect the number_decimal_places, because they can change while the
|
||||||
|
filter_value remains the same.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = Decimal(value)
|
||||||
|
if not value.is_finite():
|
||||||
|
raise ValueError
|
||||||
|
except (InvalidOperation, ValueError, TypeError):
|
||||||
|
raise ValueError(f"Invalid value for number field: {value}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class RatingFieldType(FieldType):
|
class RatingFieldType(FieldType):
|
||||||
type = "rating"
|
type = "rating"
|
||||||
|
@ -959,6 +975,14 @@ class BooleanFieldType(FieldType):
|
||||||
) -> BooleanField:
|
) -> BooleanField:
|
||||||
return BooleanField()
|
return BooleanField()
|
||||||
|
|
||||||
|
def prepare_filter_value(self, field, model_field, value):
|
||||||
|
if value in BASEROW_BOOLEAN_FIELD_TRUE_VALUES:
|
||||||
|
return True
|
||||||
|
elif value in BASEROW_BOOLEAN_FIELD_FALSE_VALUES:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid value for boolean field: {value}")
|
||||||
|
|
||||||
|
|
||||||
class DateFieldType(FieldType):
|
class DateFieldType(FieldType):
|
||||||
type = "date"
|
type = "date"
|
||||||
|
@ -4674,7 +4698,7 @@ class PhoneNumberFieldType(CollationSortMixin, CharFieldMatchingRegexFieldType):
|
||||||
return collate_expression(Value(value))
|
return collate_expression(Value(value))
|
||||||
|
|
||||||
|
|
||||||
class FormulaFieldType(FormulaArrayFilterSupport, ReadOnlyFieldType):
|
class FormulaFieldType(FormulaFieldTypeArrayFilterSupport, ReadOnlyFieldType):
|
||||||
type = "formula"
|
type = "formula"
|
||||||
model_class = FormulaField
|
model_class = FormulaField
|
||||||
_db_column_fields = []
|
_db_column_fields = []
|
||||||
|
@ -5238,6 +5262,13 @@ class FormulaFieldType(FormulaArrayFilterSupport, ReadOnlyFieldType):
|
||||||
|
|
||||||
return FormulaHandler.get_dependencies_field_names(serialized_field["formula"])
|
return FormulaHandler.get_dependencies_field_names(serialized_field["formula"])
|
||||||
|
|
||||||
|
def prepare_filter_value(self, field, model_field, value):
|
||||||
|
(
|
||||||
|
field_instance,
|
||||||
|
field_type,
|
||||||
|
) = self.get_field_instance_and_type_from_formula_field(field)
|
||||||
|
return field_type.prepare_filter_value(field_instance, model_field, value)
|
||||||
|
|
||||||
|
|
||||||
class CountFieldType(FormulaFieldType):
|
class CountFieldType(FormulaFieldType):
|
||||||
type = "count"
|
type = "count"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import re
|
import re
|
||||||
import typing
|
from typing import TYPE_CHECKING, Any, Dict, Type
|
||||||
|
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -13,17 +13,32 @@ from baserow.contrib.database.fields.field_filters import (
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.formula.expression_generator.django_expressions import (
|
from baserow.contrib.database.formula.expression_generator.django_expressions import (
|
||||||
BaserowFilterExpression,
|
BaserowFilterExpression,
|
||||||
|
ComparisonOperator,
|
||||||
JSONArrayAllAreExpr,
|
JSONArrayAllAreExpr,
|
||||||
|
JSONArrayCompareNumericValueExpr,
|
||||||
JSONArrayContainsValueExpr,
|
JSONArrayContainsValueExpr,
|
||||||
JSONArrayContainsValueLengthLowerThanExpr,
|
JSONArrayContainsValueLengthLowerThanExpr,
|
||||||
JSONArrayContainsValueSimilarToExpr,
|
JSONArrayContainsValueSimilarToExpr,
|
||||||
)
|
)
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from baserow.contrib.database.fields.models import Field
|
from baserow.contrib.database.fields.models import Field
|
||||||
|
|
||||||
|
|
||||||
class HasValueEmptyFilterSupport:
|
class HasValueEmptyFilterSupport:
|
||||||
|
def get_in_array_empty_value(self, field: "Field") -> Any:
|
||||||
|
"""
|
||||||
|
Returns the value to use for filtering empty values in an array. An empty string
|
||||||
|
is used by default and works for test-like fields, but for number fields or
|
||||||
|
other types, this method should be overridden returning None or the most
|
||||||
|
appropriate value.
|
||||||
|
|
||||||
|
:param field: The related field's instance.
|
||||||
|
:return: The value to use for filtering empty values in an array.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
def get_in_array_empty_query(
|
def get_in_array_empty_query(
|
||||||
self, field_name: str, model_field: models.Field, field: "Field"
|
self, field_name: str, model_field: models.Field, field: "Field"
|
||||||
) -> OptionallyAnnotatedQ:
|
) -> OptionallyAnnotatedQ:
|
||||||
|
@ -36,10 +51,13 @@ class HasValueEmptyFilterSupport:
|
||||||
:return: A Q or AnnotatedQ filter given value.
|
:return: A Q or AnnotatedQ filter given value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return Q(**{f"{field_name}__contains": Value([{"value": ""}], JSONField())})
|
empty_value = self.get_in_array_empty_value(field)
|
||||||
|
return Q(
|
||||||
|
**{f"{field_name}__contains": Value([{"value": empty_value}], JSONField())}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HasValueFilterSupport:
|
class HasValueEqualFilterSupport:
|
||||||
def get_in_array_is_query(
|
def get_in_array_is_query(
|
||||||
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
||||||
) -> OptionallyAnnotatedQ:
|
) -> OptionallyAnnotatedQ:
|
||||||
|
@ -53,9 +71,6 @@ class HasValueFilterSupport:
|
||||||
:return: A Q or AnnotatedQ filter given value.
|
:return: A Q or AnnotatedQ filter given value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not value:
|
|
||||||
return Q()
|
|
||||||
|
|
||||||
return Q(**{f"{field_name}__contains": Value([{"value": value}], JSONField())})
|
return Q(**{f"{field_name}__contains": Value([{"value": value}], JSONField())})
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,8 +89,6 @@ class HasValueContainsFilterSupport:
|
||||||
:return: A Q or AnnotatedQ filter given value.
|
:return: A Q or AnnotatedQ filter given value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not value:
|
|
||||||
return Q()
|
|
||||||
annotation_query = JSONArrayContainsValueExpr(
|
annotation_query = JSONArrayContainsValueExpr(
|
||||||
F(field_name), Value(f"%{value}%"), output_field=BooleanField()
|
F(field_name), Value(f"%{value}%"), output_field=BooleanField()
|
||||||
)
|
)
|
||||||
|
@ -103,9 +116,6 @@ class HasValueContainsWordFilterSupport:
|
||||||
:return: A Q or AnnotatedQ filter given value.
|
:return: A Q or AnnotatedQ filter given value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
value = value.strip()
|
|
||||||
if not value:
|
|
||||||
return Q()
|
|
||||||
value = re.escape(value.upper())
|
value = re.escape(value.upper())
|
||||||
annotation_query = JSONArrayContainsValueSimilarToExpr(
|
annotation_query = JSONArrayContainsValueSimilarToExpr(
|
||||||
F(field_name), Value(f"%\\m{value}\\M%"), output_field=BooleanField()
|
F(field_name), Value(f"%\\m{value}\\M%"), output_field=BooleanField()
|
||||||
|
@ -134,9 +144,6 @@ class HasValueLengthIsLowerThanFilterSupport:
|
||||||
:return: A Q or AnnotatedQ filter given value.
|
:return: A Q or AnnotatedQ filter given value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
value = value.strip()
|
|
||||||
if not value:
|
|
||||||
return Q()
|
|
||||||
try:
|
try:
|
||||||
converted_value = int(value)
|
converted_value = int(value)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
|
@ -182,29 +189,49 @@ class HasAllValuesEqualFilterSupport:
|
||||||
return self.default_filter_on_exception()
|
return self.default_filter_on_exception()
|
||||||
|
|
||||||
|
|
||||||
def get_array_json_filter_expression(
|
class HasNumericValueComparableToFilterSupport:
|
||||||
json_expression: typing.Type[BaserowFilterExpression], field_name: str, value: str
|
def get_has_numeric_value_comparable_to_filter_query(
|
||||||
) -> OptionallyAnnotatedQ:
|
self,
|
||||||
"""
|
field_name: str,
|
||||||
helper to generate annotated query to get filtered json-based array.
|
value: str,
|
||||||
`json_expression` should be a filter expression class.
|
model_field: models.Field,
|
||||||
|
field: "Field",
|
||||||
|
comparison_op: ComparisonOperator,
|
||||||
|
) -> OptionallyAnnotatedQ:
|
||||||
|
return get_array_json_filter_expression(
|
||||||
|
JSONArrayCompareNumericValueExpr,
|
||||||
|
field_name,
|
||||||
|
value,
|
||||||
|
comparison_op=comparison_op,
|
||||||
|
)
|
||||||
|
|
||||||
:param json_expression: BaserowFilterExpression to use
|
|
||||||
:param field_name: a name of a field
|
def get_array_json_filter_expression(
|
||||||
:param value: filter value
|
json_expression: Type[BaserowFilterExpression],
|
||||||
:param model_field:
|
field_name: str,
|
||||||
:param field:
|
value: str,
|
||||||
:return:
|
**extra: Dict[str, Any],
|
||||||
|
) -> AnnotatedQ:
|
||||||
|
"""
|
||||||
|
Helper function to generate an AnnotatedQ for the given field and filtering
|
||||||
|
expression. This function ensure a consistent way to name the annotations so they
|
||||||
|
don't clash when combined with similar filters for different fields or values.
|
||||||
|
|
||||||
|
|
||||||
|
:param json_expression: BaserowFilterExpression to use for filtering.
|
||||||
|
:param field_name: the name of the field
|
||||||
|
:param value: filter the filter value.
|
||||||
|
:param extra: extra arguments for the json_expression.
|
||||||
|
:return: the annotated query for the filter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
annotation_query = json_expression(
|
annotation_query = json_expression(
|
||||||
F(field_name), Value(value), output_field=BooleanField()
|
F(field_name), Value(value), output_field=BooleanField(), **extra
|
||||||
)
|
)
|
||||||
lookup_name = (json_expression.__name__).lower()
|
expr_name = json_expression.__name__.lower()
|
||||||
hashed_value = hash(value)
|
hashed_value = hash(value)
|
||||||
|
annotation_name = f"{field_name}_{expr_name}_{hashed_value}"
|
||||||
return AnnotatedQ(
|
return AnnotatedQ(
|
||||||
annotation={
|
annotation={annotation_name: annotation_query},
|
||||||
f"{field_name}_array_expr_{lookup_name}_{hashed_value}": annotation_query
|
q={annotation_name: True},
|
||||||
},
|
|
||||||
q={f"{field_name}_array_expr_{lookup_name}_{hashed_value}": True},
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,28 +4,39 @@ from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
|
from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
|
||||||
|
from baserow.contrib.database.formula.expression_generator.django_expressions import (
|
||||||
|
ComparisonOperator,
|
||||||
|
)
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
HasAllValuesEqualFilterSupport,
|
HasAllValuesEqualFilterSupport,
|
||||||
|
HasNumericValueComparableToFilterSupport,
|
||||||
HasValueContainsFilterSupport,
|
HasValueContainsFilterSupport,
|
||||||
HasValueContainsWordFilterSupport,
|
HasValueContainsWordFilterSupport,
|
||||||
HasValueEmptyFilterSupport,
|
HasValueEmptyFilterSupport,
|
||||||
HasValueFilterSupport,
|
HasValueEqualFilterSupport,
|
||||||
HasValueLengthIsLowerThanFilterSupport,
|
HasValueLengthIsLowerThanFilterSupport,
|
||||||
)
|
)
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from baserow.contrib.database.fields.models import FormulaField
|
from baserow.contrib.database.fields.models import Field, FormulaField
|
||||||
|
|
||||||
|
|
||||||
class FormulaArrayFilterSupport(
|
class FormulaFieldTypeArrayFilterSupport(
|
||||||
HasAllValuesEqualFilterSupport,
|
HasAllValuesEqualFilterSupport,
|
||||||
HasValueFilterSupport,
|
HasValueEqualFilterSupport,
|
||||||
HasValueEmptyFilterSupport,
|
HasValueEmptyFilterSupport,
|
||||||
HasValueContainsFilterSupport,
|
HasValueContainsFilterSupport,
|
||||||
HasValueContainsWordFilterSupport,
|
HasValueContainsWordFilterSupport,
|
||||||
HasValueLengthIsLowerThanFilterSupport,
|
HasValueLengthIsLowerThanFilterSupport,
|
||||||
|
HasNumericValueComparableToFilterSupport,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
A mixin that acts as a proxy between the formula field and the specific array
|
||||||
|
formula function to call. Every method needs to be implemented here and forwarded
|
||||||
|
to the right array formula subtype method.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_in_array_is_query(
|
def get_in_array_is_query(
|
||||||
self,
|
self,
|
||||||
field_name: str,
|
field_name: str,
|
||||||
|
@ -42,6 +53,14 @@ class FormulaArrayFilterSupport(
|
||||||
field_name, value, model_field, field_instance
|
field_name, value, model_field, field_instance
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_in_array_empty_value(self, field: "Field") -> typing.Any:
|
||||||
|
(
|
||||||
|
field_instance,
|
||||||
|
field_type,
|
||||||
|
) = self.get_field_instance_and_type_from_formula_field(field)
|
||||||
|
|
||||||
|
return field_type.get_in_array_empty_value(field_instance)
|
||||||
|
|
||||||
def get_in_array_empty_query(self, field_name, model_field, field: "FormulaField"):
|
def get_in_array_empty_query(self, field_name, model_field, field: "FormulaField"):
|
||||||
(
|
(
|
||||||
field_instance,
|
field_instance,
|
||||||
|
@ -99,3 +118,20 @@ class FormulaArrayFilterSupport(
|
||||||
return field_type.get_has_all_values_equal_query(
|
return field_type.get_has_all_values_equal_query(
|
||||||
field_name, value, model_field, field_instance
|
field_name, value, model_field, field_instance
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_has_numeric_value_comparable_to_filter_query(
|
||||||
|
self,
|
||||||
|
field_name: str,
|
||||||
|
value: str,
|
||||||
|
model_field: models.Field,
|
||||||
|
field: "Field",
|
||||||
|
comparison_op: ComparisonOperator,
|
||||||
|
) -> OptionallyAnnotatedQ:
|
||||||
|
(
|
||||||
|
field_instance,
|
||||||
|
field_type,
|
||||||
|
) = self.get_field_instance_and_type_from_formula_field(field)
|
||||||
|
|
||||||
|
return field_type.get_has_numeric_value_comparable_to_filter_query(
|
||||||
|
field_name, value, model_field, field_instance, comparison_op
|
||||||
|
)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, Any, List
|
||||||
|
|
||||||
from django.contrib.postgres.fields import JSONField
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import BooleanField, F, Q, Value
|
from django.db.models import BooleanField, F, Q, Value
|
||||||
|
|
||||||
|
@ -19,7 +18,7 @@ from .base import (
|
||||||
HasValueContainsFilterSupport,
|
HasValueContainsFilterSupport,
|
||||||
HasValueContainsWordFilterSupport,
|
HasValueContainsWordFilterSupport,
|
||||||
HasValueEmptyFilterSupport,
|
HasValueEmptyFilterSupport,
|
||||||
HasValueFilterSupport,
|
HasValueEqualFilterSupport,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -28,12 +27,12 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
class SingleSelectFormulaTypeFilterSupport(
|
class SingleSelectFormulaTypeFilterSupport(
|
||||||
HasValueEmptyFilterSupport,
|
HasValueEmptyFilterSupport,
|
||||||
HasValueFilterSupport,
|
HasValueEqualFilterSupport,
|
||||||
HasValueContainsFilterSupport,
|
HasValueContainsFilterSupport,
|
||||||
HasValueContainsWordFilterSupport,
|
HasValueContainsWordFilterSupport,
|
||||||
):
|
):
|
||||||
def get_in_array_empty_query(self, field_name, model_field, field: "Field"):
|
def get_in_array_empty_value(self, field: "Field") -> Any:
|
||||||
return Q(**{f"{field_name}__contains": Value([{"value": None}], JSONField())})
|
return None
|
||||||
|
|
||||||
def get_in_array_is_query(
|
def get_in_array_is_query(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -1825,6 +1825,30 @@ class FieldType(
|
||||||
|
|
||||||
return value1 == value2
|
return value1 == value2
|
||||||
|
|
||||||
|
def prepare_filter_value(
|
||||||
|
self, field: "Field", model_field: django_models.Field, value: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Prepare a non-empty value string to be used in a view filter, verifying if it is
|
||||||
|
compatible with the given field and model_field. This method must be called
|
||||||
|
before the value is passed to the filter method that requires it. This is useful
|
||||||
|
to ensure that comparisons are done correctly and that the value is correctly
|
||||||
|
prepared for the database, like converting a string to a date object or a
|
||||||
|
number.
|
||||||
|
|
||||||
|
:param field: The field instance that the value belongs to.
|
||||||
|
:param model_field: The model field that the value must be prepared for.
|
||||||
|
:param value: The value that must be prepared for filtering.
|
||||||
|
:return: The prepared value.
|
||||||
|
:raises ValueError: If the value is not compatible for the given field and
|
||||||
|
model_field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return model_field.get_prep_value(value)
|
||||||
|
except ValidationError as e:
|
||||||
|
raise ValueError(str(e))
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyFieldType(FieldType):
|
class ReadOnlyFieldType(FieldType):
|
||||||
read_only = True
|
read_only = True
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import typing
|
import typing
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from django.contrib.postgres.aggregates.mixins import OrderableAggMixin
|
from django.contrib.postgres.aggregates.mixins import OrderableAggMixin
|
||||||
from django.db import NotSupportedError
|
from django.db import NotSupportedError
|
||||||
|
@ -148,15 +149,33 @@ class BaserowFilterExpression(Expression):
|
||||||
|
|
||||||
return c
|
return c
|
||||||
|
|
||||||
def as_sql(self, compiler, connection, template=None):
|
def get_template_data(self, sql_value) -> dict:
|
||||||
sql_value, params_value = compiler.compile(self.value)
|
return {
|
||||||
|
|
||||||
template = template or self.template
|
|
||||||
data = {
|
|
||||||
"field_name": f'"{self.field_name.field.column}"',
|
"field_name": f'"{self.field_name.field.column}"',
|
||||||
"value": sql_value,
|
"value": sql_value,
|
||||||
}
|
}
|
||||||
return template % data, params_value
|
|
||||||
|
def render_template_as_sql(
|
||||||
|
self, filter_value: str, template: str | None = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Renders the template with the given sql_value and returns the result. If a
|
||||||
|
custom template is provided, it will be used instead of the default one.
|
||||||
|
|
||||||
|
:param filter_value: The value that will be used in the template.
|
||||||
|
:param template: The custom template to use. If not provided, the default one
|
||||||
|
will be used.
|
||||||
|
:return: The rendered template with data that will be used as SQL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = template or self.template
|
||||||
|
data = self.get_template_data(filter_value)
|
||||||
|
return template % data
|
||||||
|
|
||||||
|
def as_sql(self, compiler, connection, template=None):
|
||||||
|
sql_value, sql_params = compiler.compile(self.value)
|
||||||
|
sql_query = self.render_template_as_sql(sql_value, template)
|
||||||
|
return sql_query, sql_params
|
||||||
|
|
||||||
|
|
||||||
class FileNameContainsExpr(BaserowFilterExpression):
|
class FileNameContainsExpr(BaserowFilterExpression):
|
||||||
|
@ -164,7 +183,7 @@ class FileNameContainsExpr(BaserowFilterExpression):
|
||||||
template = (
|
template = (
|
||||||
f"""
|
f"""
|
||||||
EXISTS(
|
EXISTS(
|
||||||
SELECT attached_files ->> 'visible_name'
|
SELECT 1
|
||||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as attached_files
|
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as attached_files
|
||||||
WHERE UPPER(attached_files ->> 'visible_name') LIKE UPPER(%(value)s)
|
WHERE UPPER(attached_files ->> 'visible_name') LIKE UPPER(%(value)s)
|
||||||
)
|
)
|
||||||
|
@ -178,7 +197,7 @@ class JSONArrayContainsValueExpr(BaserowFilterExpression):
|
||||||
template = (
|
template = (
|
||||||
f"""
|
f"""
|
||||||
EXISTS(
|
EXISTS(
|
||||||
SELECT filtered_field ->> 'value'
|
SELECT 1
|
||||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||||
WHERE UPPER(filtered_field ->> 'value') LIKE UPPER(%(value)s::text)
|
WHERE UPPER(filtered_field ->> 'value') LIKE UPPER(%(value)s::text)
|
||||||
)
|
)
|
||||||
|
@ -192,7 +211,7 @@ class JSONArrayContainsValueSimilarToExpr(BaserowFilterExpression):
|
||||||
template = (
|
template = (
|
||||||
f"""
|
f"""
|
||||||
EXISTS(
|
EXISTS(
|
||||||
SELECT filtered_field ->> 'value'
|
SELECT 1
|
||||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||||
WHERE UPPER(filtered_field ->> 'value') SIMILAR TO %(value)s
|
WHERE UPPER(filtered_field ->> 'value') SIMILAR TO %(value)s
|
||||||
)
|
)
|
||||||
|
@ -206,7 +225,7 @@ class JSONArrayContainsValueLengthLowerThanExpr(BaserowFilterExpression):
|
||||||
template = (
|
template = (
|
||||||
f"""
|
f"""
|
||||||
EXISTS(
|
EXISTS(
|
||||||
SELECT filtered_field ->> 'value'
|
SELECT 1
|
||||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||||
WHERE LENGTH(filtered_field ->> 'value') < %(value)s
|
WHERE LENGTH(filtered_field ->> 'value') < %(value)s
|
||||||
)
|
)
|
||||||
|
@ -219,7 +238,7 @@ class JSONArrayAllAreExpr(BaserowFilterExpression):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
template = (
|
template = (
|
||||||
f"""
|
f"""
|
||||||
upper(%(value)s::text) =ALL(
|
upper(%(value)s::text) = ALL(
|
||||||
SELECT upper(filtered_field ->> 'value')
|
SELECT upper(filtered_field ->> 'value')
|
||||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||||
) AND JSONB_ARRAY_LENGTH(%(field_name)s) > 0
|
) AND JSONB_ARRAY_LENGTH(%(field_name)s) > 0
|
||||||
|
@ -233,7 +252,7 @@ class JSONArrayEqualSelectOptionIdExpr(BaserowFilterExpression):
|
||||||
template = (
|
template = (
|
||||||
f"""
|
f"""
|
||||||
EXISTS(
|
EXISTS(
|
||||||
SELECT filtered_field -> 'value' ->> 'id'
|
SELECT 1
|
||||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||||
WHERE (filtered_field -> 'value' ->> 'id') LIKE (%(value)s)
|
WHERE (filtered_field -> 'value' ->> 'id') LIKE (%(value)s)
|
||||||
)
|
)
|
||||||
|
@ -247,7 +266,7 @@ class JSONArrayContainsSelectOptionValueExpr(BaserowFilterExpression):
|
||||||
template = (
|
template = (
|
||||||
f"""
|
f"""
|
||||||
EXISTS(
|
EXISTS(
|
||||||
SELECT filtered_field -> 'value' ->> 'value'
|
SELECT 1
|
||||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||||
WHERE UPPER(filtered_field -> 'value' ->> 'value') LIKE UPPER(%(value)s)
|
WHERE UPPER(filtered_field -> 'value' ->> 'value') LIKE UPPER(%(value)s)
|
||||||
)
|
)
|
||||||
|
@ -261,10 +280,62 @@ class JSONArrayContainsSelectOptionValueSimilarToExpr(BaserowFilterExpression):
|
||||||
template = (
|
template = (
|
||||||
r"""
|
r"""
|
||||||
EXISTS(
|
EXISTS(
|
||||||
SELECT filtered_field -> 'value' ->> 'value'
|
SELECT 1
|
||||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||||
WHERE filtered_field -> 'value' ->> 'value' ~* ('\y' || %(value)s || '\y')
|
WHERE filtered_field -> 'value' ->> 'value' ~* ('\y' || %(value)s || '\y')
|
||||||
)
|
)
|
||||||
""" # nosec B608 %(value)s
|
""" # nosec B608 %(value)s
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
class ComparisonOperator(Enum):
|
||||||
|
"""
|
||||||
|
An enumeration of the comparison operators that can be used to compare a number
|
||||||
|
field value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
EQUAL = "="
|
||||||
|
LOWER_THAN = "<"
|
||||||
|
LOWER_THAN_OR_EQUAL = "<="
|
||||||
|
HIGHER_THAN = ">"
|
||||||
|
HIGHER_THAN_OR_EQUAL = ">="
|
||||||
|
|
||||||
|
|
||||||
|
class JSONArrayCompareNumericValueExpr(BaserowFilterExpression):
|
||||||
|
"""
|
||||||
|
Base class for expressions that compare a numeric value in a JSON array.
|
||||||
|
Together with the field_name and value, a comparison operator must be provided to be
|
||||||
|
used in the template.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
field_name: F,
|
||||||
|
value: Value,
|
||||||
|
comparison_op: ComparisonOperator,
|
||||||
|
output_field: Field,
|
||||||
|
):
|
||||||
|
super().__init__(field_name, value, output_field)
|
||||||
|
if not isinstance(comparison_op, ComparisonOperator):
|
||||||
|
raise ValueError(
|
||||||
|
f"comparison_op must be a ComparisonOperator, not {type(comparison_op)}"
|
||||||
|
)
|
||||||
|
self.comparison_op = comparison_op
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
template = (
|
||||||
|
f"""
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||||
|
WHERE (filtered_field ->> 'value')::numeric %(comparison_op)s %(value)s::numeric
|
||||||
|
)
|
||||||
|
""" # nosec B608 %(value)s %(comparison_op)s
|
||||||
|
)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
def get_template_data(self, sql_value) -> dict:
|
||||||
|
data = super().get_template_data(sql_value)
|
||||||
|
data["comparison_op"] = self.comparison_op.value
|
||||||
|
return data
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from baserow.contrib.database.fields.filter_support.base import (
|
||||||
|
HasAllValuesEqualFilterSupport,
|
||||||
|
HasNumericValueComparableToFilterSupport,
|
||||||
|
HasValueContainsFilterSupport,
|
||||||
|
HasValueContainsWordFilterSupport,
|
||||||
|
HasValueEmptyFilterSupport,
|
||||||
|
HasValueEqualFilterSupport,
|
||||||
|
HasValueLengthIsLowerThanFilterSupport,
|
||||||
|
)
|
||||||
|
from baserow.contrib.database.formula.expression_generator.django_expressions import (
|
||||||
|
ComparisonOperator,
|
||||||
|
)
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
|
||||||
|
from baserow.contrib.database.fields.models import Field
|
||||||
|
|
||||||
|
|
||||||
|
class BaserowFormulaArrayFilterSupportMixin(
|
||||||
|
HasAllValuesEqualFilterSupport,
|
||||||
|
HasValueEmptyFilterSupport,
|
||||||
|
HasValueEqualFilterSupport,
|
||||||
|
HasValueContainsFilterSupport,
|
||||||
|
HasValueContainsWordFilterSupport,
|
||||||
|
HasValueLengthIsLowerThanFilterSupport,
|
||||||
|
HasNumericValueComparableToFilterSupport,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This mixin proxies all the array formula filters methods to the formula subtype.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_in_array_empty_value(self, field):
|
||||||
|
field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
|
||||||
|
return self.sub_type.get_in_array_empty_value(field_instance)
|
||||||
|
|
||||||
|
def get_in_array_empty_query(self, field_name, model_field, field):
|
||||||
|
field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
|
||||||
|
return self.sub_type.get_in_array_empty_query(
|
||||||
|
field_name, model_field, field_instance
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_in_array_is_query(self, field_name, value, model_field, field):
|
||||||
|
field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
|
||||||
|
return self.sub_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):
|
||||||
|
field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
|
||||||
|
return self.sub_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):
|
||||||
|
field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
|
||||||
|
return self.sub_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
|
||||||
|
):
|
||||||
|
field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
|
||||||
|
return self.sub_type.get_in_array_length_is_lower_than_query(
|
||||||
|
field_name, value, model_field, field_instance
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_has_all_values_equal_query(
|
||||||
|
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
||||||
|
) -> "OptionallyAnnotatedQ":
|
||||||
|
field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
|
||||||
|
return self.sub_type.get_has_all_values_equal_query(
|
||||||
|
field_name, value, model_field, field_instance
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_has_numeric_value_comparable_to_filter_query(
|
||||||
|
self,
|
||||||
|
field_name: str,
|
||||||
|
value: str,
|
||||||
|
model_field: models.Field,
|
||||||
|
field: "Field",
|
||||||
|
comparison_op: ComparisonOperator,
|
||||||
|
) -> "OptionallyAnnotatedQ":
|
||||||
|
field_instance, _ = self.sub_type.get_baserow_field_instance_and_type()
|
||||||
|
return self.sub_type.get_has_numeric_value_comparable_to_filter_query(
|
||||||
|
field_name, value, model_field, field_instance, comparison_op
|
||||||
|
)
|
|
@ -497,6 +497,17 @@ class BaserowFormulaType(abc.ABC):
|
||||||
field_instance.id = field.id
|
field_instance.id = field.id
|
||||||
return field_type.is_searchable(field_instance)
|
return field_type.is_searchable(field_instance)
|
||||||
|
|
||||||
|
def prepare_filter_value(self, field, model_field, value):
|
||||||
|
"""
|
||||||
|
Use the Baserow field type method where possible to prepare the filter value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
(
|
||||||
|
field_instance,
|
||||||
|
field_type,
|
||||||
|
) = self.get_baserow_field_instance_and_type()
|
||||||
|
return field_type.prepare_filter_value(field_instance, model_field, value)
|
||||||
|
|
||||||
|
|
||||||
class BaserowFormulaInvalidType(BaserowFormulaType):
|
class BaserowFormulaInvalidType(BaserowFormulaType):
|
||||||
is_valid = False
|
is_valid = False
|
||||||
|
|
|
@ -6,7 +6,6 @@ from typing import Any, List, Optional, Set, Type, Union
|
||||||
|
|
||||||
from django.contrib.postgres.expressions import ArraySubquery
|
from django.contrib.postgres.expressions import ArraySubquery
|
||||||
from django.contrib.postgres.fields import ArrayField, JSONField
|
from django.contrib.postgres.fields import ArrayField, JSONField
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Expression,
|
Expression,
|
||||||
|
@ -21,7 +20,6 @@ from django.db.models import (
|
||||||
from django.db.models.functions import Cast, Concat
|
from django.db.models.functions import Cast, Concat
|
||||||
|
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
from loguru import logger
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import Field
|
from rest_framework.fields import Field
|
||||||
|
|
||||||
|
@ -37,16 +35,14 @@ from baserow.contrib.database.fields.field_filters import (
|
||||||
from baserow.contrib.database.fields.field_sortings import OptionallyAnnotatedOrderBy
|
from baserow.contrib.database.fields.field_sortings import OptionallyAnnotatedOrderBy
|
||||||
from baserow.contrib.database.fields.filter_support.base import (
|
from baserow.contrib.database.fields.filter_support.base import (
|
||||||
HasAllValuesEqualFilterSupport,
|
HasAllValuesEqualFilterSupport,
|
||||||
|
HasNumericValueComparableToFilterSupport,
|
||||||
HasValueContainsFilterSupport,
|
HasValueContainsFilterSupport,
|
||||||
HasValueContainsWordFilterSupport,
|
HasValueContainsWordFilterSupport,
|
||||||
HasValueEmptyFilterSupport,
|
HasValueEmptyFilterSupport,
|
||||||
HasValueFilterSupport,
|
HasValueEqualFilterSupport,
|
||||||
HasValueLengthIsLowerThanFilterSupport,
|
HasValueLengthIsLowerThanFilterSupport,
|
||||||
get_array_json_filter_expression,
|
get_array_json_filter_expression,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.fields.filter_support.exceptions import (
|
|
||||||
FilterNotSupportedException,
|
|
||||||
)
|
|
||||||
from baserow.contrib.database.fields.filter_support.single_select import (
|
from baserow.contrib.database.fields.filter_support.single_select import (
|
||||||
SingleSelectFormulaTypeFilterSupport,
|
SingleSelectFormulaTypeFilterSupport,
|
||||||
)
|
)
|
||||||
|
@ -65,10 +61,15 @@ from baserow.contrib.database.formula.ast.tree import (
|
||||||
BaserowStringLiteral,
|
BaserowStringLiteral,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.formula.expression_generator.django_expressions import (
|
from baserow.contrib.database.formula.expression_generator.django_expressions import (
|
||||||
|
ComparisonOperator,
|
||||||
|
JSONArrayCompareNumericValueExpr,
|
||||||
JSONArrayContainsValueExpr,
|
JSONArrayContainsValueExpr,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.formula.registries import formula_function_registry
|
from baserow.contrib.database.formula.registries import formula_function_registry
|
||||||
from baserow.contrib.database.formula.types.exceptions import UnknownFormulaType
|
from baserow.contrib.database.formula.types.exceptions import UnknownFormulaType
|
||||||
|
from baserow.contrib.database.formula.types.filter_support import (
|
||||||
|
BaserowFormulaArrayFilterSupportMixin,
|
||||||
|
)
|
||||||
from baserow.contrib.database.formula.types.formula_type import (
|
from baserow.contrib.database.formula.types.formula_type import (
|
||||||
BaserowFormulaInvalidType,
|
BaserowFormulaInvalidType,
|
||||||
BaserowFormulaType,
|
BaserowFormulaType,
|
||||||
|
@ -81,7 +82,14 @@ from baserow.core.utils import list_to_comma_separated_string
|
||||||
|
|
||||||
|
|
||||||
class BaserowJSONBObjectBaseType(BaserowFormulaValidType, ABC):
|
class BaserowJSONBObjectBaseType(BaserowFormulaValidType, ABC):
|
||||||
pass
|
def prepare_filter_value(self, field, model_field, value):
|
||||||
|
"""
|
||||||
|
Since the subclasses don't have a baserow_field_type or data might be stored
|
||||||
|
differently due to the JSONB nature, return the value as is here and let the
|
||||||
|
filter method handle it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class BaserowFormulaBaseTextType(BaserowFormulaTypeHasEmptyBaserowExpression):
|
class BaserowFormulaBaseTextType(BaserowFormulaTypeHasEmptyBaserowExpression):
|
||||||
|
@ -130,7 +138,7 @@ class BaserowFormulaBaseTextType(BaserowFormulaTypeHasEmptyBaserowExpression):
|
||||||
|
|
||||||
class BaserowFormulaTextType(
|
class BaserowFormulaTextType(
|
||||||
HasValueEmptyFilterSupport,
|
HasValueEmptyFilterSupport,
|
||||||
HasValueFilterSupport,
|
HasValueEqualFilterSupport,
|
||||||
HasValueContainsFilterSupport,
|
HasValueContainsFilterSupport,
|
||||||
HasValueContainsWordFilterSupport,
|
HasValueContainsWordFilterSupport,
|
||||||
HasValueLengthIsLowerThanFilterSupport,
|
HasValueLengthIsLowerThanFilterSupport,
|
||||||
|
@ -328,7 +336,12 @@ class BaserowFormulaButtonType(BaserowFormulaLinkType):
|
||||||
|
|
||||||
|
|
||||||
class BaserowFormulaNumberType(
|
class BaserowFormulaNumberType(
|
||||||
BaserowFormulaTypeHasEmptyBaserowExpression, BaserowFormulaValidType
|
HasValueEmptyFilterSupport,
|
||||||
|
HasValueEqualFilterSupport,
|
||||||
|
HasValueContainsFilterSupport,
|
||||||
|
HasNumericValueComparableToFilterSupport,
|
||||||
|
BaserowFormulaTypeHasEmptyBaserowExpression,
|
||||||
|
BaserowFormulaValidType,
|
||||||
):
|
):
|
||||||
type = "number"
|
type = "number"
|
||||||
baserow_field_type = "number"
|
baserow_field_type = "number"
|
||||||
|
@ -460,13 +473,26 @@ class BaserowFormulaNumberType(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_in_array_empty_value(self, field: "Field") -> Any:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_in_array_is_query(
|
||||||
|
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
||||||
|
) -> OptionallyAnnotatedQ:
|
||||||
|
return get_array_json_filter_expression(
|
||||||
|
JSONArrayCompareNumericValueExpr,
|
||||||
|
field_name,
|
||||||
|
value,
|
||||||
|
comparison_op=ComparisonOperator.EQUAL,
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"number({self.number_decimal_places})"
|
return f"number({self.number_decimal_places})"
|
||||||
|
|
||||||
|
|
||||||
class BaserowFormulaBooleanType(
|
class BaserowFormulaBooleanType(
|
||||||
HasAllValuesEqualFilterSupport,
|
HasAllValuesEqualFilterSupport,
|
||||||
HasValueFilterSupport,
|
HasValueEqualFilterSupport,
|
||||||
BaserowFormulaTypeHasEmptyBaserowExpression,
|
BaserowFormulaTypeHasEmptyBaserowExpression,
|
||||||
BaserowFormulaValidType,
|
BaserowFormulaValidType,
|
||||||
):
|
):
|
||||||
|
@ -500,22 +526,9 @@ class BaserowFormulaBooleanType(
|
||||||
):
|
):
|
||||||
return expr
|
return expr
|
||||||
|
|
||||||
def _get_prep_value(self, value: str):
|
|
||||||
from baserow.contrib.database.fields.registries import field_type_registry
|
|
||||||
|
|
||||||
baserow_field_type = field_type_registry.get(self.baserow_field_type)
|
|
||||||
# boolean field type doesn't expect instance value
|
|
||||||
field_instance = baserow_field_type.get_model_field(None)
|
|
||||||
try:
|
|
||||||
# get_prep_value can return None
|
|
||||||
return field_instance.get_prep_value(value) or False
|
|
||||||
except ValidationError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_in_array_is_query(
|
def get_in_array_is_query(
|
||||||
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
||||||
) -> OptionallyAnnotatedQ:
|
) -> OptionallyAnnotatedQ:
|
||||||
value = self._get_prep_value(value)
|
|
||||||
return get_array_json_filter_expression(
|
return get_array_json_filter_expression(
|
||||||
JSONArrayContainsValueExpr, field_name, value
|
JSONArrayContainsValueExpr, field_name, value
|
||||||
)
|
)
|
||||||
|
@ -533,7 +546,6 @@ class BaserowFormulaBooleanType(
|
||||||
def get_has_all_values_equal_query(
|
def get_has_all_values_equal_query(
|
||||||
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
||||||
) -> "OptionallyAnnotatedQ":
|
) -> "OptionallyAnnotatedQ":
|
||||||
value = self._get_prep_value(value)
|
|
||||||
return super().get_has_all_values_equal_query(
|
return super().get_has_all_values_equal_query(
|
||||||
field_name, value, model_field, field
|
field_name, value, model_field, field
|
||||||
)
|
)
|
||||||
|
@ -1062,12 +1074,7 @@ class BaserowFormulaSingleFileType(BaserowJSONBObjectBaseType):
|
||||||
|
|
||||||
|
|
||||||
class BaserowFormulaArrayType(
|
class BaserowFormulaArrayType(
|
||||||
HasAllValuesEqualFilterSupport,
|
BaserowFormulaArrayFilterSupportMixin,
|
||||||
HasValueEmptyFilterSupport,
|
|
||||||
HasValueFilterSupport,
|
|
||||||
HasValueContainsFilterSupport,
|
|
||||||
HasValueContainsWordFilterSupport,
|
|
||||||
HasValueLengthIsLowerThanFilterSupport,
|
|
||||||
BaserowFormulaValidType,
|
BaserowFormulaValidType,
|
||||||
):
|
):
|
||||||
type = "array"
|
type = "array"
|
||||||
|
@ -1235,57 +1242,6 @@ class BaserowFormulaArrayType(
|
||||||
def contains_query(self, field_name, value, model_field, field):
|
def contains_query(self, field_name, value, model_field, field):
|
||||||
return Q()
|
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_has_all_values_equal_query(
|
|
||||||
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
|
||||||
) -> "OptionallyAnnotatedQ":
|
|
||||||
if not isinstance(self.sub_type, HasAllValuesEqualFilterSupport):
|
|
||||||
logger.warning(
|
|
||||||
f"field {field} is not from {HasAllValuesEqualFilterSupport} hierarchy"
|
|
||||||
)
|
|
||||||
raise FilterNotSupportedException()
|
|
||||||
return self.sub_type.get_has_all_values_equal_query(
|
|
||||||
field_name, value, model_field, field
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_alter_column_prepare_old_value(self, connection, from_field, to_field):
|
def get_alter_column_prepare_old_value(self, connection, from_field, to_field):
|
||||||
return "p_in = '';"
|
return "p_in = '';"
|
||||||
|
|
||||||
|
@ -1362,10 +1318,8 @@ class BaserowFormulaArrayType(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def check_if_compatible_with(self, compatible_formula_types: List[str]):
|
def check_if_compatible_with(self, compatible_formula_types: List[str]):
|
||||||
return (
|
self_as_str = self.formula_array_type_as_str(self.sub_type.type)
|
||||||
self.type in compatible_formula_types
|
return self_as_str in compatible_formula_types
|
||||||
or str(self) in compatible_formula_types
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.formula_array_type_as_str(self.sub_type)
|
return self.formula_array_type_as_str(self.sub_type)
|
||||||
|
@ -1398,6 +1352,9 @@ class BaserowFormulaArrayType(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def prepare_filter_value(self, field, model_field, value):
|
||||||
|
return self.sub_type.prepare_filter_value(field, model_field, value)
|
||||||
|
|
||||||
|
|
||||||
class BaserowFormulaSingleSelectType(
|
class BaserowFormulaSingleSelectType(
|
||||||
SingleSelectFormulaTypeFilterSupport,
|
SingleSelectFormulaTypeFilterSupport,
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
|
from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
|
||||||
from baserow.contrib.database.fields.field_types import FormulaFieldType
|
from baserow.contrib.database.fields.field_types import FormulaFieldType
|
||||||
from baserow.contrib.database.fields.filter_support.base import (
|
from baserow.contrib.database.fields.filter_support.base import (
|
||||||
HasAllValuesEqualFilterSupport,
|
HasNumericValueComparableToFilterSupport,
|
||||||
HasValueContainsFilterSupport,
|
HasValueContainsFilterSupport,
|
||||||
HasValueContainsWordFilterSupport,
|
HasValueContainsWordFilterSupport,
|
||||||
HasValueEmptyFilterSupport,
|
HasValueEmptyFilterSupport,
|
||||||
HasValueFilterSupport,
|
HasValueEqualFilterSupport,
|
||||||
HasValueLengthIsLowerThanFilterSupport,
|
HasValueLengthIsLowerThanFilterSupport,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.fields.filter_support.exceptions import (
|
|
||||||
FilterNotSupportedException,
|
|
||||||
)
|
|
||||||
from baserow.contrib.database.fields.registries import field_type_registry
|
from baserow.contrib.database.fields.registries import field_type_registry
|
||||||
from baserow.contrib.database.formula import BaserowFormulaTextType
|
from baserow.contrib.database.formula import (
|
||||||
|
BaserowFormulaNumberType,
|
||||||
|
BaserowFormulaTextType,
|
||||||
|
)
|
||||||
|
from baserow.contrib.database.formula.expression_generator.django_expressions import (
|
||||||
|
ComparisonOperator,
|
||||||
|
)
|
||||||
from baserow.contrib.database.formula.types.formula_types import (
|
from baserow.contrib.database.formula.types.formula_types import (
|
||||||
BaserowFormulaBooleanType,
|
BaserowFormulaBooleanType,
|
||||||
BaserowFormulaCharType,
|
BaserowFormulaCharType,
|
||||||
|
@ -37,18 +44,13 @@ class HasEmptyValueViewFilterType(ViewFilterType):
|
||||||
FormulaFieldType.array_of(BaserowFormulaCharType.type),
|
FormulaFieldType.array_of(BaserowFormulaCharType.type),
|
||||||
FormulaFieldType.array_of(BaserowFormulaURLType.type),
|
FormulaFieldType.array_of(BaserowFormulaURLType.type),
|
||||||
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
|
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
|
||||||
|
FormulaFieldType.array_of(BaserowFormulaNumberType.type),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||||
field_type = field_type_registry.get_by_model(field)
|
field_type: HasValueEmptyFilterSupport = field_type_registry.get_by_model(field)
|
||||||
try:
|
return field_type.get_in_array_empty_query(field_name, model_field, field)
|
||||||
if not isinstance(field_type, HasValueEmptyFilterSupport):
|
|
||||||
raise FilterNotSupportedException(field_type)
|
|
||||||
|
|
||||||
return field_type.get_in_array_empty_query(field_name, model_field, field)
|
|
||||||
except FilterNotSupportedException:
|
|
||||||
return self.default_filter_on_exception()
|
|
||||||
|
|
||||||
|
|
||||||
class HasNotEmptyValueViewFilterType(
|
class HasNotEmptyValueViewFilterType(
|
||||||
|
@ -57,7 +59,43 @@ class HasNotEmptyValueViewFilterType(
|
||||||
type = "has_not_empty_value"
|
type = "has_not_empty_value"
|
||||||
|
|
||||||
|
|
||||||
class HasValueEqualViewFilterType(ViewFilterType):
|
class ComparisonHasValueFilter(ViewFilterType, ABC):
|
||||||
|
"""
|
||||||
|
A filter that can be used to compare the values in an array with a specific value
|
||||||
|
using a comparison operator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_filter(
|
||||||
|
self, field_name, value: str, model_field, field
|
||||||
|
) -> OptionallyAnnotatedQ:
|
||||||
|
if value == "":
|
||||||
|
return Q()
|
||||||
|
|
||||||
|
field_type = field_type_registry.get_by_model(field)
|
||||||
|
try:
|
||||||
|
filter_value = field_type.prepare_filter_value(field, model_field, value)
|
||||||
|
except ValueError: # invalid filter value for the field
|
||||||
|
return self.default_filter_on_exception()
|
||||||
|
|
||||||
|
return self.get_filter_expression(field_name, filter_value, model_field, field)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_filter_expression(
|
||||||
|
self, field_name, value, model_field, field
|
||||||
|
) -> OptionallyAnnotatedQ:
|
||||||
|
"""
|
||||||
|
Return the filter expression to use for the required comparison.
|
||||||
|
|
||||||
|
:param field_name: The name of the field to filter on.
|
||||||
|
:param value: A non-empty string value to compare against.
|
||||||
|
:param model_field: The model field instance.
|
||||||
|
:param field: The field instance.
|
||||||
|
:return: The filter expression to use.
|
||||||
|
:raises ValidationError: If the value cannot be parsed to the proper type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class HasValueEqualViewFilterType(ComparisonHasValueFilter):
|
||||||
"""
|
"""
|
||||||
The filter can be used to check for "is" condition for
|
The filter can be used to check for "is" condition for
|
||||||
items in an array.
|
items in an array.
|
||||||
|
@ -71,19 +109,15 @@ class HasValueEqualViewFilterType(ViewFilterType):
|
||||||
FormulaFieldType.array_of(BaserowFormulaURLType.type),
|
FormulaFieldType.array_of(BaserowFormulaURLType.type),
|
||||||
FormulaFieldType.array_of(BaserowFormulaBooleanType.type),
|
FormulaFieldType.array_of(BaserowFormulaBooleanType.type),
|
||||||
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
|
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
|
||||||
|
FormulaFieldType.array_of(BaserowFormulaNumberType.type),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
def get_filter_expression(
|
||||||
field_type = field_type_registry.get_by_model(field)
|
self, field_name, value, model_field, field
|
||||||
try:
|
) -> OptionallyAnnotatedQ:
|
||||||
if not isinstance(field_type, HasValueFilterSupport):
|
field_type: HasValueEqualFilterSupport = field_type_registry.get_by_model(field)
|
||||||
raise FilterNotSupportedException(field_type)
|
return field_type.get_in_array_is_query(field_name, value, model_field, field)
|
||||||
return field_type.get_in_array_is_query(
|
|
||||||
field_name, value, model_field, field
|
|
||||||
)
|
|
||||||
except FilterNotSupportedException:
|
|
||||||
return self.default_filter_on_exception()
|
|
||||||
|
|
||||||
|
|
||||||
class HasNotValueEqualViewFilterType(
|
class HasNotValueEqualViewFilterType(
|
||||||
|
@ -105,20 +139,20 @@ class HasValueContainsViewFilterType(ViewFilterType):
|
||||||
FormulaFieldType.array_of(BaserowFormulaCharType.type),
|
FormulaFieldType.array_of(BaserowFormulaCharType.type),
|
||||||
FormulaFieldType.array_of(BaserowFormulaURLType.type),
|
FormulaFieldType.array_of(BaserowFormulaURLType.type),
|
||||||
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
|
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
|
||||||
|
FormulaFieldType.array_of(BaserowFormulaNumberType.type),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||||
field_type = field_type_registry.get_by_model(field)
|
if value == "":
|
||||||
try:
|
return Q()
|
||||||
if not isinstance(field_type, HasValueContainsFilterSupport):
|
|
||||||
raise FilterNotSupportedException(field_type)
|
|
||||||
|
|
||||||
return field_type.get_in_array_contains_query(
|
field_type: HasValueContainsFilterSupport = field_type_registry.get_by_model(
|
||||||
field_name, value, model_field, field
|
field
|
||||||
)
|
)
|
||||||
except FilterNotSupportedException:
|
return field_type.get_in_array_contains_query(
|
||||||
return self.default_filter_on_exception()
|
field_name, value, model_field, field
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HasNotValueContainsViewFilterType(
|
class HasNotValueContainsViewFilterType(
|
||||||
|
@ -144,16 +178,15 @@ class HasValueContainsWordViewFilterType(ViewFilterType):
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||||
field_type = field_type_registry.get_by_model(field)
|
if value == "":
|
||||||
try:
|
return Q()
|
||||||
if not isinstance(field_type, HasValueContainsWordFilterSupport):
|
|
||||||
raise FilterNotSupportedException(field_type)
|
|
||||||
|
|
||||||
return field_type.get_in_array_contains_word_query(
|
field_type: HasValueContainsWordFilterSupport = (
|
||||||
field_name, value, model_field, field
|
field_type_registry.get_by_model(field)
|
||||||
)
|
)
|
||||||
except FilterNotSupportedException:
|
return field_type.get_in_array_contains_word_query(
|
||||||
return self.default_filter_on_exception()
|
field_name, value, model_field, field
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HasNotValueContainsWordViewFilterType(
|
class HasNotValueContainsWordViewFilterType(
|
||||||
|
@ -178,19 +211,25 @@ class HasValueLengthIsLowerThanViewFilterType(ViewFilterType):
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||||
field_type = field_type_registry.get_by_model(field)
|
value = value.strip()
|
||||||
try:
|
if value == "":
|
||||||
if not isinstance(field_type, HasValueLengthIsLowerThanFilterSupport):
|
return Q()
|
||||||
raise FilterNotSupportedException(field_type)
|
|
||||||
|
|
||||||
return field_type.get_in_array_length_is_lower_than_query(
|
try:
|
||||||
field_name, value, model_field, field
|
# The value is expected to be an integer representing the length to compare
|
||||||
)
|
filter_value = int(value)
|
||||||
except FilterNotSupportedException:
|
except (ValueError, TypeError):
|
||||||
return self.default_filter_on_exception()
|
return self.default_filter_on_exception()
|
||||||
|
|
||||||
|
field_type: HasValueLengthIsLowerThanFilterSupport = (
|
||||||
|
field_type_registry.get_by_model(field)
|
||||||
|
)
|
||||||
|
return field_type.get_in_array_length_is_lower_than_query(
|
||||||
|
field_name, filter_value, model_field, field
|
||||||
|
)
|
||||||
|
|
||||||
class HasAllValuesEqualViewFilterType(ViewFilterType):
|
|
||||||
|
class HasAllValuesEqualViewFilterType(ComparisonHasValueFilter):
|
||||||
"""
|
"""
|
||||||
The filter checks if all values in an array are equal to a specific value.
|
The filter checks if all values in an array are equal to a specific value.
|
||||||
"""
|
"""
|
||||||
|
@ -202,16 +241,15 @@ class HasAllValuesEqualViewFilterType(ViewFilterType):
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
def get_filter_expression(
|
||||||
try:
|
self, field_name, value, model_field, field
|
||||||
field_type = field_type_registry.get_by_model(field)
|
) -> OptionallyAnnotatedQ:
|
||||||
if not isinstance(field_type, HasAllValuesEqualFilterSupport):
|
field_type: HasAllValuesEqualViewFilterType = field_type_registry.get_by_model(
|
||||||
raise FilterNotSupportedException(field_type)
|
field
|
||||||
return field_type.get_has_all_values_equal_query(
|
)
|
||||||
field_name, value, model_field, field
|
return field_type.get_has_all_values_equal_query(
|
||||||
)
|
field_name, value, model_field, field
|
||||||
except FilterNotSupportedException:
|
)
|
||||||
return self.default_filter_on_exception()
|
|
||||||
|
|
||||||
|
|
||||||
class HasAnySelectOptionEqualViewFilterType(HasValueEqualViewFilterType):
|
class HasAnySelectOptionEqualViewFilterType(HasValueEqualViewFilterType):
|
||||||
|
@ -228,6 +266,9 @@ class HasAnySelectOptionEqualViewFilterType(HasValueEqualViewFilterType):
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||||
|
if value == "":
|
||||||
|
return Q()
|
||||||
|
|
||||||
return super().get_filter(field_name, value.split(","), model_field, field)
|
return super().get_filter(field_name, value.split(","), model_field, field)
|
||||||
|
|
||||||
|
|
||||||
|
@ -240,3 +281,101 @@ class HasNoneSelectOptionEqualViewFilterType(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
type = "has_none_select_option_equal"
|
type = "has_none_select_option_equal"
|
||||||
|
|
||||||
|
|
||||||
|
class hasValueComparableToFilter(ComparisonHasValueFilter):
|
||||||
|
type = "has_value_higher"
|
||||||
|
compatible_field_types = [
|
||||||
|
FormulaFieldType.compatible_with_formula_types(
|
||||||
|
FormulaFieldType.array_of(BaserowFormulaNumberType.type)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_filter_expression(self, field_name, value, model_field, field):
|
||||||
|
field_type: HasNumericValueComparableToFilterSupport = (
|
||||||
|
field_type_registry.get_by_model(field)
|
||||||
|
)
|
||||||
|
return field_type.get_has_numeric_value_comparable_to_filter_query(
|
||||||
|
field_name, value, model_field, field, ComparisonOperator.HIGHER_THAN
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HasNotValueHigherThanFilterType(
|
||||||
|
NotViewFilterTypeMixin, hasValueComparableToFilter
|
||||||
|
):
|
||||||
|
type = "has_not_value_higher"
|
||||||
|
|
||||||
|
|
||||||
|
class HasValueHigherOrEqualThanFilter(ComparisonHasValueFilter):
|
||||||
|
type = "has_value_higher_or_equal"
|
||||||
|
compatible_field_types = [
|
||||||
|
FormulaFieldType.compatible_with_formula_types(
|
||||||
|
FormulaFieldType.array_of(BaserowFormulaNumberType.type)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_filter_expression(self, field_name, value, model_field, field):
|
||||||
|
field_type: HasNumericValueComparableToFilterSupport = (
|
||||||
|
field_type_registry.get_by_model(field)
|
||||||
|
)
|
||||||
|
return field_type.get_has_numeric_value_comparable_to_filter_query(
|
||||||
|
field_name,
|
||||||
|
value,
|
||||||
|
model_field,
|
||||||
|
field,
|
||||||
|
ComparisonOperator.HIGHER_THAN_OR_EQUAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HasNotValueHigherOrEqualTHanFilterType(
|
||||||
|
NotViewFilterTypeMixin, HasValueHigherOrEqualThanFilter
|
||||||
|
):
|
||||||
|
type = "has_not_value_higher_or_equal"
|
||||||
|
|
||||||
|
|
||||||
|
class HasValueLowerThanFilter(ComparisonHasValueFilter):
|
||||||
|
type = "has_value_lower"
|
||||||
|
compatible_field_types = [
|
||||||
|
FormulaFieldType.compatible_with_formula_types(
|
||||||
|
FormulaFieldType.array_of(BaserowFormulaNumberType.type)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_filter_expression(self, field_name, value, model_field, field):
|
||||||
|
field_type: HasNumericValueComparableToFilterSupport = (
|
||||||
|
field_type_registry.get_by_model(field)
|
||||||
|
)
|
||||||
|
return field_type.get_has_numeric_value_comparable_to_filter_query(
|
||||||
|
field_name, value, model_field, field, ComparisonOperator.LOWER_THAN
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HasNotValueLowerThanFilterType(NotViewFilterTypeMixin, HasValueLowerThanFilter):
|
||||||
|
type = "has_not_value_lower"
|
||||||
|
|
||||||
|
|
||||||
|
class HasValueLowerOrEqualThanFilter(ComparisonHasValueFilter):
|
||||||
|
type = "has_value_lower_or_equal"
|
||||||
|
compatible_field_types = [
|
||||||
|
FormulaFieldType.compatible_with_formula_types(
|
||||||
|
FormulaFieldType.array_of(BaserowFormulaNumberType.type)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_filter_expression(self, field_name, value, model_field, field):
|
||||||
|
field_type: HasNumericValueComparableToFilterSupport = (
|
||||||
|
field_type_registry.get_by_model(field)
|
||||||
|
)
|
||||||
|
return field_type.get_has_numeric_value_comparable_to_filter_query(
|
||||||
|
field_name,
|
||||||
|
value,
|
||||||
|
model_field,
|
||||||
|
field,
|
||||||
|
ComparisonOperator.LOWER_THAN_OR_EQUAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HasNotValueLowerOrEqualTHanFilterType(
|
||||||
|
NotViewFilterTypeMixin, HasValueLowerOrEqualThanFilter
|
||||||
|
):
|
||||||
|
type = "has_not_value_lower_or_equal"
|
||||||
|
|
|
@ -2,10 +2,8 @@ import datetime as datetime_module
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from decimal import Decimal
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from math import ceil, floor
|
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import Any, Dict, NamedTuple, Optional, Tuple, Union
|
from typing import Any, Dict, NamedTuple, Optional, Tuple, Union
|
||||||
|
|
||||||
|
@ -124,8 +122,9 @@ class EqualViewFilterType(ViewFilterType):
|
||||||
return Q()
|
return Q()
|
||||||
|
|
||||||
# Check if the model_field accepts the value.
|
# Check if the model_field accepts the value.
|
||||||
|
field_type = field_type_registry.get_by_model(field)
|
||||||
try:
|
try:
|
||||||
value = model_field.get_prep_value(value)
|
value = field_type.prepare_filter_value(field, model_field, value)
|
||||||
return Q(**{field_name: value})
|
return Q(**{field_name: value})
|
||||||
except Exception:
|
except Exception:
|
||||||
return self.default_filter_on_exception()
|
return self.default_filter_on_exception()
|
||||||
|
@ -379,9 +378,6 @@ class NumericComparisonViewFilterType(ViewFilterType):
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def should_round_value_to_compare(self, value, model_field):
|
|
||||||
return isinstance(model_field, IntegerField) and value.find(".") != -1
|
|
||||||
|
|
||||||
def get_filter(self, field_name, value, model_field, field):
|
def get_filter(self, field_name, value, model_field, field):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
|
|
||||||
|
@ -389,17 +385,14 @@ class NumericComparisonViewFilterType(ViewFilterType):
|
||||||
if value == "":
|
if value == "":
|
||||||
return Q()
|
return Q()
|
||||||
|
|
||||||
if self.should_round_value_to_compare(value, model_field):
|
field_type = field_type_registry.get_by_model(field)
|
||||||
decimal_value = Decimal(value)
|
|
||||||
value = self.rounding_func(decimal_value)
|
|
||||||
|
|
||||||
# Check if the model_field accepts the value.
|
|
||||||
try:
|
try:
|
||||||
value = model_field.get_prep_value(value)
|
filter_value = field_type.prepare_filter_value(field, model_field, value)
|
||||||
return Q(**{f"{field_name}__{self.operator}": value})
|
except ValueError:
|
||||||
except Exception:
|
|
||||||
return self.default_filter_on_exception()
|
return self.default_filter_on_exception()
|
||||||
|
|
||||||
|
return Q(**{f"{field_name}__{self.operator}": filter_value})
|
||||||
|
|
||||||
|
|
||||||
class LowerThanViewFilterType(NumericComparisonViewFilterType):
|
class LowerThanViewFilterType(NumericComparisonViewFilterType):
|
||||||
"""
|
"""
|
||||||
|
@ -409,7 +402,6 @@ class LowerThanViewFilterType(NumericComparisonViewFilterType):
|
||||||
|
|
||||||
type = "lower_than"
|
type = "lower_than"
|
||||||
operator = "lt"
|
operator = "lt"
|
||||||
rounding_func = floor
|
|
||||||
|
|
||||||
|
|
||||||
class LowerThanOrEqualViewFilterType(NumericComparisonViewFilterType):
|
class LowerThanOrEqualViewFilterType(NumericComparisonViewFilterType):
|
||||||
|
@ -421,7 +413,6 @@ class LowerThanOrEqualViewFilterType(NumericComparisonViewFilterType):
|
||||||
|
|
||||||
type = "lower_than_or_equal"
|
type = "lower_than_or_equal"
|
||||||
operator = "lte"
|
operator = "lte"
|
||||||
rounding_func = floor
|
|
||||||
|
|
||||||
|
|
||||||
class HigherThanViewFilterType(NumericComparisonViewFilterType):
|
class HigherThanViewFilterType(NumericComparisonViewFilterType):
|
||||||
|
@ -432,7 +423,6 @@ class HigherThanViewFilterType(NumericComparisonViewFilterType):
|
||||||
|
|
||||||
type = "higher_than"
|
type = "higher_than"
|
||||||
operator = "gt"
|
operator = "gt"
|
||||||
rounding_func = ceil
|
|
||||||
|
|
||||||
|
|
||||||
class HigherThanOrEqualViewFilterType(NumericComparisonViewFilterType):
|
class HigherThanOrEqualViewFilterType(NumericComparisonViewFilterType):
|
||||||
|
@ -444,7 +434,6 @@ class HigherThanOrEqualViewFilterType(NumericComparisonViewFilterType):
|
||||||
|
|
||||||
type = "higher_than_or_equal"
|
type = "higher_than_or_equal"
|
||||||
operator = "gte"
|
operator = "gte"
|
||||||
rounding_func = ceil
|
|
||||||
|
|
||||||
|
|
||||||
class TimezoneAwareDateViewFilterType(ViewFilterType):
|
class TimezoneAwareDateViewFilterType(ViewFilterType):
|
||||||
|
@ -1217,24 +1206,15 @@ class BooleanViewFilterType(ViewFilterType):
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_filter(self, field_name, value, model_field, field):
|
def get_filter(self, field_name, value, model_field, field):
|
||||||
value = value.strip().lower()
|
if value == "": # consider emtpy value as False
|
||||||
value = value in [
|
value = "false"
|
||||||
"y",
|
|
||||||
"t",
|
|
||||||
"o",
|
|
||||||
"yes",
|
|
||||||
"true",
|
|
||||||
"on",
|
|
||||||
"1",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Check if the model_field accepts the value.
|
field_type = field_type_registry.get_by_model(field)
|
||||||
# noinspection PyBroadException
|
|
||||||
try:
|
try:
|
||||||
value = model_field.get_prep_value(value)
|
value = field_type.prepare_filter_value(field, model_field, value)
|
||||||
return Q(**{field_name: value})
|
return Q(**{field_name: value})
|
||||||
except Exception:
|
except ValueError:
|
||||||
return Q()
|
return self.default_filter_on_exception()
|
||||||
|
|
||||||
|
|
||||||
class ManyToManyHasBaseViewFilter(ViewFilterType):
|
class ManyToManyHasBaseViewFilter(ViewFilterType):
|
||||||
|
|
|
@ -0,0 +1,561 @@
|
||||||
|
import typing
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.baserow.contrib.database.utils import (
|
||||||
|
number_field_factory,
|
||||||
|
setup_linked_table_and_lookup,
|
||||||
|
text_field_factory,
|
||||||
|
)
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from baserow.test_utils.fixtures import Fixtures
|
||||||
|
|
||||||
|
|
||||||
|
def number_lookup_filter_proc(
|
||||||
|
data_fixture: "Fixtures",
|
||||||
|
filter_type_name: str,
|
||||||
|
test_value: str,
|
||||||
|
expected_rows: set[str],
|
||||||
|
number_decimal_places: int = 5,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Common numeric lookup field test procedure. Each test operates on a fixed set of
|
||||||
|
data, where each table row contains a lookup field with a predefined set of linked
|
||||||
|
rows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
t = setup_linked_table_and_lookup(
|
||||||
|
data_fixture,
|
||||||
|
target_field_factory=partial(
|
||||||
|
number_field_factory,
|
||||||
|
number_decimal_places=number_decimal_places,
|
||||||
|
number_negative=True,
|
||||||
|
),
|
||||||
|
helper_fields_table=[partial(text_field_factory, name="row name")],
|
||||||
|
)
|
||||||
|
|
||||||
|
row_name_field = t.model.get_field_object_by_user_field_name("row name")["field"]
|
||||||
|
link_row_name = t.link_row_field.db_column
|
||||||
|
target_name = t.target_field.db_column
|
||||||
|
lookup_name = t.lookup_field.db_column
|
||||||
|
|
||||||
|
row_name = row_name_field.db_column
|
||||||
|
|
||||||
|
row_values = [
|
||||||
|
"1000.0001", # 0
|
||||||
|
"1000",
|
||||||
|
"999.999",
|
||||||
|
"123.45",
|
||||||
|
"100",
|
||||||
|
"50", # 5
|
||||||
|
"1.000001", # will be rounded to 1.00000
|
||||||
|
"1",
|
||||||
|
"0.00007",
|
||||||
|
"0.00004",
|
||||||
|
"0.00001", # 10
|
||||||
|
"0.0000",
|
||||||
|
"-0.1", # mind that it will be rounded to 0.10000
|
||||||
|
"-2",
|
||||||
|
None, # 14
|
||||||
|
"999.99999999",
|
||||||
|
]
|
||||||
|
dict_rows = [{target_name: rval} for rval in row_values]
|
||||||
|
|
||||||
|
linked_rows = t.row_handler.create_rows(
|
||||||
|
user=t.user, table=t.other_table, rows_values=dict_rows
|
||||||
|
)
|
||||||
|
|
||||||
|
# helper to get linked rows by indexes
|
||||||
|
def get_linked_rows(*indexes) -> list[int]:
|
||||||
|
return [linked_rows[idx].id for idx in indexes]
|
||||||
|
|
||||||
|
rows = [
|
||||||
|
{row_name: "above 100", link_row_name: get_linked_rows(0, 1, 2, 3)},
|
||||||
|
{row_name: "exact 100", link_row_name: get_linked_rows(4)},
|
||||||
|
{row_name: "between 100 and 10", link_row_name: get_linked_rows(4, 5)},
|
||||||
|
{
|
||||||
|
row_name: "between 10 and 0",
|
||||||
|
link_row_name: get_linked_rows(6, 7, 8, 9, 10, 11),
|
||||||
|
},
|
||||||
|
{row_name: "zero", link_row_name: get_linked_rows(11)},
|
||||||
|
{row_name: "below zero", link_row_name: get_linked_rows(12, 13)},
|
||||||
|
{row_name: "nineninenine", link_row_name: get_linked_rows(2)},
|
||||||
|
{row_name: "onetwothree", link_row_name: get_linked_rows(3)},
|
||||||
|
# no refs is a tricky one - we don't have is_empty for numeric field
|
||||||
|
{row_name: "no refs", link_row_name: []},
|
||||||
|
{row_name: "refs with empty", link_row_name: get_linked_rows(14)},
|
||||||
|
{row_name: "100 with empty", link_row_name: get_linked_rows(4, 14)},
|
||||||
|
{row_name: "ninieninenine rounded", link_row_name: get_linked_rows(15)},
|
||||||
|
]
|
||||||
|
|
||||||
|
t.row_handler.create_rows(user=t.user, table=t.table, rows_values=rows)
|
||||||
|
|
||||||
|
clean_query = t.view_handler.get_queryset(t.grid_view)
|
||||||
|
|
||||||
|
t.view_handler.create_filter(
|
||||||
|
t.user,
|
||||||
|
t.grid_view,
|
||||||
|
field=t.lookup_field,
|
||||||
|
type_name=filter_type_name,
|
||||||
|
value=test_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
q = t.view_handler.get_queryset(t.grid_view)
|
||||||
|
print(f"filter {filter_type_name} with value: {(test_value,)}")
|
||||||
|
print(f"expected: {expected_rows}")
|
||||||
|
print(f"filtered: {[getattr(item, row_name) for item in q]}")
|
||||||
|
for item in q:
|
||||||
|
print(f" {item.id} -> {getattr(item, row_name)}: {getattr(item, lookup_name)}")
|
||||||
|
print()
|
||||||
|
for item in clean_query:
|
||||||
|
print(f" {item.id} -> {getattr(item, row_name)}: {getattr(item, lookup_name)}")
|
||||||
|
assert len(q) == len(expected_rows)
|
||||||
|
assert set([getattr(r, row_name) for r in q]) == set(expected_rows)
|
||||||
|
|
||||||
|
|
||||||
|
ALL_ROW_NAMES = [
|
||||||
|
"above 100",
|
||||||
|
"exact 100",
|
||||||
|
"between 100 and 10",
|
||||||
|
"between 10 and 0",
|
||||||
|
"zero",
|
||||||
|
"below zero",
|
||||||
|
"nineninenine",
|
||||||
|
"onetwothree",
|
||||||
|
"no refs",
|
||||||
|
"refs with empty",
|
||||||
|
"100 with empty",
|
||||||
|
"ninieninenine rounded",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filter_type_name,expected_rows",
|
||||||
|
[
|
||||||
|
("has_empty_value", ["refs with empty", "100 with empty"]),
|
||||||
|
(
|
||||||
|
"has_not_empty_value",
|
||||||
|
[
|
||||||
|
"above 100",
|
||||||
|
"exact 100",
|
||||||
|
"between 100 and 10",
|
||||||
|
"between 10 and 0",
|
||||||
|
"zero",
|
||||||
|
"below zero",
|
||||||
|
"nineninenine",
|
||||||
|
"ninieninenine rounded",
|
||||||
|
"onetwothree",
|
||||||
|
"no refs", # this is due to inversion of has_empty_value
|
||||||
|
# "refs with empty", "100 with empty"
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_number_lookup_field_has_empty_value_filter(
|
||||||
|
data_fixture, filter_type_name, expected_rows
|
||||||
|
):
|
||||||
|
return number_lookup_filter_proc(data_fixture, filter_type_name, "", expected_rows)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filter_type_name,test_value,expected_rows",
|
||||||
|
[
|
||||||
|
("has_value_equal", "", ALL_ROW_NAMES),
|
||||||
|
("has_value_equal", "invalid", []),
|
||||||
|
# too large, the same as invalid
|
||||||
|
(
|
||||||
|
"has_value_equal",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_equal",
|
||||||
|
"100",
|
||||||
|
["exact 100", "between 100 and 10", "100 with empty"],
|
||||||
|
),
|
||||||
|
# no rounding to 0.00001
|
||||||
|
("has_value_equal", "100.00000000001", []),
|
||||||
|
("has_not_value_equal", "", ALL_ROW_NAMES),
|
||||||
|
("has_not_value_equal", "invalid", ALL_ROW_NAMES),
|
||||||
|
(
|
||||||
|
"has_not_value_equal",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
ALL_ROW_NAMES,
|
||||||
|
),
|
||||||
|
("has_not_value_equal", "999", ALL_ROW_NAMES),
|
||||||
|
("has_not_value_equal", "1.00000001", ALL_ROW_NAMES),
|
||||||
|
(
|
||||||
|
"has_not_value_equal",
|
||||||
|
"0.0",
|
||||||
|
[o for o in ALL_ROW_NAMES if o not in {"zero", "between 10 and 0"}],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_number_lookup_field_has_value_equal_filter(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
):
|
||||||
|
return number_lookup_filter_proc(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filter_type_name,test_value,expected_rows",
|
||||||
|
[
|
||||||
|
("has_value_contains", "", ALL_ROW_NAMES),
|
||||||
|
("has_value_contains", "invalid", []),
|
||||||
|
(
|
||||||
|
"has_value_contains",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_contains",
|
||||||
|
"100",
|
||||||
|
# includes '0.10000'
|
||||||
|
[
|
||||||
|
"above 100",
|
||||||
|
"exact 100",
|
||||||
|
"between 100 and 10",
|
||||||
|
"below zero",
|
||||||
|
"100 with empty",
|
||||||
|
"ninieninenine rounded",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
("has_value_contains", "999", ["nineninenine", "above 100"]),
|
||||||
|
("has_value_contains", "99.999", ["nineninenine", "above 100"]),
|
||||||
|
("has_not_value_contains", "", ALL_ROW_NAMES),
|
||||||
|
("has_not_value_contains", "invalid", ALL_ROW_NAMES),
|
||||||
|
(
|
||||||
|
"has_not_value_contains",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
ALL_ROW_NAMES,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_not_value_contains",
|
||||||
|
"999",
|
||||||
|
[o for o in ALL_ROW_NAMES if o not in {"nineninenine", "above 100"}],
|
||||||
|
),
|
||||||
|
("has_not_value_contains", "1.00000001", ALL_ROW_NAMES),
|
||||||
|
(
|
||||||
|
"has_not_value_contains",
|
||||||
|
"001",
|
||||||
|
# between 10 and 0 contains 1.00001
|
||||||
|
# above 100 contains 1000.0001
|
||||||
|
[o for o in ALL_ROW_NAMES if o not in {"between 10 and 0", "above 100"}],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_number_lookup_field_has_value_contains_filter(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
):
|
||||||
|
return number_lookup_filter_proc(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filter_type_name,test_value,expected_rows",
|
||||||
|
[
|
||||||
|
("has_value_higher", "", ALL_ROW_NAMES),
|
||||||
|
("has_value_higher", "invalid", []),
|
||||||
|
(
|
||||||
|
"has_value_higher",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_higher",
|
||||||
|
"-1" + ("0" * 40),
|
||||||
|
[o for o in ALL_ROW_NAMES if o not in {"no refs", "refs with empty"}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_higher",
|
||||||
|
"-0.0000001",
|
||||||
|
[
|
||||||
|
"above 100",
|
||||||
|
"exact 100",
|
||||||
|
"between 100 and 10",
|
||||||
|
"between 10 and 0",
|
||||||
|
"zero",
|
||||||
|
"nineninenine",
|
||||||
|
"onetwothree",
|
||||||
|
"100 with empty",
|
||||||
|
"ninieninenine rounded",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_higher",
|
||||||
|
"0.00000001",
|
||||||
|
[
|
||||||
|
"above 100",
|
||||||
|
"exact 100",
|
||||||
|
"between 100 and 10",
|
||||||
|
"between 10 and 0",
|
||||||
|
"nineninenine",
|
||||||
|
"onetwothree",
|
||||||
|
"100 with empty",
|
||||||
|
"ninieninenine rounded",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_higher",
|
||||||
|
"999.998999", # not rounded
|
||||||
|
["above 100", "nineninenine", "ninieninenine rounded"],
|
||||||
|
),
|
||||||
|
("has_value_higher", "999.999", ["above 100", "ninieninenine rounded"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_number_lookup_field_has_value_higher_than_filter(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
):
|
||||||
|
return number_lookup_filter_proc(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filter_type_name,test_value,expected_rows",
|
||||||
|
[
|
||||||
|
("has_value_higher_or_equal", "", ALL_ROW_NAMES),
|
||||||
|
("has_value_higher_or_equal", "invalid", []),
|
||||||
|
(
|
||||||
|
"has_value_higher_or_equal",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_higher_or_equal",
|
||||||
|
"-1" + ("0" * 40),
|
||||||
|
[o for o in ALL_ROW_NAMES if o not in {"no refs", "refs with empty"}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_higher_or_equal",
|
||||||
|
"-0.0",
|
||||||
|
[
|
||||||
|
"above 100",
|
||||||
|
"exact 100",
|
||||||
|
"between 100 and 10",
|
||||||
|
"between 10 and 0",
|
||||||
|
"zero",
|
||||||
|
"nineninenine",
|
||||||
|
"onetwothree",
|
||||||
|
"100 with empty",
|
||||||
|
"ninieninenine rounded",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_higher_or_equal",
|
||||||
|
"999.999",
|
||||||
|
["above 100", "nineninenine", "ninieninenine rounded"],
|
||||||
|
),
|
||||||
|
("has_not_value_higher_or_equal", "", ALL_ROW_NAMES),
|
||||||
|
(
|
||||||
|
"has_not_value_higher_or_equal",
|
||||||
|
"invalid",
|
||||||
|
ALL_ROW_NAMES,
|
||||||
|
), # reversed has_value_higher_or_equal
|
||||||
|
(
|
||||||
|
"has_not_value_higher_or_equal",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
ALL_ROW_NAMES, # reversed has_value_higher_or_equal
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_not_value_higher_or_equal",
|
||||||
|
"-1" + ("0" * 40),
|
||||||
|
[o for o in ALL_ROW_NAMES if o in {"no refs", "refs with empty"}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_not_value_higher_or_equal",
|
||||||
|
"-0.0",
|
||||||
|
["below zero", "no refs", "refs with empty"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_not_value_higher_or_equal",
|
||||||
|
"999.999",
|
||||||
|
[
|
||||||
|
o
|
||||||
|
for o in ALL_ROW_NAMES
|
||||||
|
if o not in {"above 100", "nineninenine", "ninieninenine rounded"}
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_number_lookup_field_has_value_higher_equal_than_filter(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
):
|
||||||
|
return number_lookup_filter_proc(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filter_type_name,test_value,expected_rows",
|
||||||
|
[
|
||||||
|
("has_value_lower_or_equal", "", ALL_ROW_NAMES),
|
||||||
|
("has_value_lower_or_equal", "invalid", []),
|
||||||
|
(
|
||||||
|
"has_value_lower_or_equal",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
[o for o in ALL_ROW_NAMES if o not in {"no refs", "refs with empty"}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_lower_or_equal",
|
||||||
|
"-1" + ("0" * 40),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_lower_or_equal",
|
||||||
|
"-0.0",
|
||||||
|
["between 10 and 0", "zero", "below zero"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_lower_or_equal",
|
||||||
|
"999.999",
|
||||||
|
[
|
||||||
|
"above 100",
|
||||||
|
"exact 100",
|
||||||
|
"between 100 and 10",
|
||||||
|
"between 10 and 0",
|
||||||
|
"zero",
|
||||||
|
"below zero",
|
||||||
|
"nineninenine",
|
||||||
|
"onetwothree",
|
||||||
|
"100 with empty",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
("has_not_value_lower_or_equal", "", ALL_ROW_NAMES),
|
||||||
|
(
|
||||||
|
"has_not_value_lower_or_equal",
|
||||||
|
"invalid",
|
||||||
|
ALL_ROW_NAMES,
|
||||||
|
), # reversed has_value_lower_or_equal
|
||||||
|
(
|
||||||
|
"has_not_value_lower_or_equal",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
["no refs", "refs with empty"], # reversed has_value_lower_or_equal
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_not_value_lower_or_equal",
|
||||||
|
"-1" + ("0" * 40),
|
||||||
|
ALL_ROW_NAMES,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_not_value_lower_or_equal",
|
||||||
|
"-0.0",
|
||||||
|
[
|
||||||
|
"above 100",
|
||||||
|
"exact 100",
|
||||||
|
"between 100 and 10",
|
||||||
|
"nineninenine",
|
||||||
|
"onetwothree",
|
||||||
|
"no refs",
|
||||||
|
"refs with empty",
|
||||||
|
"100 with empty",
|
||||||
|
"ninieninenine rounded",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_not_value_lower_or_equal",
|
||||||
|
"999.999",
|
||||||
|
["no refs", "refs with empty", "ninieninenine rounded"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_number_lookup_field_has_value_lower_equal_than_filter(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
):
|
||||||
|
return number_lookup_filter_proc(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"filter_type_name,test_value,expected_rows",
|
||||||
|
[
|
||||||
|
("has_value_lower", "", ALL_ROW_NAMES),
|
||||||
|
("has_value_lower", "invalid", []),
|
||||||
|
(
|
||||||
|
"has_value_lower",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
[o for o in ALL_ROW_NAMES if o not in {"no refs", "refs with empty"}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_lower",
|
||||||
|
"-1" + ("0" * 40),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_lower",
|
||||||
|
"-0.0",
|
||||||
|
["below zero"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_value_lower",
|
||||||
|
"999.999",
|
||||||
|
[
|
||||||
|
"above 100",
|
||||||
|
"exact 100",
|
||||||
|
"between 100 and 10",
|
||||||
|
"between 10 and 0",
|
||||||
|
"zero",
|
||||||
|
"below zero",
|
||||||
|
"onetwothree",
|
||||||
|
"100 with empty",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
("has_not_value_lower", "", ALL_ROW_NAMES),
|
||||||
|
(
|
||||||
|
"has_not_value_lower",
|
||||||
|
"invalid",
|
||||||
|
ALL_ROW_NAMES,
|
||||||
|
), # reversed has_value_lower
|
||||||
|
(
|
||||||
|
"has_not_value_lower",
|
||||||
|
"1" + ("0" * 40),
|
||||||
|
["no refs", "refs with empty"], # reversed has_value_lower
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_not_value_lower",
|
||||||
|
"-1" + ("0" * 40),
|
||||||
|
ALL_ROW_NAMES,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_not_value_lower",
|
||||||
|
"-0.0",
|
||||||
|
[
|
||||||
|
"above 100",
|
||||||
|
"exact 100",
|
||||||
|
"between 100 and 10",
|
||||||
|
"between 10 and 0",
|
||||||
|
"zero",
|
||||||
|
"nineninenine",
|
||||||
|
"onetwothree",
|
||||||
|
"no refs",
|
||||||
|
"refs with empty",
|
||||||
|
"100 with empty",
|
||||||
|
"ninieninenine rounded",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_not_value_lower",
|
||||||
|
"999.999",
|
||||||
|
["nineninenine", "no refs", "refs with empty", "ninieninenine rounded"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_number_lookup_field_has_value_lower_than_filter(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
):
|
||||||
|
return number_lookup_filter_proc(
|
||||||
|
data_fixture, filter_type_name, test_value, expected_rows
|
||||||
|
)
|
|
@ -142,13 +142,18 @@ def text_field_value_factory(data_fixture, target_field, value=None):
|
||||||
|
|
||||||
|
|
||||||
def setup_linked_table_and_lookup(
|
def setup_linked_table_and_lookup(
|
||||||
data_fixture, target_field_factory
|
data_fixture,
|
||||||
|
target_field_factory,
|
||||||
|
helper_fields_other_table: Iterable[Callable] = frozenset(),
|
||||||
|
helper_fields_table: Iterable[Callable] = frozenset(),
|
||||||
) -> LookupFieldSetup:
|
) -> LookupFieldSetup:
|
||||||
user = data_fixture.create_user()
|
user = data_fixture.create_user()
|
||||||
database = data_fixture.create_database_application(user=user)
|
database = data_fixture.create_database_application(user=user)
|
||||||
table = data_fixture.create_database_table(user=user, database=database)
|
table = data_fixture.create_database_table(user=user, database=database)
|
||||||
other_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)
|
target_field = target_field_factory(data_fixture, other_table, user)
|
||||||
|
for helper_field_factory in helper_fields_other_table:
|
||||||
|
helper_field_factory(data_fixture, table=other_table, user=user)
|
||||||
link_row_field = data_fixture.create_link_row_field(
|
link_row_field = data_fixture.create_link_row_field(
|
||||||
name="link", table=table, link_row_table=other_table
|
name="link", table=table, link_row_table=other_table
|
||||||
)
|
)
|
||||||
|
@ -160,6 +165,8 @@ def setup_linked_table_and_lookup(
|
||||||
target_field_name=target_field.name,
|
target_field_name=target_field.name,
|
||||||
setup_dependencies=False,
|
setup_dependencies=False,
|
||||||
)
|
)
|
||||||
|
for helper_field_factory in helper_fields_table:
|
||||||
|
helper_field_factory(data_fixture, table=table, user=user)
|
||||||
grid_view = data_fixture.create_grid_view(table=table)
|
grid_view = data_fixture.create_grid_view(table=table)
|
||||||
view_handler = ViewHandler()
|
view_handler = ViewHandler()
|
||||||
row_handler = RowHandler()
|
row_handler = RowHandler()
|
||||||
|
|
|
@ -1629,12 +1629,17 @@ def test_has_value_length_is_lower_than_uuid_field_types(data_fixture):
|
||||||
(
|
(
|
||||||
"has_all_values_equal",
|
"has_all_values_equal",
|
||||||
"",
|
"",
|
||||||
[BooleanLookupRow.ALL_FALSE],
|
[
|
||||||
|
BooleanLookupRow.ALL_TRUE,
|
||||||
|
BooleanLookupRow.ALL_FALSE,
|
||||||
|
BooleanLookupRow.NO_VALUES,
|
||||||
|
BooleanLookupRow.MIXED,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"has_all_values_equal",
|
"has_all_values_equal",
|
||||||
"invalid",
|
"invalid",
|
||||||
[BooleanLookupRow.ALL_FALSE],
|
[],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1683,12 +1688,17 @@ def test_has_all_values_equal_filter_boolean_lookup_field_type(
|
||||||
(
|
(
|
||||||
"has_value_equal",
|
"has_value_equal",
|
||||||
"",
|
"",
|
||||||
[BooleanLookupRow.MIXED, BooleanLookupRow.ALL_FALSE],
|
[
|
||||||
|
BooleanLookupRow.MIXED,
|
||||||
|
BooleanLookupRow.ALL_FALSE,
|
||||||
|
BooleanLookupRow.NO_VALUES,
|
||||||
|
BooleanLookupRow.ALL_TRUE,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"has_value_equal",
|
"has_value_equal",
|
||||||
"invalid",
|
"invalid",
|
||||||
[BooleanLookupRow.MIXED, BooleanLookupRow.ALL_FALSE],
|
[],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1737,12 +1747,23 @@ def test_has_value_equal_filter_boolean_lookup_field_type(
|
||||||
(
|
(
|
||||||
"has_not_value_equal",
|
"has_not_value_equal",
|
||||||
"",
|
"",
|
||||||
[BooleanLookupRow.ALL_TRUE, BooleanLookupRow.NO_VALUES],
|
[
|
||||||
|
BooleanLookupRow.MIXED,
|
||||||
|
BooleanLookupRow.ALL_FALSE,
|
||||||
|
BooleanLookupRow.NO_VALUES,
|
||||||
|
BooleanLookupRow.ALL_TRUE,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"has_not_value_equal",
|
"has_not_value_equal",
|
||||||
"invalid",
|
"invalid",
|
||||||
[BooleanLookupRow.ALL_TRUE, BooleanLookupRow.NO_VALUES],
|
# inverse of has_value_equal with `invalid` value
|
||||||
|
[
|
||||||
|
BooleanLookupRow.ALL_TRUE,
|
||||||
|
BooleanLookupRow.ALL_FALSE,
|
||||||
|
BooleanLookupRow.NO_VALUES,
|
||||||
|
BooleanLookupRow.MIXED,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"type": "feature",
|
||||||
|
"message": "Number lookup field filters",
|
||||||
|
"issue_number": 3112,
|
||||||
|
"bullet_points": [],
|
||||||
|
"created_at": "2024-12-13"
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ import {
|
||||||
genericHasEmptyValueFilter,
|
genericHasEmptyValueFilter,
|
||||||
genericHasValueLengthLowerThanFilter,
|
genericHasValueLengthLowerThanFilter,
|
||||||
genericHasAllValuesEqualFilter,
|
genericHasAllValuesEqualFilter,
|
||||||
|
numericHasValueComparableToFilterFunction,
|
||||||
|
ComparisonOperator,
|
||||||
} from '@baserow/modules/database/utils/fieldFilters'
|
} from '@baserow/modules/database/utils/fieldFilters'
|
||||||
|
|
||||||
export const hasEmptyValueFilterMixin = {
|
export const hasEmptyValueFilterMixin = {
|
||||||
|
@ -24,6 +26,7 @@ export const hasAllValuesEqualFilterMixin = {
|
||||||
this.getHasAllValuesEqualFilterFunction(field)(cellValue, filterValue)
|
this.getHasAllValuesEqualFilterFunction(field)(cellValue, filterValue)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
hasNotAllValuesEqualFilter(cellValue, filterValue, field) {
|
hasNotAllValuesEqualFilter(cellValue, filterValue, field) {
|
||||||
return (
|
return (
|
||||||
filterValue === '' ||
|
filterValue === '' ||
|
||||||
|
@ -91,11 +94,107 @@ export const hasValueLengthIsLowerThanFilterMixin = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formulaArrayFilterMixin = {
|
export const hasNumericValueComparableToFilterMixin = {
|
||||||
getSubType(field) {
|
// equal to
|
||||||
return this.app.$registry.get('formula_type', field.array_formula_type)
|
getHasValueEqualFilterFunction(field) {
|
||||||
|
return numericHasValueComparableToFilterFunction(ComparisonOperator.EQUAL)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasValueEqualFilter(cellValue, filterValue, field) {
|
||||||
|
return (
|
||||||
|
filterValue === '' ||
|
||||||
|
this.getHasValueEqualFilterFunction(field)(cellValue, filterValue)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
hasNotValueEqualFilter(cellValue, filterValue, field) {
|
||||||
|
return (
|
||||||
|
filterValue === '' ||
|
||||||
|
!this.getHasValueEqualFilterFunction(field)(cellValue, filterValue)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All other comparison operators: higher_than, lower_than, etc.
|
||||||
|
*/
|
||||||
|
hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
comparisonOperator
|
||||||
|
) {
|
||||||
|
return numericHasValueComparableToFilterFunction(comparisonOperator)(
|
||||||
|
cellValue,
|
||||||
|
filterValue
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mixin for the FormulaField to handle the array formula filters for number fields.
|
||||||
|
*/
|
||||||
|
export const formulaFieldArrayFilterMixin = Object.assign(
|
||||||
|
{},
|
||||||
|
hasAllValuesEqualFilterMixin,
|
||||||
|
hasEmptyValueFilterMixin,
|
||||||
|
hasValueEqualFilterMixin,
|
||||||
|
hasValueContainsFilterMixin,
|
||||||
|
hasValueContainsWordFilterMixin,
|
||||||
|
hasValueLengthIsLowerThanFilterMixin,
|
||||||
|
{
|
||||||
|
getHasAllValuesEqualFilterFunction(field) {
|
||||||
|
return this.getFormulaType(field)?.getHasAllValuesEqualFilterFunction(
|
||||||
|
field
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
getHasEmptyValueFilterFunction(field) {
|
||||||
|
return this.getFormulaType(field)?.getHasEmptyValueFilterFunction(field)
|
||||||
|
},
|
||||||
|
|
||||||
|
getHasValueEqualFilterFunction(field) {
|
||||||
|
return this.getFormulaType(field)?.getHasValueEqualFilterFunction(field)
|
||||||
|
},
|
||||||
|
|
||||||
|
getHasValueContainsFilterFunction(field) {
|
||||||
|
return this.getFormulaType(field)?.getHasValueContainsFilterFunction(
|
||||||
|
field
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
getHasValueContainsWordFilterFunction(field) {
|
||||||
|
return this.getFormulaType(field)?.getHasValueContainsWordFilterFunction(
|
||||||
|
field
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
getHasValueLengthIsLowerThanFilterFunction(field) {
|
||||||
|
return this.getFormulaType(
|
||||||
|
field
|
||||||
|
)?.getHasValueLengthIsLowerThanFilterFunction(field)
|
||||||
|
},
|
||||||
|
|
||||||
|
hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
comparisonOperator
|
||||||
|
) {
|
||||||
|
return this.getFormulaType(field)?.hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
comparisonOperator
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mixin for the BaserowFormulaArrayType to proxy all the array filters to the
|
||||||
|
* correct sub type.
|
||||||
|
*/
|
||||||
|
export const baserowFormulaArrayTypeFilterMixin = {
|
||||||
getHasEmptyValueFilterFunction(field) {
|
getHasEmptyValueFilterFunction(field) {
|
||||||
const subType = this.getSubType(field)
|
const subType = this.getSubType(field)
|
||||||
return subType.getHasEmptyValueFilterFunction(field)
|
return subType.getHasEmptyValueFilterFunction(field)
|
||||||
|
@ -144,6 +243,20 @@ export const formulaArrayFilterMixin = {
|
||||||
getHasAllValuesEqualFilterFunction(field) {
|
getHasAllValuesEqualFilterFunction(field) {
|
||||||
return this.getSubType(field)?.getHasAllValuesEqualFilterFunction(field)
|
return this.getSubType(field)?.getHasAllValuesEqualFilterFunction(field)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
comparisonOperator
|
||||||
|
) {
|
||||||
|
return this.getSubType(field)?.hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
comparisonOperator
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasSelectOptionIdEqualMixin = Object.assign(
|
export const hasSelectOptionIdEqualMixin = Object.assign(
|
||||||
|
|
|
@ -4,20 +4,8 @@ import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
|
||||||
import { ViewFilterType } from '@baserow/modules/database/viewFilters'
|
import { ViewFilterType } from '@baserow/modules/database/viewFilters'
|
||||||
import viewFilterTypeText from '@baserow/modules/database/components/view/ViewFilterTypeText.vue'
|
import viewFilterTypeText from '@baserow/modules/database/components/view/ViewFilterTypeText.vue'
|
||||||
import ViewFilterTypeMultipleSelectOptions from '@baserow/modules/database/components/view/ViewFilterTypeMultipleSelectOptions'
|
import ViewFilterTypeMultipleSelectOptions from '@baserow/modules/database/components/view/ViewFilterTypeMultipleSelectOptions'
|
||||||
import _ from 'lodash'
|
import { BaserowFormulaNumberType } from '@baserow/modules/database/formula/formulaTypes'
|
||||||
|
import { ComparisonOperator } from '@baserow/modules/database//utils/fieldFilters'
|
||||||
/**
|
|
||||||
* This function normalizes boolean values that may be used internally in the filter
|
|
||||||
* to values that can be transferred to the backend.
|
|
||||||
* @param value
|
|
||||||
* @returns {string|*}
|
|
||||||
*/
|
|
||||||
const normalizeBooleanForFilters = (value) => {
|
|
||||||
if (!_.isBoolean(value)) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return value ? '1' : '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HasEmptyValueViewFilterType extends ViewFilterType {
|
export class HasEmptyValueViewFilterType extends ViewFilterType {
|
||||||
static getType() {
|
static getType() {
|
||||||
|
@ -35,6 +23,7 @@ export class HasEmptyValueViewFilterType extends ViewFilterType {
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
|
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
|
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
|
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
|
||||||
|
FormulaFieldType.compatibleWithFormulaTypes('array(number)'),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +48,7 @@ export class HasNotEmptyValueViewFilterType extends ViewFilterType {
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
|
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
|
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
|
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
|
||||||
|
FormulaFieldType.compatibleWithFormulaTypes('array(number)'),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,22 +67,11 @@ export class HasValueEqualViewFilterType extends ViewFilterType {
|
||||||
return i18n.t('viewFilter.hasValueEqual')
|
return i18n.t('viewFilter.hasValueEqual')
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultValue(field) {
|
|
||||||
// has_value_equal filter by default sends an empty string. For consistency
|
|
||||||
// a default value should be in pair with a default value from the input component.
|
|
||||||
return this.prepareValue('', field)
|
|
||||||
}
|
|
||||||
|
|
||||||
matches(cellValue, filterValue, field, fieldType) {
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
filterValue = fieldType.parseInputValue(field, filterValue)
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
return fieldType.hasValueEqualFilter(cellValue, filterValue, field)
|
return fieldType.hasValueEqualFilter(cellValue, filterValue, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareValue(value, field) {
|
|
||||||
const fieldType = this.app.$registry.get('field', field.type)
|
|
||||||
return normalizeBooleanForFilters(fieldType.parseInputValue(field, value))
|
|
||||||
}
|
|
||||||
|
|
||||||
getInputComponent(field) {
|
getInputComponent(field) {
|
||||||
const fieldType = this.app.$registry.get('field', field.type)
|
const fieldType = this.app.$registry.get('field', field.type)
|
||||||
return fieldType.getFilterInputComponent(field, this) || viewFilterTypeText
|
return fieldType.getFilterInputComponent(field, this) || viewFilterTypeText
|
||||||
|
@ -105,7 +84,8 @@ export class HasValueEqualViewFilterType extends ViewFilterType {
|
||||||
FormulaFieldType.arrayOf('char'),
|
FormulaFieldType.arrayOf('char'),
|
||||||
FormulaFieldType.arrayOf('url'),
|
FormulaFieldType.arrayOf('url'),
|
||||||
FormulaFieldType.arrayOf('boolean'),
|
FormulaFieldType.arrayOf('boolean'),
|
||||||
FormulaFieldType.arrayOf('single_select')
|
FormulaFieldType.arrayOf('single_select'),
|
||||||
|
FormulaFieldType.arrayOf('number')
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -122,21 +102,10 @@ export class HasNotValueEqualViewFilterType extends ViewFilterType {
|
||||||
}
|
}
|
||||||
|
|
||||||
matches(cellValue, filterValue, field, fieldType) {
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
filterValue = fieldType.parseInputValue(field, filterValue)
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
return fieldType.hasNotValueEqualFilter(cellValue, filterValue, field)
|
return fieldType.hasNotValueEqualFilter(cellValue, filterValue, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultValue(field) {
|
|
||||||
// has_not_value_equal filter by default sends an empty string. For consistency
|
|
||||||
// a default value should be in pair with a default value from the input component.
|
|
||||||
return this.prepareValue('', field)
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareValue(value, field) {
|
|
||||||
const fieldType = this.app.$registry.get('field', field.type)
|
|
||||||
return normalizeBooleanForFilters(fieldType.parseInputValue(field, value))
|
|
||||||
}
|
|
||||||
|
|
||||||
getInputComponent(field) {
|
getInputComponent(field) {
|
||||||
const fieldType = this.app.$registry.get('field', field.type)
|
const fieldType = this.app.$registry.get('field', field.type)
|
||||||
return fieldType.getFilterInputComponent(field, this) || viewFilterTypeText
|
return fieldType.getFilterInputComponent(field, this) || viewFilterTypeText
|
||||||
|
@ -149,7 +118,8 @@ export class HasNotValueEqualViewFilterType extends ViewFilterType {
|
||||||
FormulaFieldType.arrayOf('char'),
|
FormulaFieldType.arrayOf('char'),
|
||||||
FormulaFieldType.arrayOf('url'),
|
FormulaFieldType.arrayOf('url'),
|
||||||
FormulaFieldType.arrayOf('boolean'),
|
FormulaFieldType.arrayOf('boolean'),
|
||||||
FormulaFieldType.arrayOf('single_select')
|
FormulaFieldType.arrayOf('single_select'),
|
||||||
|
FormulaFieldType.arrayOf('number')
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -171,10 +141,13 @@ export class HasValueContainsViewFilterType extends ViewFilterType {
|
||||||
|
|
||||||
getCompatibleFieldTypes() {
|
getCompatibleFieldTypes() {
|
||||||
return [
|
return [
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
|
FormulaFieldType.compatibleWithFormulaTypes(
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
|
FormulaFieldType.arrayOf('char'),
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
|
FormulaFieldType.arrayOf('text'),
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
|
FormulaFieldType.arrayOf('url'),
|
||||||
|
FormulaFieldType.arrayOf('single_select'),
|
||||||
|
FormulaFieldType.arrayOf('number')
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,10 +172,13 @@ export class HasNotValueContainsViewFilterType extends ViewFilterType {
|
||||||
|
|
||||||
getCompatibleFieldTypes() {
|
getCompatibleFieldTypes() {
|
||||||
return [
|
return [
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
|
FormulaFieldType.compatibleWithFormulaTypes(
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
|
FormulaFieldType.arrayOf('char'),
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
|
FormulaFieldType.arrayOf('text'),
|
||||||
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
|
FormulaFieldType.arrayOf('url'),
|
||||||
|
FormulaFieldType.arrayOf('single_select'),
|
||||||
|
FormulaFieldType.arrayOf('number')
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,7 +297,7 @@ export class HasAllValuesEqualViewFilterType extends ViewFilterType {
|
||||||
}
|
}
|
||||||
|
|
||||||
matches(cellValue, filterValue, field, fieldType) {
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
filterValue = fieldType.parseInputValue(field, filterValue)
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
return fieldType.hasAllValuesEqualFilter(cellValue, filterValue, field)
|
return fieldType.hasAllValuesEqualFilter(cellValue, filterValue, field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -371,3 +347,243 @@ export class HasNoneSelectOptionEqualViewFilterType extends ViewFilterType {
|
||||||
return fieldType.hasNotValueEqualFilter(cellValue, filterValue, field)
|
return fieldType.hasNotValueEqualFilter(cellValue, filterValue, field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class HasValueHigherThanViewFilterType extends ViewFilterType {
|
||||||
|
static getType() {
|
||||||
|
return 'has_value_higher'
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
const { i18n } = this.app
|
||||||
|
return i18n.t('viewFilter.hasValueHigherThan')
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputComponent(field) {
|
||||||
|
return ViewFilterTypeNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
getCompatibleFieldTypes() {
|
||||||
|
return [
|
||||||
|
FormulaFieldType.compatibleWithFormulaTypes(
|
||||||
|
FormulaFieldType.arrayOf(BaserowFormulaNumberType.getType())
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
|
return (
|
||||||
|
filterValue === '' ||
|
||||||
|
fieldType.hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
ComparisonOperator.HIGHER_THAN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasNotValueHigherThanViewFilterType extends HasValueHigherThanViewFilterType {
|
||||||
|
static getType() {
|
||||||
|
return 'has_not_value_higher'
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
const { i18n } = this.app
|
||||||
|
return i18n.t('viewFilter.hasNotValueHigherThan')
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
|
return (
|
||||||
|
filterValue === '' ||
|
||||||
|
!fieldType.hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
ComparisonOperator.HIGHER_THAN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasValueHigherThanOrEqualViewFilterType extends ViewFilterType {
|
||||||
|
static getType() {
|
||||||
|
return 'has_value_higher_or_equal'
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
const { i18n } = this.app
|
||||||
|
return i18n.t('viewFilter.hasValueHigherThanOrEqual')
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputComponent(field) {
|
||||||
|
return ViewFilterTypeNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
getCompatibleFieldTypes() {
|
||||||
|
return [
|
||||||
|
FormulaFieldType.compatibleWithFormulaTypes(
|
||||||
|
FormulaFieldType.arrayOf(BaserowFormulaNumberType.getType())
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
|
return (
|
||||||
|
filterValue === '' ||
|
||||||
|
fieldType.hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
ComparisonOperator.HIGHER_THAN_OR_EQUAL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasNotValueHigherThanOrEqualViewFilterType extends HasValueHigherThanOrEqualViewFilterType {
|
||||||
|
static getType() {
|
||||||
|
return 'has_not_value_higher_or_equal'
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
const { i18n } = this.app
|
||||||
|
return i18n.t('viewFilter.hasNotValueHigherThanOrEqual')
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
|
return (
|
||||||
|
filterValue === '' ||
|
||||||
|
!fieldType.hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
ComparisonOperator.HIGHER_THAN_OR_EQUAL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasValueLowerThanViewFilterType extends ViewFilterType {
|
||||||
|
static getType() {
|
||||||
|
return 'has_value_lower'
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
const { i18n } = this.app
|
||||||
|
return i18n.t('viewFilter.hasValueLowerThan')
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputComponent(field) {
|
||||||
|
return ViewFilterTypeNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
getCompatibleFieldTypes() {
|
||||||
|
return [
|
||||||
|
FormulaFieldType.compatibleWithFormulaTypes(
|
||||||
|
FormulaFieldType.arrayOf(BaserowFormulaNumberType.getType())
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
|
return (
|
||||||
|
filterValue === '' ||
|
||||||
|
fieldType.hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
ComparisonOperator.LOWER_THAN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasNotValueLowerThanViewFilterType extends HasValueLowerThanViewFilterType {
|
||||||
|
static getType() {
|
||||||
|
return 'has_not_value_lower'
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
const { i18n } = this.app
|
||||||
|
return i18n.t('viewFilter.hasNotValueLowerThan')
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
|
return (
|
||||||
|
filterValue === '' ||
|
||||||
|
!fieldType.hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
ComparisonOperator.LOWER_THAN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasValueLowerThanOrEqualViewFilterType extends ViewFilterType {
|
||||||
|
static getType() {
|
||||||
|
return 'has_value_lower_or_equal'
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
const { i18n } = this.app
|
||||||
|
return i18n.t('viewFilter.hasValueLowerThanOrEqual')
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputComponent(field) {
|
||||||
|
return ViewFilterTypeNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
getCompatibleFieldTypes() {
|
||||||
|
return [
|
||||||
|
FormulaFieldType.compatibleWithFormulaTypes(
|
||||||
|
FormulaFieldType.arrayOf(BaserowFormulaNumberType.getType())
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
|
return (
|
||||||
|
filterValue === '' ||
|
||||||
|
fieldType.hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
ComparisonOperator.LOWER_THAN_OR_EQUAL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasNotValueLowerThanOrEqualViewFilterType extends HasValueLowerThanOrEqualViewFilterType {
|
||||||
|
static getType() {
|
||||||
|
return 'has_not_value_lower_or_equal'
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
const { i18n } = this.app
|
||||||
|
return i18n.t('viewFilter.hasNotValueLowerThanOrEqual')
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(cellValue, filterValue, field, fieldType) {
|
||||||
|
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||||
|
return (
|
||||||
|
filterValue === '' ||
|
||||||
|
!fieldType.hasValueComparableToFilter(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
field,
|
||||||
|
ComparisonOperator.LOWER_THAN_OR_EQUAL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,12 @@ import numberField from '@baserow/modules/database/mixins/numberField'
|
||||||
export default {
|
export default {
|
||||||
name: 'ViewFilterTypeNumber',
|
name: 'ViewFilterTypeNumber',
|
||||||
mixins: [filterTypeInput, numberField],
|
mixins: [filterTypeInput, numberField],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// Avoid rounding decimals to ensure filter values match backend behavior.
|
||||||
|
roundDecimals: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
field: {
|
field: {
|
||||||
handler() {
|
handler() {
|
||||||
|
|
|
@ -15,14 +15,7 @@ import {
|
||||||
isValidEmail,
|
isValidEmail,
|
||||||
isValidURL,
|
isValidURL,
|
||||||
} from '@baserow/modules/core/utils/string'
|
} from '@baserow/modules/core/utils/string'
|
||||||
import {
|
import { formulaFieldArrayFilterMixin } from '@baserow/modules/database/arrayFilterMixins'
|
||||||
hasValueContainsFilterMixin,
|
|
||||||
hasValueEqualFilterMixin,
|
|
||||||
hasValueContainsWordFilterMixin,
|
|
||||||
hasValueLengthIsLowerThanFilterMixin,
|
|
||||||
hasEmptyValueFilterMixin,
|
|
||||||
hasAllValuesEqualFilterMixin,
|
|
||||||
} from '@baserow/modules/database/arrayFilterMixins'
|
|
||||||
import {
|
import {
|
||||||
parseNumberValue,
|
parseNumberValue,
|
||||||
formatNumberValue,
|
formatNumberValue,
|
||||||
|
@ -709,10 +702,25 @@ export class FieldType extends Registerable {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns optionally input component for a field / filter type combination.
|
||||||
|
* This is called by FilterType to get the component. FilterType should provide
|
||||||
|
* a default if FieldType returns null.
|
||||||
|
*
|
||||||
|
* @returns {null}
|
||||||
|
*/
|
||||||
getFilterInputComponent(field, filterType) {
|
getFilterInputComponent(field, filterType) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a valid filter value for the field type. This is used to parse the
|
||||||
|
* filter value from the frontend to the backend.
|
||||||
|
*/
|
||||||
|
prepareFilterValue(field, filterValue) {
|
||||||
|
return filterValue
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is called for each field in the row when another field value in the row has
|
* Is called for each field in the row when another field value in the row has
|
||||||
* changed. Optionally, a different value can be returned here for that field. This
|
* changed. Optionally, a different value can be returned here for that field. This
|
||||||
|
@ -1643,6 +1651,11 @@ export class NumberFieldType extends FieldType {
|
||||||
}
|
}
|
||||||
return new BigNumber(value)
|
return new BigNumber(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareFilterValue(field, value) {
|
||||||
|
const res = parseNumberValue(field, String(value ?? ''), false)
|
||||||
|
return res === null || res.isNaN() ? '' : res.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BigNumber.config({ EXPONENTIAL_AT: NumberFieldType.getMaxNumberLength() })
|
BigNumber.config({ EXPONENTIAL_AT: NumberFieldType.getMaxNumberLength() })
|
||||||
|
@ -1914,6 +1927,10 @@ export class BooleanFieldType extends FieldType {
|
||||||
getHasNotValueEqualFilterFunction(field) {
|
getHasNotValueEqualFilterFunction(field) {
|
||||||
return this.getHasValueEqualFilterFunction(field, true)
|
return this.getHasValueEqualFilterFunction(field, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareFilterValue(field, value) {
|
||||||
|
return this.parseInputValue(field, String(value ?? ''))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BaseDateFieldType extends FieldType {
|
class BaseDateFieldType extends FieldType {
|
||||||
|
@ -3745,12 +3762,7 @@ export class PhoneNumberFieldType extends FieldType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FormulaFieldType extends mix(
|
export class FormulaFieldType extends mix(
|
||||||
hasAllValuesEqualFilterMixin,
|
formulaFieldArrayFilterMixin,
|
||||||
hasEmptyValueFilterMixin,
|
|
||||||
hasValueEqualFilterMixin,
|
|
||||||
hasValueContainsFilterMixin,
|
|
||||||
hasValueContainsWordFilterMixin,
|
|
||||||
hasValueLengthIsLowerThanFilterMixin,
|
|
||||||
FieldType
|
FieldType
|
||||||
) {
|
) {
|
||||||
static getType() {
|
static getType() {
|
||||||
|
@ -3792,7 +3804,11 @@ export class FormulaFieldType extends mix(
|
||||||
return i18n.t('fieldType.formula')
|
return i18n.t('fieldType.formula')
|
||||||
}
|
}
|
||||||
|
|
||||||
getFormulaSubtype(field) {
|
prepareFilterValue(field, value) {
|
||||||
|
return this.getFormulaType(field)?.prepareFilterValue(field, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormulaType(field) {
|
||||||
return this.app.$registry.get('formula_type', field.formula_type)
|
return this.app.$registry.get('formula_type', field.formula_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3813,7 +3829,7 @@ export class FormulaFieldType extends mix(
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilterInputComponent(field, filterType) {
|
getFilterInputComponent(field, filterType) {
|
||||||
return this.getFormulaSubtype(field)?.getFilterInputComponent(
|
return this.getFormulaType(field)?.getFilterInputComponent(
|
||||||
field,
|
field,
|
||||||
filterType
|
filterType
|
||||||
)
|
)
|
||||||
|
@ -3824,15 +3840,15 @@ export class FormulaFieldType extends mix(
|
||||||
}
|
}
|
||||||
|
|
||||||
getCardValueHeight(field) {
|
getCardValueHeight(field) {
|
||||||
return this.getFormulaSubtype(field)?.getCardComponent().height || 0
|
return this.getFormulaType(field)?.getCardComponent().height || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
getCanSortInView(field) {
|
getCanSortInView(field) {
|
||||||
return this.getFormulaSubtype(field)?.getCanSortInView(field)
|
return this.getFormulaType(field)?.getCanSortInView(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
getSort(name, order, field) {
|
getSort(name, order, field) {
|
||||||
return this.getFormulaSubtype(field)?.getSort(name, order, field)
|
return this.getFormulaType(field)?.getSort(name, order, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmptyValue(field) {
|
getEmptyValue(field) {
|
||||||
|
@ -3840,7 +3856,7 @@ export class FormulaFieldType extends mix(
|
||||||
}
|
}
|
||||||
|
|
||||||
getDocsDataType(field) {
|
getDocsDataType(field) {
|
||||||
return this.getFormulaSubtype(field)?.getDocsDataType(field)
|
return this.getFormulaType(field)?.getDocsDataType(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDocsDescription(field) {
|
getDocsDescription(field) {
|
||||||
|
@ -3852,11 +3868,11 @@ export class FormulaFieldType extends mix(
|
||||||
}
|
}
|
||||||
|
|
||||||
getDocsResponseExample(field) {
|
getDocsResponseExample(field) {
|
||||||
return this.getFormulaSubtype(field)?.getDocsResponseExample(field)
|
return this.getFormulaType(field)?.getDocsResponseExample(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareValueForCopy(field, value) {
|
prepareValueForCopy(field, value) {
|
||||||
return this.getFormulaSubtype(field)?.prepareValueForCopy(field, value)
|
return this.getFormulaType(field)?.prepareValueForCopy(field, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
getContainsFilterFunction(field) {
|
getContainsFilterFunction(field) {
|
||||||
|
@ -3876,11 +3892,11 @@ export class FormulaFieldType extends mix(
|
||||||
}
|
}
|
||||||
|
|
||||||
toHumanReadableString(field, value) {
|
toHumanReadableString(field, value) {
|
||||||
return this.getFormulaSubtype(field)?.toHumanReadableString(field, value)
|
return this.getFormulaType(field)?.toHumanReadableString(field, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortIndicator(field) {
|
getSortIndicator(field) {
|
||||||
return this.getFormulaSubtype(field)?.getSortIndicator(field)
|
return this.getFormulaType(field)?.getSortIndicator(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
getFormComponent() {
|
getFormComponent() {
|
||||||
|
@ -3912,57 +3928,25 @@ export class FormulaFieldType extends mix(
|
||||||
}
|
}
|
||||||
|
|
||||||
canRepresentDate(field) {
|
canRepresentDate(field) {
|
||||||
return this.getFormulaSubtype(field)?.canRepresentDate(field)
|
return this.getFormulaType(field)?.canRepresentDate(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCanGroupByInView(field) {
|
getCanGroupByInView(field) {
|
||||||
return this.getFormulaSubtype(field)?.canGroupByInView(field)
|
return this.getFormulaType(field)?.canGroupByInView(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
parseInputValue(field, value) {
|
parseInputValue(field, value) {
|
||||||
const underlyingFieldType = this.getFormulaSubtype(field)
|
const underlyingFieldType = this.getFormulaType(field)
|
||||||
return underlyingFieldType.parseInputValue(field, value)
|
return underlyingFieldType.parseInputValue(field, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
parseFromLinkedRowItemValue(field, value) {
|
parseFromLinkedRowItemValue(field, value) {
|
||||||
const underlyingFieldType = this.getFormulaSubtype(field)
|
const underlyingFieldType = this.getFormulaType(field)
|
||||||
return underlyingFieldType.parseFromLinkedRowItemValue(field, value)
|
return underlyingFieldType.parseFromLinkedRowItemValue(field, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
canRepresentFiles(field) {
|
canRepresentFiles(field) {
|
||||||
return this.getFormulaSubtype(field)?.canRepresentFiles(field)
|
return this.getFormulaType(field)?.canRepresentFiles(field)
|
||||||
}
|
|
||||||
|
|
||||||
getHasAllValuesEqualFilterFunction(field) {
|
|
||||||
return this.getFormulaSubtype(field)?.getHasAllValuesEqualFilterFunction(
|
|
||||||
field
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getHasEmptyValueFilterFunction(field) {
|
|
||||||
return this.getFormulaSubtype(field)?.getHasEmptyValueFilterFunction(field)
|
|
||||||
}
|
|
||||||
|
|
||||||
getHasValueEqualFilterFunction(field) {
|
|
||||||
return this.getFormulaSubtype(field)?.getHasValueEqualFilterFunction(field)
|
|
||||||
}
|
|
||||||
|
|
||||||
getHasValueContainsFilterFunction(field) {
|
|
||||||
return this.getFormulaSubtype(field)?.getHasValueContainsFilterFunction(
|
|
||||||
field
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getHasValueContainsWordFilterFunction(field) {
|
|
||||||
return this.getFormulaSubtype(field)?.getHasValueContainsWordFilterFunction(
|
|
||||||
field
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getHasValueLengthIsLowerThanFilterFunction(field) {
|
|
||||||
return this.getFormulaSubtype(
|
|
||||||
field
|
|
||||||
)?.getHasValueLengthIsLowerThanFilterFunction(field)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,8 @@ import {
|
||||||
hasSelectOptionIdEqualMixin,
|
hasSelectOptionIdEqualMixin,
|
||||||
hasSelectOptionValueContainsFilterMixin,
|
hasSelectOptionValueContainsFilterMixin,
|
||||||
hasSelectOptionValueContainsWordFilterMixin,
|
hasSelectOptionValueContainsWordFilterMixin,
|
||||||
formulaArrayFilterMixin,
|
baserowFormulaArrayTypeFilterMixin,
|
||||||
|
hasNumericValueComparableToFilterMixin,
|
||||||
} from '@baserow/modules/database/arrayFilterMixins'
|
} from '@baserow/modules/database/arrayFilterMixins'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ViewFilterTypeBoolean from '@baserow/modules/database/components/view/ViewFilterTypeBoolean.vue'
|
import ViewFilterTypeBoolean from '@baserow/modules/database/components/view/ViewFilterTypeBoolean.vue'
|
||||||
|
@ -81,18 +82,22 @@ export class BaserowFormulaTypeDefinition extends Registerable {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns optionally input component for a field / filter type combination
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getFilterInputComponent(field, filterType) {
|
getFilterInputComponent(field, filterType) {
|
||||||
return null
|
return this.app.$registry
|
||||||
|
.get('field', this.getFieldType())
|
||||||
|
.getFilterInputComponent(field, filterType)
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowEditArrayFieldComponent() {
|
getRowEditArrayFieldComponent() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareFilterValue(field, value) {
|
||||||
|
return this.app.$registry
|
||||||
|
.get('field', this.getFieldType())
|
||||||
|
.prepareFilterValue(field, value)
|
||||||
|
}
|
||||||
|
|
||||||
getFunctionalGridViewFieldComponent() {
|
getFunctionalGridViewFieldComponent() {
|
||||||
return this.app.$registry
|
return this.app.$registry
|
||||||
.get('field', this.getFieldType())
|
.get('field', this.getFieldType())
|
||||||
|
@ -305,7 +310,13 @@ export class BaserowFormulaCharType extends mix(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BaserowFormulaNumberType extends BaserowFormulaTypeDefinition {
|
export class BaserowFormulaNumberType extends mix(
|
||||||
|
hasEmptyValueFilterMixin,
|
||||||
|
hasValueContainsFilterMixin,
|
||||||
|
hasNumericValueComparableToFilterMixin,
|
||||||
|
|
||||||
|
BaserowFormulaTypeDefinition
|
||||||
|
) {
|
||||||
static getType() {
|
static getType() {
|
||||||
return 'number'
|
return 'number'
|
||||||
}
|
}
|
||||||
|
@ -582,7 +593,7 @@ export class BaserowFormulaInvalidType extends BaserowFormulaTypeDefinition {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BaserowFormulaArrayType extends mix(
|
export class BaserowFormulaArrayType extends mix(
|
||||||
formulaArrayFilterMixin,
|
baserowFormulaArrayTypeFilterMixin,
|
||||||
BaserowFormulaTypeDefinition
|
BaserowFormulaTypeDefinition
|
||||||
) {
|
) {
|
||||||
static getType() {
|
static getType() {
|
||||||
|
@ -605,6 +616,14 @@ export class BaserowFormulaArrayType extends mix(
|
||||||
return RowCardFieldArray
|
return RowCardFieldArray
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareFilterValue(field, value) {
|
||||||
|
return this.getSubType(field)?.prepareFilterValue(field, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubType(field) {
|
||||||
|
return this.app.$registry.get('formula_type', field.array_formula_type)
|
||||||
|
}
|
||||||
|
|
||||||
getRowEditFieldComponent(field) {
|
getRowEditFieldComponent(field) {
|
||||||
const arrayOverride =
|
const arrayOverride =
|
||||||
this.getSubType(field)?.getRowEditArrayFieldComponent()
|
this.getSubType(field)?.getRowEditArrayFieldComponent()
|
||||||
|
|
|
@ -580,7 +580,15 @@
|
||||||
},
|
},
|
||||||
"viewFilter": {
|
"viewFilter": {
|
||||||
"filter": "Filter | 1 Filter | {count} Filters",
|
"filter": "Filter | 1 Filter | {count} Filters",
|
||||||
"hasAllValuesEqual": "has all values equal"
|
"hasAllValuesEqual": "has all values equal",
|
||||||
|
"hasValueHigherThan": "has value higher than",
|
||||||
|
"hasValueHigherThanOrEqual": "has value higher than or equal",
|
||||||
|
"hasValueLowerThan": "has value lower than",
|
||||||
|
"hasValueLowerThanOrEqual": "has value lower than or equal",
|
||||||
|
"hasNotValueHigherThan": "doesn't have value higher than",
|
||||||
|
"hasNotValueHigherThanOrEqual": "doesn't have value higher than or equal",
|
||||||
|
"hasNotValueLowerThan": "doesn't have value lower than",
|
||||||
|
"hasNotValueLowerThanOrEqual": "doesn't have value lower than or equal"
|
||||||
},
|
},
|
||||||
"viewContext": {
|
"viewContext": {
|
||||||
"exportView": "Export view",
|
"exportView": "Export view",
|
||||||
|
|
|
@ -16,6 +16,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
formattedValue: '',
|
formattedValue: '',
|
||||||
focused: false,
|
focused: false,
|
||||||
|
roundDecimals: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -23,7 +24,7 @@ export default {
|
||||||
this.formattedValue = this.formatNumberValue(field, value)
|
this.formattedValue = this.formatNumberValue(field, value)
|
||||||
},
|
},
|
||||||
formatNumberValue(field, value) {
|
formatNumberValue(field, value) {
|
||||||
return formatNumberValue(field, value)
|
return formatNumberValue(field, value, true, this.roundDecimals)
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
* This method is similar to formatNumberValue, but it returns the value as a
|
* This method is similar to formatNumberValue, but it returns the value as a
|
||||||
|
@ -33,10 +34,15 @@ export default {
|
||||||
*/
|
*/
|
||||||
formatNumberValueForEdit(field, value) {
|
formatNumberValueForEdit(field, value) {
|
||||||
const withThousandSeparator = false
|
const withThousandSeparator = false
|
||||||
return formatNumberValue(field, value, withThousandSeparator)
|
return formatNumberValue(
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
withThousandSeparator,
|
||||||
|
this.roundDecimals
|
||||||
|
)
|
||||||
},
|
},
|
||||||
parseNumberValue(field, value) {
|
parseNumberValue(field, value) {
|
||||||
return parseNumberValue(field, value)
|
return parseNumberValue(field, value, this.roundDecimals)
|
||||||
},
|
},
|
||||||
getNumberFormatOptions(field) {
|
getNumberFormatOptions(field) {
|
||||||
return getNumberFormatOptions(field)
|
return getNumberFormatOptions(field)
|
||||||
|
|
|
@ -108,6 +108,14 @@ import {
|
||||||
HasAllValuesEqualViewFilterType,
|
HasAllValuesEqualViewFilterType,
|
||||||
HasAnySelectOptionEqualViewFilterType,
|
HasAnySelectOptionEqualViewFilterType,
|
||||||
HasNoneSelectOptionEqualViewFilterType,
|
HasNoneSelectOptionEqualViewFilterType,
|
||||||
|
HasValueLowerThanViewFilterType,
|
||||||
|
HasValueLowerThanOrEqualViewFilterType,
|
||||||
|
HasValueHigherThanViewFilterType,
|
||||||
|
HasValueHigherThanOrEqualViewFilterType,
|
||||||
|
HasNotValueLowerThanOrEqualViewFilterType,
|
||||||
|
HasNotValueLowerThanViewFilterType,
|
||||||
|
HasNotValueHigherThanOrEqualViewFilterType,
|
||||||
|
HasNotValueHigherThanViewFilterType,
|
||||||
} from '@baserow/modules/database/arrayViewFilters'
|
} from '@baserow/modules/database/arrayViewFilters'
|
||||||
import {
|
import {
|
||||||
CSVImporterType,
|
CSVImporterType,
|
||||||
|
@ -581,6 +589,40 @@ export default (context) => {
|
||||||
app.$registry.register('viewFilter', new NotEmptyViewFilterType(context))
|
app.$registry.register('viewFilter', new NotEmptyViewFilterType(context))
|
||||||
app.$registry.register('viewFilter', new UserIsFilterType(context))
|
app.$registry.register('viewFilter', new UserIsFilterType(context))
|
||||||
app.$registry.register('viewFilter', new UserIsNotFilterType(context))
|
app.$registry.register('viewFilter', new UserIsNotFilterType(context))
|
||||||
|
app.$registry.register(
|
||||||
|
'viewFilter',
|
||||||
|
new HasValueHigherThanViewFilterType(context)
|
||||||
|
)
|
||||||
|
app.$registry.register(
|
||||||
|
'viewFilter',
|
||||||
|
new HasNotValueHigherThanViewFilterType(context)
|
||||||
|
)
|
||||||
|
app.$registry.register(
|
||||||
|
'viewFilter',
|
||||||
|
new HasValueHigherThanOrEqualViewFilterType(context)
|
||||||
|
)
|
||||||
|
app.$registry.register(
|
||||||
|
'viewFilter',
|
||||||
|
new HasNotValueHigherThanOrEqualViewFilterType(context)
|
||||||
|
)
|
||||||
|
|
||||||
|
app.$registry.register(
|
||||||
|
'viewFilter',
|
||||||
|
new HasValueLowerThanViewFilterType(context)
|
||||||
|
)
|
||||||
|
|
||||||
|
app.$registry.register(
|
||||||
|
'viewFilter',
|
||||||
|
new HasNotValueLowerThanViewFilterType(context)
|
||||||
|
)
|
||||||
|
app.$registry.register(
|
||||||
|
'viewFilter',
|
||||||
|
new HasValueLowerThanOrEqualViewFilterType(context)
|
||||||
|
)
|
||||||
|
app.$registry.register(
|
||||||
|
'viewFilter',
|
||||||
|
new HasNotValueLowerThanOrEqualViewFilterType(context)
|
||||||
|
)
|
||||||
|
|
||||||
app.$registry.register(
|
app.$registry.register(
|
||||||
'viewOwnershipType',
|
'viewOwnershipType',
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
// list of file names, we don't want the filterValue to accidentally match the end
|
// list of file names, we don't want the filterValue to accidentally match the end
|
||||||
// of one filename and the start of another.
|
// of one filename and the start of another.
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
export function filenameContainsFilter(
|
export function filenameContainsFilter(
|
||||||
rowValue,
|
rowValue,
|
||||||
|
@ -31,8 +32,8 @@ export function genericContainsFilter(
|
||||||
if (humanReadableRowValue == null) {
|
if (humanReadableRowValue == null) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
humanReadableRowValue = humanReadableRowValue.toString().toLowerCase().trim()
|
humanReadableRowValue = String(humanReadableRowValue).toLowerCase().trim()
|
||||||
filterValue = filterValue.toString().toLowerCase().trim()
|
filterValue = String(filterValue).toLowerCase().trim()
|
||||||
|
|
||||||
return humanReadableRowValue.includes(filterValue)
|
return humanReadableRowValue.includes(filterValue)
|
||||||
}
|
}
|
||||||
|
@ -45,8 +46,8 @@ export function genericContainsWordFilter(
|
||||||
if (humanReadableRowValue == null) {
|
if (humanReadableRowValue == null) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
humanReadableRowValue = humanReadableRowValue.toString().toLowerCase().trim()
|
humanReadableRowValue = String(humanReadableRowValue).toLowerCase().trim()
|
||||||
filterValue = filterValue.toString().toLowerCase().trim()
|
filterValue = String(filterValue).toLowerCase().trim()
|
||||||
// check using regex to match whole words
|
// check using regex to match whole words
|
||||||
// make sure to escape the filterValue as it may contain regex special characters
|
// make sure to escape the filterValue as it may contain regex special characters
|
||||||
filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
|
filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
|
||||||
|
@ -100,10 +101,10 @@ export function genericHasValueContainsFilter(cellValue, filterValue) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
filterValue = filterValue.toString().toLowerCase().trim()
|
filterValue = String(filterValue).toLowerCase().trim()
|
||||||
|
|
||||||
for (let i = 0; i < cellValue.length; i++) {
|
for (let i = 0; i < cellValue.length; i++) {
|
||||||
const value = cellValue[i].value.toString().toLowerCase().trim()
|
const value = String(cellValue[i].value).toLowerCase().trim()
|
||||||
|
|
||||||
if (value.includes(filterValue)) {
|
if (value.includes(filterValue)) {
|
||||||
return true
|
return true
|
||||||
|
@ -118,14 +119,14 @@ export function genericHasValueContainsWordFilter(cellValue, filterValue) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
filterValue = filterValue.toString().toLowerCase().trim()
|
filterValue = String(filterValue).toLowerCase().trim()
|
||||||
filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
|
filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
|
||||||
|
|
||||||
for (let i = 0; i < cellValue.length; i++) {
|
for (let i = 0; i < cellValue.length; i++) {
|
||||||
if (cellValue[i].value == null) {
|
if (cellValue[i].value == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const value = cellValue[i].value.toString().toLowerCase().trim()
|
const value = String(cellValue[i].value).toLowerCase().trim()
|
||||||
if (value.match(new RegExp(`\\b${filterValue}\\b`))) {
|
if (value.match(new RegExp(`\\b${filterValue}\\b`))) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -143,7 +144,7 @@ export function genericHasValueLengthLowerThanFilter(cellValue, filterValue) {
|
||||||
if (cellValue[i].value == null) {
|
if (cellValue[i].value == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const valueLength = cellValue[i].value.toString().length
|
const valueLength = String(cellValue[i].value).length
|
||||||
if (valueLength < filterValue) {
|
if (valueLength < filterValue) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -151,3 +152,53 @@ export function genericHasValueLengthLowerThanFilter(cellValue, filterValue) {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ComparisonOperator = {
|
||||||
|
EQUAL: '=',
|
||||||
|
HIGHER_THAN: '>',
|
||||||
|
HIGHER_THAN_OR_EQUAL: '>=',
|
||||||
|
LOWER_THAN: '<',
|
||||||
|
LOWER_THAN_OR_EQUAL: '<=',
|
||||||
|
}
|
||||||
|
|
||||||
|
function doNumericArrayComparison(cellValue, filterValue, compareFunc) {
|
||||||
|
const filterNr = new BigNumber(filterValue)
|
||||||
|
if (!Array.isArray(cellValue) || filterNr.isNaN()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return _.some(_.map(cellValue, 'value'), (item) =>
|
||||||
|
compareFunc(new BigNumber(item), filterNr)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numericHasValueComparableToFilterFunction(comparisonOp) {
|
||||||
|
return (cellValue, filterValue) => {
|
||||||
|
let compareFunc
|
||||||
|
switch (comparisonOp) {
|
||||||
|
case ComparisonOperator.EQUAL:
|
||||||
|
compareFunc = (a, b) => a.isEqualTo(b)
|
||||||
|
break
|
||||||
|
case ComparisonOperator.HIGHER_THAN:
|
||||||
|
compareFunc = (a, b) => a.isGreaterThan(b)
|
||||||
|
break
|
||||||
|
case ComparisonOperator.HIGHER_THAN_OR_EQUAL:
|
||||||
|
compareFunc = (a, b) => a.isGreaterThanOrEqualTo(b)
|
||||||
|
break
|
||||||
|
case ComparisonOperator.LOWER_THAN:
|
||||||
|
compareFunc = (a, b) => a.isLessThan(b)
|
||||||
|
break
|
||||||
|
case ComparisonOperator.LOWER_THAN_OR_EQUAL:
|
||||||
|
compareFunc = (a, b) => a.isLessThanOrEqualTo(b)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (compareFunc === undefined) {
|
||||||
|
throw new Error('Invalid comparison operator')
|
||||||
|
}
|
||||||
|
|
||||||
|
return doNumericArrayComparison(
|
||||||
|
cellValue,
|
||||||
|
filterValue,
|
||||||
|
(arrayItemNr, filterNr) => compareFunc(arrayItemNr, filterNr)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ const DECIMAL_SEPARATORS = {
|
||||||
PERIOD: '.',
|
PERIOD: '.',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NUMBER_MAX_DECIMAL_PLACES = 10
|
||||||
|
|
||||||
const DEFAULT_THOUSAND_SEPARATOR = THOUSAND_SEPARATORS.NONE
|
const DEFAULT_THOUSAND_SEPARATOR = THOUSAND_SEPARATORS.NONE
|
||||||
const DEFAULT_DECIMAL_SEPARATOR = DECIMAL_SEPARATORS.PERIOD
|
const DEFAULT_DECIMAL_SEPARATOR = DECIMAL_SEPARATORS.PERIOD
|
||||||
|
|
||||||
|
@ -57,7 +59,7 @@ export const getNumberFormatOptions = (field) => {
|
||||||
|
|
||||||
const numberPrefix = field.number_prefix ?? ''
|
const numberPrefix = field.number_prefix ?? ''
|
||||||
const numberSuffix = field.number_suffix ?? ''
|
const numberSuffix = field.number_suffix ?? ''
|
||||||
const decimalPlaces = field.number_decimal_places ?? 0
|
const decimalPlaces = field.number_decimal_places ?? undefined
|
||||||
const allowNegative = field.number_negative ?? false
|
const allowNegative = field.number_negative ?? false
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -79,7 +81,8 @@ export const getNumberFormatOptions = (field) => {
|
||||||
export const formatNumberValue = (
|
export const formatNumberValue = (
|
||||||
field,
|
field,
|
||||||
value,
|
value,
|
||||||
withThousandSeparator = true
|
withThousandSeparator = true,
|
||||||
|
roundDecimals = true
|
||||||
) => {
|
) => {
|
||||||
if (value === null || value === undefined || value === '') {
|
if (value === null || value === undefined || value === '') {
|
||||||
return ''
|
return ''
|
||||||
|
@ -95,7 +98,9 @@ export const formatNumberValue = (
|
||||||
|
|
||||||
// Parse the input value if it's a string
|
// Parse the input value if it's a string
|
||||||
let numericValue =
|
let numericValue =
|
||||||
typeof value === 'string' ? parseNumberValue(field, value) : value
|
typeof value === 'string'
|
||||||
|
? parseNumberValue(field, value, roundDecimals)
|
||||||
|
: value
|
||||||
|
|
||||||
if (numericValue === null) {
|
if (numericValue === null) {
|
||||||
return null
|
return null
|
||||||
|
@ -116,9 +121,13 @@ export const formatNumberValue = (
|
||||||
locale = 'en-US'
|
locale = 'en-US'
|
||||||
localeThousandsSeparator = DECIMAL_SEPARATORS.COMMA
|
localeThousandsSeparator = DECIMAL_SEPARATORS.COMMA
|
||||||
}
|
}
|
||||||
|
// Format the number, but keep all decimal places if roundDecimals is false.
|
||||||
|
// For example, filter values are not rounded since the backend doesn't round them.
|
||||||
const formatter = new Intl.NumberFormat(locale, {
|
const formatter = new Intl.NumberFormat(locale, {
|
||||||
minimumFractionDigits: decimalPlaces,
|
minimumFractionDigits: decimalPlaces,
|
||||||
maximumFractionDigits: decimalPlaces,
|
maximumFractionDigits: roundDecimals
|
||||||
|
? decimalPlaces
|
||||||
|
: NUMBER_MAX_DECIMAL_PLACES,
|
||||||
useGrouping: true,
|
useGrouping: true,
|
||||||
})
|
})
|
||||||
let formatted = formatter.format(numericValue)
|
let formatted = formatter.format(numericValue)
|
||||||
|
@ -139,18 +148,22 @@ export const formatNumberValue = (
|
||||||
return `${sign}${numberPrefix}${formatted}${numberSuffix}`.trim()
|
return `${sign}${numberPrefix}${formatted}${numberSuffix}`.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseNumberValue = (field, value) => {
|
export const parseNumberValue = (field, value, roundDecimals = true) => {
|
||||||
const { numberPrefix, numberSuffix, decimalSeparator } =
|
const { numberPrefix, numberSuffix, decimalSeparator } =
|
||||||
getNumberFormatOptions(field)
|
getNumberFormatOptions(field)
|
||||||
|
|
||||||
if (value === null || value === undefined || value === '') {
|
if (value == null || value === '') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const toBigNumber = (val) => {
|
const toBigNumber = (val) => {
|
||||||
return new BigNumber(
|
let rounded = val
|
||||||
new BigNumber(val).toFixed(field.number_decimal_places)
|
if (roundDecimals) {
|
||||||
)
|
rounded = new BigNumber(val).decimalPlaces(
|
||||||
|
field.number_decimal_places ?? 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return new BigNumber(rounded)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'number' || BigNumber.isBigNumber(value)) {
|
if (typeof value === 'number' || BigNumber.isBigNumber(value)) {
|
||||||
|
@ -192,5 +205,5 @@ export const parseNumberValue = (field, value) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedNumber = toBigNumber(result)
|
const parsedNumber = toBigNumber(result)
|
||||||
return isNaN(parsedNumber) ? null : isNegative ? -parsedNumber : parsedNumber
|
return parsedNumber.isNaN() ? null : isNegative ? -parsedNumber : parsedNumber
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,15 @@ import {
|
||||||
HasNotEmptyValueViewFilterType,
|
HasNotEmptyValueViewFilterType,
|
||||||
HasValueLengthIsLowerThanViewFilterType,
|
HasValueLengthIsLowerThanViewFilterType,
|
||||||
HasAllValuesEqualViewFilterType,
|
HasAllValuesEqualViewFilterType,
|
||||||
|
HasValueHigherThanViewFilterType,
|
||||||
|
HasValueHigherThanOrEqualViewFilterType,
|
||||||
|
HasNotValueHigherThanViewFilterType,
|
||||||
|
HasNotValueHigherThanOrEqualViewFilterType,
|
||||||
} from '@baserow/modules/database/arrayViewFilters'
|
} from '@baserow/modules/database/arrayViewFilters'
|
||||||
import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
|
import {
|
||||||
|
FormulaFieldType,
|
||||||
|
LookupFieldType,
|
||||||
|
} from '@baserow/modules/database/fieldTypes'
|
||||||
import {
|
import {
|
||||||
EmptyViewFilterType,
|
EmptyViewFilterType,
|
||||||
NotEmptyViewFilterType,
|
NotEmptyViewFilterType,
|
||||||
|
@ -998,3 +1005,344 @@ describe('Boolean-based array view filters', () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Number-based array view filters', () => {
|
||||||
|
let testApp = null
|
||||||
|
let fieldType = null
|
||||||
|
|
||||||
|
const fieldDefinition = {
|
||||||
|
type: 'lookup',
|
||||||
|
formula_type: 'array',
|
||||||
|
array_formula_type: 'number',
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
testApp = new TestApp()
|
||||||
|
fieldType = new LookupFieldType({
|
||||||
|
app: testApp._app,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testApp.afterEach()
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasEmptyValueTestCases = [
|
||||||
|
{
|
||||||
|
cellValue: [],
|
||||||
|
filterValue: '',
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: null }],
|
||||||
|
filterValue: '',
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: '123.0' }, { value: null }],
|
||||||
|
filterValue: '',
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: '123.0' }, { value: '' }],
|
||||||
|
filterValue: '',
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const hasValueContainsTestCases = [
|
||||||
|
{
|
||||||
|
cellValue: [],
|
||||||
|
filterValue: '',
|
||||||
|
expected: { has: true, hasNot: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [],
|
||||||
|
filterValue: null,
|
||||||
|
expected: { has: false, hasNot: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: null }],
|
||||||
|
filterValue: '',
|
||||||
|
expected: { has: true, hasNot: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: '' }],
|
||||||
|
filterValue: '',
|
||||||
|
expected: { has: true, hasNot: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: null }],
|
||||||
|
filterValue: null,
|
||||||
|
expected: { has: true, hasNot: false },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
cellValue: [{ value: 123.0 }, { value: null }],
|
||||||
|
filterValue: '123',
|
||||||
|
expected: { has: true, hasNot: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: '123.0' }, { value: null }],
|
||||||
|
filterValue: '123',
|
||||||
|
expected: { has: true, hasNot: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: 123.0 }, { value: '' }],
|
||||||
|
filterValue: '100',
|
||||||
|
expected: { has: false, hasNot: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: 1.11101 }],
|
||||||
|
filterValue: '10',
|
||||||
|
expected: { has: true, hasNot: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: '1.11101' }],
|
||||||
|
filterValue: '10',
|
||||||
|
expected: { has: true, hasNot: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: 10100.001 }],
|
||||||
|
filterValue: '20',
|
||||||
|
expected: { has: false, hasNot: true },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// has value higher/lower are implemented with a code path that will return
|
||||||
|
// true for empty filterValues. This requires per filter type result check
|
||||||
|
const hasValueHigherThanOrEqualTestCases = [
|
||||||
|
{
|
||||||
|
cellValue: [],
|
||||||
|
filterValue: '',
|
||||||
|
expected: {
|
||||||
|
higher: true,
|
||||||
|
higherEqual: true,
|
||||||
|
notHigher: true,
|
||||||
|
notHigherEqual: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [],
|
||||||
|
filterValue: null,
|
||||||
|
expected: {
|
||||||
|
higher: true,
|
||||||
|
higherEqual: true,
|
||||||
|
notHigher: true,
|
||||||
|
notHigherEqual: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: null }],
|
||||||
|
filterValue: '',
|
||||||
|
expected: {
|
||||||
|
higher: true,
|
||||||
|
higherEqual: true,
|
||||||
|
notHigher: true,
|
||||||
|
notHigherEqual: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: '' }],
|
||||||
|
filterValue: '',
|
||||||
|
expected: {
|
||||||
|
higher: true,
|
||||||
|
higherEqual: true,
|
||||||
|
notHigher: true,
|
||||||
|
notHigherEqual: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: null }],
|
||||||
|
filterValue: 10,
|
||||||
|
expected: {
|
||||||
|
higher: false,
|
||||||
|
higherEqual: false,
|
||||||
|
notHigher: true,
|
||||||
|
notHigherEqual: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: null }],
|
||||||
|
filterValue: 10,
|
||||||
|
expected: {
|
||||||
|
higher: false,
|
||||||
|
higherEqual: false,
|
||||||
|
notHigher: true,
|
||||||
|
notHigherEqual: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: 123.0 }, { value: null }],
|
||||||
|
filterValue: '123',
|
||||||
|
expected: {
|
||||||
|
higher: false,
|
||||||
|
higherEqual: true,
|
||||||
|
notHigher: true,
|
||||||
|
notHigherEqual: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
cellValue: [{ value: '123.0' }, { value: null }],
|
||||||
|
filterValue: '123',
|
||||||
|
expected: {
|
||||||
|
higher: false,
|
||||||
|
higherEqual: true,
|
||||||
|
notHigher: true,
|
||||||
|
notHigherEqual: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
cellValue: [{ value: 123.0 }],
|
||||||
|
filterValue: '123.0001',
|
||||||
|
expected: {
|
||||||
|
higher: false,
|
||||||
|
higherEqual: false,
|
||||||
|
notHigher: true,
|
||||||
|
notHigherEqual: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: '123.0' }],
|
||||||
|
filterValue: '123.0001',
|
||||||
|
expected: {
|
||||||
|
higher: false,
|
||||||
|
higherEqual: false,
|
||||||
|
notHigher: true,
|
||||||
|
notHigherEqual: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellValue: [{ value: 123.0 }, { value: 500 }],
|
||||||
|
filterValue: '123.0001',
|
||||||
|
expected: {
|
||||||
|
higher: true,
|
||||||
|
higherEqual: true,
|
||||||
|
notHigher: false,
|
||||||
|
notHigherEqual: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
test.each(hasEmptyValueTestCases)(
|
||||||
|
'hasEmptyValueTestCases %j',
|
||||||
|
(testValues) => {
|
||||||
|
const result = new HasEmptyValueViewFilterType({
|
||||||
|
app: testApp._app,
|
||||||
|
}).matches(
|
||||||
|
testValues.cellValue,
|
||||||
|
testValues.filterValue,
|
||||||
|
fieldDefinition,
|
||||||
|
fieldType
|
||||||
|
)
|
||||||
|
expect(result).toBe(testValues.expected)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test.each(hasEmptyValueTestCases)(
|
||||||
|
'hasNotEmptyValueTestCases %j',
|
||||||
|
(testValues) => {
|
||||||
|
const result = new HasNotEmptyValueViewFilterType({
|
||||||
|
app: testApp._app,
|
||||||
|
}).matches(
|
||||||
|
testValues.cellValue,
|
||||||
|
testValues.filterValue,
|
||||||
|
fieldDefinition,
|
||||||
|
fieldType
|
||||||
|
)
|
||||||
|
expect(result).toBe(!testValues.expected)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test.each(hasValueContainsTestCases)(
|
||||||
|
'hasValueContainsTestCases %j',
|
||||||
|
(testValues) => {
|
||||||
|
const result = new HasValueContainsViewFilterType({
|
||||||
|
app: testApp._app,
|
||||||
|
}).matches(
|
||||||
|
testValues.cellValue,
|
||||||
|
testValues.filterValue,
|
||||||
|
fieldDefinition,
|
||||||
|
fieldType
|
||||||
|
)
|
||||||
|
expect(result).toBe(testValues.expected.has)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test.each(hasValueContainsTestCases)(
|
||||||
|
'hasNotValueContainsTestCases %j',
|
||||||
|
(testValues) => {
|
||||||
|
const result = new HasNotValueContainsViewFilterType({
|
||||||
|
app: testApp._app,
|
||||||
|
}).matches(
|
||||||
|
testValues.cellValue,
|
||||||
|
testValues.filterValue,
|
||||||
|
fieldDefinition,
|
||||||
|
fieldType
|
||||||
|
)
|
||||||
|
expect(result).toBe(testValues.expected.hasNot)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test.each(hasValueHigherThanOrEqualTestCases)(
|
||||||
|
'hasValueHigherTestCases %j',
|
||||||
|
(testValues) => {
|
||||||
|
const result = new HasValueHigherThanViewFilterType({
|
||||||
|
app: testApp._app,
|
||||||
|
}).matches(
|
||||||
|
testValues.cellValue,
|
||||||
|
testValues.filterValue,
|
||||||
|
fieldDefinition,
|
||||||
|
fieldType
|
||||||
|
)
|
||||||
|
expect(result).toBe(testValues.expected.higher)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test.each(hasValueHigherThanOrEqualTestCases)(
|
||||||
|
'hasValueHigherOrEqualTestCases %j',
|
||||||
|
(testValues) => {
|
||||||
|
const result = new HasValueHigherThanOrEqualViewFilterType({
|
||||||
|
app: testApp._app,
|
||||||
|
}).matches(
|
||||||
|
testValues.cellValue,
|
||||||
|
testValues.filterValue,
|
||||||
|
fieldDefinition,
|
||||||
|
fieldType
|
||||||
|
)
|
||||||
|
expect(result).toBe(testValues.expected.higherEqual)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test.each(hasValueHigherThanOrEqualTestCases)(
|
||||||
|
'hasNotValueHigherTestCases %j',
|
||||||
|
(testValues) => {
|
||||||
|
const result = new HasNotValueHigherThanViewFilterType({
|
||||||
|
app: testApp._app,
|
||||||
|
}).matches(
|
||||||
|
testValues.cellValue,
|
||||||
|
testValues.filterValue,
|
||||||
|
fieldDefinition,
|
||||||
|
fieldType
|
||||||
|
)
|
||||||
|
expect(result).toBe(testValues.expected.notHigher)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test.each(hasValueHigherThanOrEqualTestCases)(
|
||||||
|
'hasNotValueHigherOrEqualTestCases %j',
|
||||||
|
(testValues) => {
|
||||||
|
const result = new HasNotValueHigherThanOrEqualViewFilterType({
|
||||||
|
app: testApp._app,
|
||||||
|
}).matches(
|
||||||
|
testValues.cellValue,
|
||||||
|
testValues.filterValue,
|
||||||
|
fieldDefinition,
|
||||||
|
fieldType
|
||||||
|
)
|
||||||
|
expect(result).toBe(testValues.expected.notHigherEqual)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue