mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-06 22:08:52 +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,
|
||||
HasNotValueContainsWordViewFilterType,
|
||||
HasNotValueEqualViewFilterType,
|
||||
HasNotValueHigherOrEqualTHanFilterType,
|
||||
HasNotValueHigherThanFilterType,
|
||||
HasNotValueLowerOrEqualTHanFilterType,
|
||||
HasNotValueLowerThanFilterType,
|
||||
HasValueContainsViewFilterType,
|
||||
HasValueContainsWordViewFilterType,
|
||||
HasValueEqualViewFilterType,
|
||||
HasValueHigherOrEqualThanFilter,
|
||||
HasValueLengthIsLowerThanViewFilterType,
|
||||
HasValueLowerOrEqualThanFilter,
|
||||
HasValueLowerThanFilter,
|
||||
hasValueComparableToFilter,
|
||||
)
|
||||
|
||||
view_filter_type_registry.register(HasValueEqualViewFilterType())
|
||||
|
@ -471,6 +479,14 @@ class DatabaseConfig(AppConfig):
|
|||
view_filter_type_registry.register(HasNotEmptyValueViewFilterType())
|
||||
view_filter_type_registry.register(HasAnySelectOptionEqualViewFilterType())
|
||||
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 (
|
||||
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.export_serialized import DatabaseExportSerializedStructure
|
||||
from baserow.contrib.database.fields.filter_support.formula import (
|
||||
FormulaArrayFilterSupport,
|
||||
FormulaFieldTypeArrayFilterSupport,
|
||||
)
|
||||
from baserow.contrib.database.formula import (
|
||||
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 .constants import (
|
||||
BASEROW_BOOLEAN_FIELD_FALSE_VALUES,
|
||||
BASEROW_BOOLEAN_FIELD_TRUE_VALUES,
|
||||
UPSERT_OPTION_DICT_KEY,
|
||||
DeleteFieldStrategyEnum,
|
||||
|
@ -783,6 +784,21 @@ class NumberFieldType(FieldType):
|
|||
"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):
|
||||
type = "rating"
|
||||
|
@ -959,6 +975,14 @@ class BooleanFieldType(FieldType):
|
|||
) -> 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):
|
||||
type = "date"
|
||||
|
@ -4674,7 +4698,7 @@ class PhoneNumberFieldType(CollationSortMixin, CharFieldMatchingRegexFieldType):
|
|||
return collate_expression(Value(value))
|
||||
|
||||
|
||||
class FormulaFieldType(FormulaArrayFilterSupport, ReadOnlyFieldType):
|
||||
class FormulaFieldType(FormulaFieldTypeArrayFilterSupport, ReadOnlyFieldType):
|
||||
type = "formula"
|
||||
model_class = FormulaField
|
||||
_db_column_fields = []
|
||||
|
@ -5238,6 +5262,13 @@ class FormulaFieldType(FormulaArrayFilterSupport, ReadOnlyFieldType):
|
|||
|
||||
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):
|
||||
type = "count"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import re
|
||||
import typing
|
||||
from typing import TYPE_CHECKING, Any, Dict, Type
|
||||
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
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 (
|
||||
BaserowFilterExpression,
|
||||
ComparisonOperator,
|
||||
JSONArrayAllAreExpr,
|
||||
JSONArrayCompareNumericValueExpr,
|
||||
JSONArrayContainsValueExpr,
|
||||
JSONArrayContainsValueLengthLowerThanExpr,
|
||||
JSONArrayContainsValueSimilarToExpr,
|
||||
)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from baserow.contrib.database.fields.models import Field
|
||||
|
||||
|
||||
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(
|
||||
self, field_name: str, model_field: models.Field, field: "Field"
|
||||
) -> OptionallyAnnotatedQ:
|
||||
|
@ -36,10 +51,13 @@ class HasValueEmptyFilterSupport:
|
|||
: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(
|
||||
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
||||
) -> OptionallyAnnotatedQ:
|
||||
|
@ -53,9 +71,6 @@ class HasValueFilterSupport:
|
|||
:return: A Q or AnnotatedQ filter given value.
|
||||
"""
|
||||
|
||||
if not value:
|
||||
return Q()
|
||||
|
||||
return Q(**{f"{field_name}__contains": Value([{"value": value}], JSONField())})
|
||||
|
||||
|
||||
|
@ -74,8 +89,6 @@ class HasValueContainsFilterSupport:
|
|||
:return: A Q or AnnotatedQ filter given value.
|
||||
"""
|
||||
|
||||
if not value:
|
||||
return Q()
|
||||
annotation_query = JSONArrayContainsValueExpr(
|
||||
F(field_name), Value(f"%{value}%"), output_field=BooleanField()
|
||||
)
|
||||
|
@ -103,9 +116,6 @@ class HasValueContainsWordFilterSupport:
|
|||
:return: A Q or AnnotatedQ filter given value.
|
||||
"""
|
||||
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return Q()
|
||||
value = re.escape(value.upper())
|
||||
annotation_query = JSONArrayContainsValueSimilarToExpr(
|
||||
F(field_name), Value(f"%\\m{value}\\M%"), output_field=BooleanField()
|
||||
|
@ -134,9 +144,6 @@ class HasValueLengthIsLowerThanFilterSupport:
|
|||
:return: A Q or AnnotatedQ filter given value.
|
||||
"""
|
||||
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return Q()
|
||||
try:
|
||||
converted_value = int(value)
|
||||
except (TypeError, ValueError):
|
||||
|
@ -182,29 +189,49 @@ class HasAllValuesEqualFilterSupport:
|
|||
return self.default_filter_on_exception()
|
||||
|
||||
|
||||
def get_array_json_filter_expression(
|
||||
json_expression: typing.Type[BaserowFilterExpression], field_name: str, value: str
|
||||
) -> OptionallyAnnotatedQ:
|
||||
"""
|
||||
helper to generate annotated query to get filtered json-based array.
|
||||
`json_expression` should be a filter expression class.
|
||||
class HasNumericValueComparableToFilterSupport:
|
||||
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:
|
||||
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
|
||||
:param value: filter value
|
||||
:param model_field:
|
||||
:param field:
|
||||
:return:
|
||||
|
||||
def get_array_json_filter_expression(
|
||||
json_expression: Type[BaserowFilterExpression],
|
||||
field_name: str,
|
||||
value: str,
|
||||
**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(
|
||||
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)
|
||||
annotation_name = f"{field_name}_{expr_name}_{hashed_value}"
|
||||
return AnnotatedQ(
|
||||
annotation={
|
||||
f"{field_name}_array_expr_{lookup_name}_{hashed_value}": annotation_query
|
||||
},
|
||||
q={f"{field_name}_array_expr_{lookup_name}_{hashed_value}": True},
|
||||
annotation={annotation_name: annotation_query},
|
||||
q={annotation_name: True},
|
||||
)
|
||||
|
|
|
@ -4,28 +4,39 @@ from django.db import models
|
|||
from django.db.models import Q
|
||||
|
||||
from baserow.contrib.database.fields.field_filters import OptionallyAnnotatedQ
|
||||
from baserow.contrib.database.formula.expression_generator.django_expressions import (
|
||||
ComparisonOperator,
|
||||
)
|
||||
|
||||
from .base import (
|
||||
HasAllValuesEqualFilterSupport,
|
||||
HasNumericValueComparableToFilterSupport,
|
||||
HasValueContainsFilterSupport,
|
||||
HasValueContainsWordFilterSupport,
|
||||
HasValueEmptyFilterSupport,
|
||||
HasValueFilterSupport,
|
||||
HasValueEqualFilterSupport,
|
||||
HasValueLengthIsLowerThanFilterSupport,
|
||||
)
|
||||
|
||||
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,
|
||||
HasValueFilterSupport,
|
||||
HasValueEqualFilterSupport,
|
||||
HasValueEmptyFilterSupport,
|
||||
HasValueContainsFilterSupport,
|
||||
HasValueContainsWordFilterSupport,
|
||||
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(
|
||||
self,
|
||||
field_name: str,
|
||||
|
@ -42,6 +53,14 @@ class FormulaArrayFilterSupport(
|
|||
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"):
|
||||
(
|
||||
field_instance,
|
||||
|
@ -99,3 +118,20 @@ class FormulaArrayFilterSupport(
|
|||
return field_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,
|
||||
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 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.models import BooleanField, F, Q, Value
|
||||
|
||||
|
@ -19,7 +18,7 @@ from .base import (
|
|||
HasValueContainsFilterSupport,
|
||||
HasValueContainsWordFilterSupport,
|
||||
HasValueEmptyFilterSupport,
|
||||
HasValueFilterSupport,
|
||||
HasValueEqualFilterSupport,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -28,12 +27,12 @@ if TYPE_CHECKING:
|
|||
|
||||
class SingleSelectFormulaTypeFilterSupport(
|
||||
HasValueEmptyFilterSupport,
|
||||
HasValueFilterSupport,
|
||||
HasValueEqualFilterSupport,
|
||||
HasValueContainsFilterSupport,
|
||||
HasValueContainsWordFilterSupport,
|
||||
):
|
||||
def get_in_array_empty_query(self, field_name, model_field, field: "Field"):
|
||||
return Q(**{f"{field_name}__contains": Value([{"value": None}], JSONField())})
|
||||
def get_in_array_empty_value(self, field: "Field") -> Any:
|
||||
return None
|
||||
|
||||
def get_in_array_is_query(
|
||||
self,
|
||||
|
|
|
@ -1825,6 +1825,30 @@ class FieldType(
|
|||
|
||||
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):
|
||||
read_only = True
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import typing
|
||||
from enum import Enum
|
||||
|
||||
from django.contrib.postgres.aggregates.mixins import OrderableAggMixin
|
||||
from django.db import NotSupportedError
|
||||
|
@ -148,15 +149,33 @@ class BaserowFilterExpression(Expression):
|
|||
|
||||
return c
|
||||
|
||||
def as_sql(self, compiler, connection, template=None):
|
||||
sql_value, params_value = compiler.compile(self.value)
|
||||
|
||||
template = template or self.template
|
||||
data = {
|
||||
def get_template_data(self, sql_value) -> dict:
|
||||
return {
|
||||
"field_name": f'"{self.field_name.field.column}"',
|
||||
"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):
|
||||
|
@ -164,7 +183,7 @@ class FileNameContainsExpr(BaserowFilterExpression):
|
|||
template = (
|
||||
f"""
|
||||
EXISTS(
|
||||
SELECT attached_files ->> 'visible_name'
|
||||
SELECT 1
|
||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as attached_files
|
||||
WHERE UPPER(attached_files ->> 'visible_name') LIKE UPPER(%(value)s)
|
||||
)
|
||||
|
@ -178,7 +197,7 @@ class JSONArrayContainsValueExpr(BaserowFilterExpression):
|
|||
template = (
|
||||
f"""
|
||||
EXISTS(
|
||||
SELECT filtered_field ->> 'value'
|
||||
SELECT 1
|
||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||
WHERE UPPER(filtered_field ->> 'value') LIKE UPPER(%(value)s::text)
|
||||
)
|
||||
|
@ -192,7 +211,7 @@ class JSONArrayContainsValueSimilarToExpr(BaserowFilterExpression):
|
|||
template = (
|
||||
f"""
|
||||
EXISTS(
|
||||
SELECT filtered_field ->> 'value'
|
||||
SELECT 1
|
||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||
WHERE UPPER(filtered_field ->> 'value') SIMILAR TO %(value)s
|
||||
)
|
||||
|
@ -206,7 +225,7 @@ class JSONArrayContainsValueLengthLowerThanExpr(BaserowFilterExpression):
|
|||
template = (
|
||||
f"""
|
||||
EXISTS(
|
||||
SELECT filtered_field ->> 'value'
|
||||
SELECT 1
|
||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||
WHERE LENGTH(filtered_field ->> 'value') < %(value)s
|
||||
)
|
||||
|
@ -219,7 +238,7 @@ class JSONArrayAllAreExpr(BaserowFilterExpression):
|
|||
# fmt: off
|
||||
template = (
|
||||
f"""
|
||||
upper(%(value)s::text) =ALL(
|
||||
upper(%(value)s::text) = ALL(
|
||||
SELECT upper(filtered_field ->> 'value')
|
||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||
) AND JSONB_ARRAY_LENGTH(%(field_name)s) > 0
|
||||
|
@ -233,7 +252,7 @@ class JSONArrayEqualSelectOptionIdExpr(BaserowFilterExpression):
|
|||
template = (
|
||||
f"""
|
||||
EXISTS(
|
||||
SELECT filtered_field -> 'value' ->> 'id'
|
||||
SELECT 1
|
||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||
WHERE (filtered_field -> 'value' ->> 'id') LIKE (%(value)s)
|
||||
)
|
||||
|
@ -247,7 +266,7 @@ class JSONArrayContainsSelectOptionValueExpr(BaserowFilterExpression):
|
|||
template = (
|
||||
f"""
|
||||
EXISTS(
|
||||
SELECT filtered_field -> 'value' ->> 'value'
|
||||
SELECT 1
|
||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||
WHERE UPPER(filtered_field -> 'value' ->> 'value') LIKE UPPER(%(value)s)
|
||||
)
|
||||
|
@ -261,10 +280,62 @@ class JSONArrayContainsSelectOptionValueSimilarToExpr(BaserowFilterExpression):
|
|||
template = (
|
||||
r"""
|
||||
EXISTS(
|
||||
SELECT filtered_field -> 'value' ->> 'value'
|
||||
SELECT 1
|
||||
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
|
||||
WHERE filtered_field -> 'value' ->> 'value' ~* ('\y' || %(value)s || '\y')
|
||||
)
|
||||
""" # nosec B608 %(value)s
|
||||
)
|
||||
# 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
|
||||
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):
|
||||
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.fields import ArrayField, JSONField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import (
|
||||
Expression,
|
||||
|
@ -21,7 +20,6 @@ from django.db.models import (
|
|||
from django.db.models.functions import Cast, Concat
|
||||
|
||||
from dateutil import parser
|
||||
from loguru import logger
|
||||
from rest_framework import serializers
|
||||
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.filter_support.base import (
|
||||
HasAllValuesEqualFilterSupport,
|
||||
HasNumericValueComparableToFilterSupport,
|
||||
HasValueContainsFilterSupport,
|
||||
HasValueContainsWordFilterSupport,
|
||||
HasValueEmptyFilterSupport,
|
||||
HasValueFilterSupport,
|
||||
HasValueEqualFilterSupport,
|
||||
HasValueLengthIsLowerThanFilterSupport,
|
||||
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 (
|
||||
SingleSelectFormulaTypeFilterSupport,
|
||||
)
|
||||
|
@ -65,10 +61,15 @@ from baserow.contrib.database.formula.ast.tree import (
|
|||
BaserowStringLiteral,
|
||||
)
|
||||
from baserow.contrib.database.formula.expression_generator.django_expressions import (
|
||||
ComparisonOperator,
|
||||
JSONArrayCompareNumericValueExpr,
|
||||
JSONArrayContainsValueExpr,
|
||||
)
|
||||
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.filter_support import (
|
||||
BaserowFormulaArrayFilterSupportMixin,
|
||||
)
|
||||
from baserow.contrib.database.formula.types.formula_type import (
|
||||
BaserowFormulaInvalidType,
|
||||
BaserowFormulaType,
|
||||
|
@ -81,7 +82,14 @@ from baserow.core.utils import list_to_comma_separated_string
|
|||
|
||||
|
||||
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):
|
||||
|
@ -130,7 +138,7 @@ class BaserowFormulaBaseTextType(BaserowFormulaTypeHasEmptyBaserowExpression):
|
|||
|
||||
class BaserowFormulaTextType(
|
||||
HasValueEmptyFilterSupport,
|
||||
HasValueFilterSupport,
|
||||
HasValueEqualFilterSupport,
|
||||
HasValueContainsFilterSupport,
|
||||
HasValueContainsWordFilterSupport,
|
||||
HasValueLengthIsLowerThanFilterSupport,
|
||||
|
@ -328,7 +336,12 @@ class BaserowFormulaButtonType(BaserowFormulaLinkType):
|
|||
|
||||
|
||||
class BaserowFormulaNumberType(
|
||||
BaserowFormulaTypeHasEmptyBaserowExpression, BaserowFormulaValidType
|
||||
HasValueEmptyFilterSupport,
|
||||
HasValueEqualFilterSupport,
|
||||
HasValueContainsFilterSupport,
|
||||
HasNumericValueComparableToFilterSupport,
|
||||
BaserowFormulaTypeHasEmptyBaserowExpression,
|
||||
BaserowFormulaValidType,
|
||||
):
|
||||
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:
|
||||
return f"number({self.number_decimal_places})"
|
||||
|
||||
|
||||
class BaserowFormulaBooleanType(
|
||||
HasAllValuesEqualFilterSupport,
|
||||
HasValueFilterSupport,
|
||||
HasValueEqualFilterSupport,
|
||||
BaserowFormulaTypeHasEmptyBaserowExpression,
|
||||
BaserowFormulaValidType,
|
||||
):
|
||||
|
@ -500,22 +526,9 @@ class BaserowFormulaBooleanType(
|
|||
):
|
||||
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(
|
||||
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
||||
) -> OptionallyAnnotatedQ:
|
||||
value = self._get_prep_value(value)
|
||||
return get_array_json_filter_expression(
|
||||
JSONArrayContainsValueExpr, field_name, value
|
||||
)
|
||||
|
@ -533,7 +546,6 @@ class BaserowFormulaBooleanType(
|
|||
def get_has_all_values_equal_query(
|
||||
self, field_name: str, value: str, model_field: models.Field, field: "Field"
|
||||
) -> "OptionallyAnnotatedQ":
|
||||
value = self._get_prep_value(value)
|
||||
return super().get_has_all_values_equal_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
|
@ -1062,12 +1074,7 @@ class BaserowFormulaSingleFileType(BaserowJSONBObjectBaseType):
|
|||
|
||||
|
||||
class BaserowFormulaArrayType(
|
||||
HasAllValuesEqualFilterSupport,
|
||||
HasValueEmptyFilterSupport,
|
||||
HasValueFilterSupport,
|
||||
HasValueContainsFilterSupport,
|
||||
HasValueContainsWordFilterSupport,
|
||||
HasValueLengthIsLowerThanFilterSupport,
|
||||
BaserowFormulaArrayFilterSupportMixin,
|
||||
BaserowFormulaValidType,
|
||||
):
|
||||
type = "array"
|
||||
|
@ -1235,57 +1242,6 @@ class BaserowFormulaArrayType(
|
|||
def contains_query(self, field_name, value, model_field, field):
|
||||
return Q()
|
||||
|
||||
def get_in_array_is_query(self, field_name, value, model_field, field):
|
||||
if not isinstance(self.sub_type, HasValueFilterSupport):
|
||||
raise FilterNotSupportedException()
|
||||
return self.sub_type.get_in_array_is_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
|
||||
def get_in_array_empty_query(self, field_name, model_field, field):
|
||||
if not isinstance(self.sub_type, HasValueEmptyFilterSupport):
|
||||
raise FilterNotSupportedException()
|
||||
|
||||
return self.sub_type.get_in_array_empty_query(field_name, model_field, field)
|
||||
|
||||
def get_in_array_contains_query(self, field_name, value, model_field, field):
|
||||
if not isinstance(self.sub_type, HasValueContainsFilterSupport):
|
||||
raise FilterNotSupportedException()
|
||||
|
||||
return self.sub_type.get_in_array_contains_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
|
||||
def get_in_array_contains_word_query(self, field_name, value, model_field, field):
|
||||
if not isinstance(self.sub_type, HasValueContainsWordFilterSupport):
|
||||
raise FilterNotSupportedException()
|
||||
|
||||
return self.sub_type.get_in_array_contains_word_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
|
||||
def get_in_array_length_is_lower_than_query(
|
||||
self, field_name, value, model_field, field
|
||||
):
|
||||
if not isinstance(self.sub_type, HasValueLengthIsLowerThanFilterSupport):
|
||||
raise FilterNotSupportedException()
|
||||
|
||||
return self.sub_type.get_in_array_length_is_lower_than_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
|
||||
def get_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):
|
||||
return "p_in = '';"
|
||||
|
||||
|
@ -1362,10 +1318,8 @@ class BaserowFormulaArrayType(
|
|||
return None
|
||||
|
||||
def check_if_compatible_with(self, compatible_formula_types: List[str]):
|
||||
return (
|
||||
self.type in compatible_formula_types
|
||||
or str(self) in compatible_formula_types
|
||||
)
|
||||
self_as_str = self.formula_array_type_as_str(self.sub_type.type)
|
||||
return self_as_str in compatible_formula_types
|
||||
|
||||
def __str__(self) -> str:
|
||||
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(
|
||||
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_types import FormulaFieldType
|
||||
from baserow.contrib.database.fields.filter_support.base import (
|
||||
HasAllValuesEqualFilterSupport,
|
||||
HasNumericValueComparableToFilterSupport,
|
||||
HasValueContainsFilterSupport,
|
||||
HasValueContainsWordFilterSupport,
|
||||
HasValueEmptyFilterSupport,
|
||||
HasValueFilterSupport,
|
||||
HasValueEqualFilterSupport,
|
||||
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.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 (
|
||||
BaserowFormulaBooleanType,
|
||||
BaserowFormulaCharType,
|
||||
|
@ -37,18 +44,13 @@ class HasEmptyValueViewFilterType(ViewFilterType):
|
|||
FormulaFieldType.array_of(BaserowFormulaCharType.type),
|
||||
FormulaFieldType.array_of(BaserowFormulaURLType.type),
|
||||
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
|
||||
FormulaFieldType.array_of(BaserowFormulaNumberType.type),
|
||||
),
|
||||
]
|
||||
|
||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||
field_type = field_type_registry.get_by_model(field)
|
||||
try:
|
||||
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()
|
||||
field_type: HasValueEmptyFilterSupport = field_type_registry.get_by_model(field)
|
||||
return field_type.get_in_array_empty_query(field_name, model_field, field)
|
||||
|
||||
|
||||
class HasNotEmptyValueViewFilterType(
|
||||
|
@ -57,7 +59,43 @@ class HasNotEmptyValueViewFilterType(
|
|||
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
|
||||
items in an array.
|
||||
|
@ -71,19 +109,15 @@ class HasValueEqualViewFilterType(ViewFilterType):
|
|||
FormulaFieldType.array_of(BaserowFormulaURLType.type),
|
||||
FormulaFieldType.array_of(BaserowFormulaBooleanType.type),
|
||||
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
|
||||
FormulaFieldType.array_of(BaserowFormulaNumberType.type),
|
||||
),
|
||||
]
|
||||
|
||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||
field_type = field_type_registry.get_by_model(field)
|
||||
try:
|
||||
if not isinstance(field_type, HasValueFilterSupport):
|
||||
raise FilterNotSupportedException(field_type)
|
||||
return field_type.get_in_array_is_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
except FilterNotSupportedException:
|
||||
return self.default_filter_on_exception()
|
||||
def get_filter_expression(
|
||||
self, field_name, value, model_field, field
|
||||
) -> OptionallyAnnotatedQ:
|
||||
field_type: HasValueEqualFilterSupport = field_type_registry.get_by_model(field)
|
||||
return field_type.get_in_array_is_query(field_name, value, model_field, field)
|
||||
|
||||
|
||||
class HasNotValueEqualViewFilterType(
|
||||
|
@ -105,20 +139,20 @@ class HasValueContainsViewFilterType(ViewFilterType):
|
|||
FormulaFieldType.array_of(BaserowFormulaCharType.type),
|
||||
FormulaFieldType.array_of(BaserowFormulaURLType.type),
|
||||
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
|
||||
FormulaFieldType.array_of(BaserowFormulaNumberType.type),
|
||||
),
|
||||
]
|
||||
|
||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||
field_type = field_type_registry.get_by_model(field)
|
||||
try:
|
||||
if not isinstance(field_type, HasValueContainsFilterSupport):
|
||||
raise FilterNotSupportedException(field_type)
|
||||
if value == "":
|
||||
return Q()
|
||||
|
||||
return field_type.get_in_array_contains_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
except FilterNotSupportedException:
|
||||
return self.default_filter_on_exception()
|
||||
field_type: HasValueContainsFilterSupport = field_type_registry.get_by_model(
|
||||
field
|
||||
)
|
||||
return field_type.get_in_array_contains_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
|
||||
|
||||
class HasNotValueContainsViewFilterType(
|
||||
|
@ -144,16 +178,15 @@ class HasValueContainsWordViewFilterType(ViewFilterType):
|
|||
]
|
||||
|
||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||
field_type = field_type_registry.get_by_model(field)
|
||||
try:
|
||||
if not isinstance(field_type, HasValueContainsWordFilterSupport):
|
||||
raise FilterNotSupportedException(field_type)
|
||||
if value == "":
|
||||
return Q()
|
||||
|
||||
return field_type.get_in_array_contains_word_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
except FilterNotSupportedException:
|
||||
return self.default_filter_on_exception()
|
||||
field_type: HasValueContainsWordFilterSupport = (
|
||||
field_type_registry.get_by_model(field)
|
||||
)
|
||||
return field_type.get_in_array_contains_word_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
|
||||
|
||||
class HasNotValueContainsWordViewFilterType(
|
||||
|
@ -178,19 +211,25 @@ class HasValueLengthIsLowerThanViewFilterType(ViewFilterType):
|
|||
]
|
||||
|
||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||
field_type = field_type_registry.get_by_model(field)
|
||||
try:
|
||||
if not isinstance(field_type, HasValueLengthIsLowerThanFilterSupport):
|
||||
raise FilterNotSupportedException(field_type)
|
||||
value = value.strip()
|
||||
if value == "":
|
||||
return Q()
|
||||
|
||||
return field_type.get_in_array_length_is_lower_than_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
except FilterNotSupportedException:
|
||||
try:
|
||||
# The value is expected to be an integer representing the length to compare
|
||||
filter_value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
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.
|
||||
"""
|
||||
|
@ -202,16 +241,15 @@ class HasAllValuesEqualViewFilterType(ViewFilterType):
|
|||
),
|
||||
]
|
||||
|
||||
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
|
||||
try:
|
||||
field_type = field_type_registry.get_by_model(field)
|
||||
if not isinstance(field_type, HasAllValuesEqualFilterSupport):
|
||||
raise FilterNotSupportedException(field_type)
|
||||
return field_type.get_has_all_values_equal_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
except FilterNotSupportedException:
|
||||
return self.default_filter_on_exception()
|
||||
def get_filter_expression(
|
||||
self, field_name, value, model_field, field
|
||||
) -> OptionallyAnnotatedQ:
|
||||
field_type: HasAllValuesEqualViewFilterType = field_type_registry.get_by_model(
|
||||
field
|
||||
)
|
||||
return field_type.get_has_all_values_equal_query(
|
||||
field_name, value, model_field, field
|
||||
)
|
||||
|
||||
|
||||
class HasAnySelectOptionEqualViewFilterType(HasValueEqualViewFilterType):
|
||||
|
@ -228,6 +266,9 @@ class HasAnySelectOptionEqualViewFilterType(HasValueEqualViewFilterType):
|
|||
]
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -240,3 +281,101 @@ class HasNoneSelectOptionEqualViewFilterType(
|
|||
"""
|
||||
|
||||
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
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from functools import reduce
|
||||
from math import ceil, floor
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Dict, NamedTuple, Optional, Tuple, Union
|
||||
|
||||
|
@ -124,8 +122,9 @@ class EqualViewFilterType(ViewFilterType):
|
|||
return Q()
|
||||
|
||||
# Check if the model_field accepts the value.
|
||||
field_type = field_type_registry.get_by_model(field)
|
||||
try:
|
||||
value = model_field.get_prep_value(value)
|
||||
value = field_type.prepare_filter_value(field, model_field, value)
|
||||
return Q(**{field_name: value})
|
||||
except 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):
|
||||
value = value.strip()
|
||||
|
||||
|
@ -389,17 +385,14 @@ class NumericComparisonViewFilterType(ViewFilterType):
|
|||
if value == "":
|
||||
return Q()
|
||||
|
||||
if self.should_round_value_to_compare(value, model_field):
|
||||
decimal_value = Decimal(value)
|
||||
value = self.rounding_func(decimal_value)
|
||||
|
||||
# Check if the model_field accepts the value.
|
||||
field_type = field_type_registry.get_by_model(field)
|
||||
try:
|
||||
value = model_field.get_prep_value(value)
|
||||
return Q(**{f"{field_name}__{self.operator}": value})
|
||||
except Exception:
|
||||
filter_value = field_type.prepare_filter_value(field, model_field, value)
|
||||
except ValueError:
|
||||
return self.default_filter_on_exception()
|
||||
|
||||
return Q(**{f"{field_name}__{self.operator}": filter_value})
|
||||
|
||||
|
||||
class LowerThanViewFilterType(NumericComparisonViewFilterType):
|
||||
"""
|
||||
|
@ -409,7 +402,6 @@ class LowerThanViewFilterType(NumericComparisonViewFilterType):
|
|||
|
||||
type = "lower_than"
|
||||
operator = "lt"
|
||||
rounding_func = floor
|
||||
|
||||
|
||||
class LowerThanOrEqualViewFilterType(NumericComparisonViewFilterType):
|
||||
|
@ -421,7 +413,6 @@ class LowerThanOrEqualViewFilterType(NumericComparisonViewFilterType):
|
|||
|
||||
type = "lower_than_or_equal"
|
||||
operator = "lte"
|
||||
rounding_func = floor
|
||||
|
||||
|
||||
class HigherThanViewFilterType(NumericComparisonViewFilterType):
|
||||
|
@ -432,7 +423,6 @@ class HigherThanViewFilterType(NumericComparisonViewFilterType):
|
|||
|
||||
type = "higher_than"
|
||||
operator = "gt"
|
||||
rounding_func = ceil
|
||||
|
||||
|
||||
class HigherThanOrEqualViewFilterType(NumericComparisonViewFilterType):
|
||||
|
@ -444,7 +434,6 @@ class HigherThanOrEqualViewFilterType(NumericComparisonViewFilterType):
|
|||
|
||||
type = "higher_than_or_equal"
|
||||
operator = "gte"
|
||||
rounding_func = ceil
|
||||
|
||||
|
||||
class TimezoneAwareDateViewFilterType(ViewFilterType):
|
||||
|
@ -1217,24 +1206,15 @@ class BooleanViewFilterType(ViewFilterType):
|
|||
]
|
||||
|
||||
def get_filter(self, field_name, value, model_field, field):
|
||||
value = value.strip().lower()
|
||||
value = value in [
|
||||
"y",
|
||||
"t",
|
||||
"o",
|
||||
"yes",
|
||||
"true",
|
||||
"on",
|
||||
"1",
|
||||
]
|
||||
if value == "": # consider emtpy value as False
|
||||
value = "false"
|
||||
|
||||
# Check if the model_field accepts the value.
|
||||
# noinspection PyBroadException
|
||||
field_type = field_type_registry.get_by_model(field)
|
||||
try:
|
||||
value = model_field.get_prep_value(value)
|
||||
value = field_type.prepare_filter_value(field, model_field, value)
|
||||
return Q(**{field_name: value})
|
||||
except Exception:
|
||||
return Q()
|
||||
except ValueError:
|
||||
return self.default_filter_on_exception()
|
||||
|
||||
|
||||
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(
|
||||
data_fixture, target_field_factory
|
||||
data_fixture,
|
||||
target_field_factory,
|
||||
helper_fields_other_table: Iterable[Callable] = frozenset(),
|
||||
helper_fields_table: Iterable[Callable] = frozenset(),
|
||||
) -> LookupFieldSetup:
|
||||
user = data_fixture.create_user()
|
||||
database = data_fixture.create_database_application(user=user)
|
||||
table = data_fixture.create_database_table(user=user, database=database)
|
||||
other_table = data_fixture.create_database_table(user=user, database=database)
|
||||
target_field = target_field_factory(data_fixture, other_table, user)
|
||||
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(
|
||||
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,
|
||||
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)
|
||||
view_handler = ViewHandler()
|
||||
row_handler = RowHandler()
|
||||
|
|
|
@ -1629,12 +1629,17 @@ def test_has_value_length_is_lower_than_uuid_field_types(data_fixture):
|
|||
(
|
||||
"has_all_values_equal",
|
||||
"",
|
||||
[BooleanLookupRow.ALL_FALSE],
|
||||
[
|
||||
BooleanLookupRow.ALL_TRUE,
|
||||
BooleanLookupRow.ALL_FALSE,
|
||||
BooleanLookupRow.NO_VALUES,
|
||||
BooleanLookupRow.MIXED,
|
||||
],
|
||||
),
|
||||
(
|
||||
"has_all_values_equal",
|
||||
"invalid",
|
||||
[BooleanLookupRow.ALL_FALSE],
|
||||
[],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
@ -1683,12 +1688,17 @@ def test_has_all_values_equal_filter_boolean_lookup_field_type(
|
|||
(
|
||||
"has_value_equal",
|
||||
"",
|
||||
[BooleanLookupRow.MIXED, BooleanLookupRow.ALL_FALSE],
|
||||
[
|
||||
BooleanLookupRow.MIXED,
|
||||
BooleanLookupRow.ALL_FALSE,
|
||||
BooleanLookupRow.NO_VALUES,
|
||||
BooleanLookupRow.ALL_TRUE,
|
||||
],
|
||||
),
|
||||
(
|
||||
"has_value_equal",
|
||||
"invalid",
|
||||
[BooleanLookupRow.MIXED, BooleanLookupRow.ALL_FALSE],
|
||||
[],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
@ -1737,12 +1747,23 @@ def test_has_value_equal_filter_boolean_lookup_field_type(
|
|||
(
|
||||
"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",
|
||||
"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,
|
||||
genericHasValueLengthLowerThanFilter,
|
||||
genericHasAllValuesEqualFilter,
|
||||
numericHasValueComparableToFilterFunction,
|
||||
ComparisonOperator,
|
||||
} from '@baserow/modules/database/utils/fieldFilters'
|
||||
|
||||
export const hasEmptyValueFilterMixin = {
|
||||
|
@ -24,6 +26,7 @@ export const hasAllValuesEqualFilterMixin = {
|
|||
this.getHasAllValuesEqualFilterFunction(field)(cellValue, filterValue)
|
||||
)
|
||||
},
|
||||
|
||||
hasNotAllValuesEqualFilter(cellValue, filterValue, field) {
|
||||
return (
|
||||
filterValue === '' ||
|
||||
|
@ -91,11 +94,107 @@ export const hasValueLengthIsLowerThanFilterMixin = {
|
|||
},
|
||||
}
|
||||
|
||||
export const formulaArrayFilterMixin = {
|
||||
getSubType(field) {
|
||||
return this.app.$registry.get('formula_type', field.array_formula_type)
|
||||
export const hasNumericValueComparableToFilterMixin = {
|
||||
// equal to
|
||||
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) {
|
||||
const subType = this.getSubType(field)
|
||||
return subType.getHasEmptyValueFilterFunction(field)
|
||||
|
@ -144,6 +243,20 @@ export const formulaArrayFilterMixin = {
|
|||
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(
|
||||
|
|
|
@ -4,20 +4,8 @@ import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
|
|||
import { ViewFilterType } from '@baserow/modules/database/viewFilters'
|
||||
import viewFilterTypeText from '@baserow/modules/database/components/view/ViewFilterTypeText.vue'
|
||||
import ViewFilterTypeMultipleSelectOptions from '@baserow/modules/database/components/view/ViewFilterTypeMultipleSelectOptions'
|
||||
import _ from 'lodash'
|
||||
|
||||
/**
|
||||
* 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'
|
||||
}
|
||||
import { BaserowFormulaNumberType } from '@baserow/modules/database/formula/formulaTypes'
|
||||
import { ComparisonOperator } from '@baserow/modules/database//utils/fieldFilters'
|
||||
|
||||
export class HasEmptyValueViewFilterType extends ViewFilterType {
|
||||
static getType() {
|
||||
|
@ -35,6 +23,7 @@ export class HasEmptyValueViewFilterType extends ViewFilterType {
|
|||
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
|
||||
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(url)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(number)'),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -77,22 +67,11 @@ export class HasValueEqualViewFilterType extends ViewFilterType {
|
|||
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) {
|
||||
filterValue = fieldType.parseInputValue(field, filterValue)
|
||||
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||
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) {
|
||||
const fieldType = this.app.$registry.get('field', field.type)
|
||||
return fieldType.getFilterInputComponent(field, this) || viewFilterTypeText
|
||||
|
@ -105,7 +84,8 @@ export class HasValueEqualViewFilterType extends ViewFilterType {
|
|||
FormulaFieldType.arrayOf('char'),
|
||||
FormulaFieldType.arrayOf('url'),
|
||||
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) {
|
||||
filterValue = fieldType.parseInputValue(field, filterValue)
|
||||
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||
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) {
|
||||
const fieldType = this.app.$registry.get('field', field.type)
|
||||
return fieldType.getFilterInputComponent(field, this) || viewFilterTypeText
|
||||
|
@ -149,7 +118,8 @@ export class HasNotValueEqualViewFilterType extends ViewFilterType {
|
|||
FormulaFieldType.arrayOf('char'),
|
||||
FormulaFieldType.arrayOf('url'),
|
||||
FormulaFieldType.arrayOf('boolean'),
|
||||
FormulaFieldType.arrayOf('single_select')
|
||||
FormulaFieldType.arrayOf('single_select'),
|
||||
FormulaFieldType.arrayOf('number')
|
||||
),
|
||||
]
|
||||
}
|
||||
|
@ -171,10 +141,13 @@ export class HasValueContainsViewFilterType extends ViewFilterType {
|
|||
|
||||
getCompatibleFieldTypes() {
|
||||
return [
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes(
|
||||
FormulaFieldType.arrayOf('char'),
|
||||
FormulaFieldType.arrayOf('text'),
|
||||
FormulaFieldType.arrayOf('url'),
|
||||
FormulaFieldType.arrayOf('single_select'),
|
||||
FormulaFieldType.arrayOf('number')
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -199,10 +172,13 @@ export class HasNotValueContainsViewFilterType extends ViewFilterType {
|
|||
|
||||
getCompatibleFieldTypes() {
|
||||
return [
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
|
||||
FormulaFieldType.compatibleWithFormulaTypes(
|
||||
FormulaFieldType.arrayOf('char'),
|
||||
FormulaFieldType.arrayOf('text'),
|
||||
FormulaFieldType.arrayOf('url'),
|
||||
FormulaFieldType.arrayOf('single_select'),
|
||||
FormulaFieldType.arrayOf('number')
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -321,7 +297,7 @@ export class HasAllValuesEqualViewFilterType extends ViewFilterType {
|
|||
}
|
||||
|
||||
matches(cellValue, filterValue, field, fieldType) {
|
||||
filterValue = fieldType.parseInputValue(field, filterValue)
|
||||
filterValue = fieldType.prepareFilterValue(field, filterValue)
|
||||
return fieldType.hasAllValuesEqualFilter(cellValue, filterValue, field)
|
||||
}
|
||||
}
|
||||
|
@ -371,3 +347,243 @@ export class HasNoneSelectOptionEqualViewFilterType extends ViewFilterType {
|
|||
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 {
|
||||
name: 'ViewFilterTypeNumber',
|
||||
mixins: [filterTypeInput, numberField],
|
||||
data() {
|
||||
return {
|
||||
// Avoid rounding decimals to ensure filter values match backend behavior.
|
||||
roundDecimals: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
field: {
|
||||
handler() {
|
||||
|
|
|
@ -15,14 +15,7 @@ import {
|
|||
isValidEmail,
|
||||
isValidURL,
|
||||
} from '@baserow/modules/core/utils/string'
|
||||
import {
|
||||
hasValueContainsFilterMixin,
|
||||
hasValueEqualFilterMixin,
|
||||
hasValueContainsWordFilterMixin,
|
||||
hasValueLengthIsLowerThanFilterMixin,
|
||||
hasEmptyValueFilterMixin,
|
||||
hasAllValuesEqualFilterMixin,
|
||||
} from '@baserow/modules/database/arrayFilterMixins'
|
||||
import { formulaFieldArrayFilterMixin } from '@baserow/modules/database/arrayFilterMixins'
|
||||
import {
|
||||
parseNumberValue,
|
||||
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) {
|
||||
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
|
||||
* 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)
|
||||
}
|
||||
|
||||
prepareFilterValue(field, value) {
|
||||
const res = parseNumberValue(field, String(value ?? ''), false)
|
||||
return res === null || res.isNaN() ? '' : res.toString()
|
||||
}
|
||||
}
|
||||
|
||||
BigNumber.config({ EXPONENTIAL_AT: NumberFieldType.getMaxNumberLength() })
|
||||
|
@ -1914,6 +1927,10 @@ export class BooleanFieldType extends FieldType {
|
|||
getHasNotValueEqualFilterFunction(field) {
|
||||
return this.getHasValueEqualFilterFunction(field, true)
|
||||
}
|
||||
|
||||
prepareFilterValue(field, value) {
|
||||
return this.parseInputValue(field, String(value ?? ''))
|
||||
}
|
||||
}
|
||||
|
||||
class BaseDateFieldType extends FieldType {
|
||||
|
@ -3745,12 +3762,7 @@ export class PhoneNumberFieldType extends FieldType {
|
|||
}
|
||||
|
||||
export class FormulaFieldType extends mix(
|
||||
hasAllValuesEqualFilterMixin,
|
||||
hasEmptyValueFilterMixin,
|
||||
hasValueEqualFilterMixin,
|
||||
hasValueContainsFilterMixin,
|
||||
hasValueContainsWordFilterMixin,
|
||||
hasValueLengthIsLowerThanFilterMixin,
|
||||
formulaFieldArrayFilterMixin,
|
||||
FieldType
|
||||
) {
|
||||
static getType() {
|
||||
|
@ -3792,7 +3804,11 @@ export class FormulaFieldType extends mix(
|
|||
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)
|
||||
}
|
||||
|
||||
|
@ -3813,7 +3829,7 @@ export class FormulaFieldType extends mix(
|
|||
}
|
||||
|
||||
getFilterInputComponent(field, filterType) {
|
||||
return this.getFormulaSubtype(field)?.getFilterInputComponent(
|
||||
return this.getFormulaType(field)?.getFilterInputComponent(
|
||||
field,
|
||||
filterType
|
||||
)
|
||||
|
@ -3824,15 +3840,15 @@ export class FormulaFieldType extends mix(
|
|||
}
|
||||
|
||||
getCardValueHeight(field) {
|
||||
return this.getFormulaSubtype(field)?.getCardComponent().height || 0
|
||||
return this.getFormulaType(field)?.getCardComponent().height || 0
|
||||
}
|
||||
|
||||
getCanSortInView(field) {
|
||||
return this.getFormulaSubtype(field)?.getCanSortInView(field)
|
||||
return this.getFormulaType(field)?.getCanSortInView(field)
|
||||
}
|
||||
|
||||
getSort(name, order, field) {
|
||||
return this.getFormulaSubtype(field)?.getSort(name, order, field)
|
||||
return this.getFormulaType(field)?.getSort(name, order, field)
|
||||
}
|
||||
|
||||
getEmptyValue(field) {
|
||||
|
@ -3840,7 +3856,7 @@ export class FormulaFieldType extends mix(
|
|||
}
|
||||
|
||||
getDocsDataType(field) {
|
||||
return this.getFormulaSubtype(field)?.getDocsDataType(field)
|
||||
return this.getFormulaType(field)?.getDocsDataType(field)
|
||||
}
|
||||
|
||||
getDocsDescription(field) {
|
||||
|
@ -3852,11 +3868,11 @@ export class FormulaFieldType extends mix(
|
|||
}
|
||||
|
||||
getDocsResponseExample(field) {
|
||||
return this.getFormulaSubtype(field)?.getDocsResponseExample(field)
|
||||
return this.getFormulaType(field)?.getDocsResponseExample(field)
|
||||
}
|
||||
|
||||
prepareValueForCopy(field, value) {
|
||||
return this.getFormulaSubtype(field)?.prepareValueForCopy(field, value)
|
||||
return this.getFormulaType(field)?.prepareValueForCopy(field, value)
|
||||
}
|
||||
|
||||
getContainsFilterFunction(field) {
|
||||
|
@ -3876,11 +3892,11 @@ export class FormulaFieldType extends mix(
|
|||
}
|
||||
|
||||
toHumanReadableString(field, value) {
|
||||
return this.getFormulaSubtype(field)?.toHumanReadableString(field, value)
|
||||
return this.getFormulaType(field)?.toHumanReadableString(field, value)
|
||||
}
|
||||
|
||||
getSortIndicator(field) {
|
||||
return this.getFormulaSubtype(field)?.getSortIndicator(field)
|
||||
return this.getFormulaType(field)?.getSortIndicator(field)
|
||||
}
|
||||
|
||||
getFormComponent() {
|
||||
|
@ -3912,57 +3928,25 @@ export class FormulaFieldType extends mix(
|
|||
}
|
||||
|
||||
canRepresentDate(field) {
|
||||
return this.getFormulaSubtype(field)?.canRepresentDate(field)
|
||||
return this.getFormulaType(field)?.canRepresentDate(field)
|
||||
}
|
||||
|
||||
getCanGroupByInView(field) {
|
||||
return this.getFormulaSubtype(field)?.canGroupByInView(field)
|
||||
return this.getFormulaType(field)?.canGroupByInView(field)
|
||||
}
|
||||
|
||||
parseInputValue(field, value) {
|
||||
const underlyingFieldType = this.getFormulaSubtype(field)
|
||||
const underlyingFieldType = this.getFormulaType(field)
|
||||
return underlyingFieldType.parseInputValue(field, value)
|
||||
}
|
||||
|
||||
parseFromLinkedRowItemValue(field, value) {
|
||||
const underlyingFieldType = this.getFormulaSubtype(field)
|
||||
const underlyingFieldType = this.getFormulaType(field)
|
||||
return underlyingFieldType.parseFromLinkedRowItemValue(field, value)
|
||||
}
|
||||
|
||||
canRepresentFiles(field) {
|
||||
return this.getFormulaSubtype(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)
|
||||
return this.getFormulaType(field)?.canRepresentFiles(field)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,8 @@ import {
|
|||
hasSelectOptionIdEqualMixin,
|
||||
hasSelectOptionValueContainsFilterMixin,
|
||||
hasSelectOptionValueContainsWordFilterMixin,
|
||||
formulaArrayFilterMixin,
|
||||
baserowFormulaArrayTypeFilterMixin,
|
||||
hasNumericValueComparableToFilterMixin,
|
||||
} from '@baserow/modules/database/arrayFilterMixins'
|
||||
import _ from 'lodash'
|
||||
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) {
|
||||
return null
|
||||
return this.app.$registry
|
||||
.get('field', this.getFieldType())
|
||||
.getFilterInputComponent(field, filterType)
|
||||
}
|
||||
|
||||
getRowEditArrayFieldComponent() {
|
||||
return null
|
||||
}
|
||||
|
||||
prepareFilterValue(field, value) {
|
||||
return this.app.$registry
|
||||
.get('field', this.getFieldType())
|
||||
.prepareFilterValue(field, value)
|
||||
}
|
||||
|
||||
getFunctionalGridViewFieldComponent() {
|
||||
return this.app.$registry
|
||||
.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() {
|
||||
return 'number'
|
||||
}
|
||||
|
@ -582,7 +593,7 @@ export class BaserowFormulaInvalidType extends BaserowFormulaTypeDefinition {
|
|||
}
|
||||
|
||||
export class BaserowFormulaArrayType extends mix(
|
||||
formulaArrayFilterMixin,
|
||||
baserowFormulaArrayTypeFilterMixin,
|
||||
BaserowFormulaTypeDefinition
|
||||
) {
|
||||
static getType() {
|
||||
|
@ -605,6 +616,14 @@ export class BaserowFormulaArrayType extends mix(
|
|||
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) {
|
||||
const arrayOverride =
|
||||
this.getSubType(field)?.getRowEditArrayFieldComponent()
|
||||
|
|
|
@ -580,7 +580,15 @@
|
|||
},
|
||||
"viewFilter": {
|
||||
"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": {
|
||||
"exportView": "Export view",
|
||||
|
|
|
@ -16,6 +16,7 @@ export default {
|
|||
return {
|
||||
formattedValue: '',
|
||||
focused: false,
|
||||
roundDecimals: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -23,7 +24,7 @@ export default {
|
|||
this.formattedValue = this.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
|
||||
|
@ -33,10 +34,15 @@ export default {
|
|||
*/
|
||||
formatNumberValueForEdit(field, value) {
|
||||
const withThousandSeparator = false
|
||||
return formatNumberValue(field, value, withThousandSeparator)
|
||||
return formatNumberValue(
|
||||
field,
|
||||
value,
|
||||
withThousandSeparator,
|
||||
this.roundDecimals
|
||||
)
|
||||
},
|
||||
parseNumberValue(field, value) {
|
||||
return parseNumberValue(field, value)
|
||||
return parseNumberValue(field, value, this.roundDecimals)
|
||||
},
|
||||
getNumberFormatOptions(field) {
|
||||
return getNumberFormatOptions(field)
|
||||
|
|
|
@ -108,6 +108,14 @@ import {
|
|||
HasAllValuesEqualViewFilterType,
|
||||
HasAnySelectOptionEqualViewFilterType,
|
||||
HasNoneSelectOptionEqualViewFilterType,
|
||||
HasValueLowerThanViewFilterType,
|
||||
HasValueLowerThanOrEqualViewFilterType,
|
||||
HasValueHigherThanViewFilterType,
|
||||
HasValueHigherThanOrEqualViewFilterType,
|
||||
HasNotValueLowerThanOrEqualViewFilterType,
|
||||
HasNotValueLowerThanViewFilterType,
|
||||
HasNotValueHigherThanOrEqualViewFilterType,
|
||||
HasNotValueHigherThanViewFilterType,
|
||||
} from '@baserow/modules/database/arrayViewFilters'
|
||||
import {
|
||||
CSVImporterType,
|
||||
|
@ -581,6 +589,40 @@ export default (context) => {
|
|||
app.$registry.register('viewFilter', new NotEmptyViewFilterType(context))
|
||||
app.$registry.register('viewFilter', new UserIsFilterType(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(
|
||||
'viewOwnershipType',
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
// list of file names, we don't want the filterValue to accidentally match the end
|
||||
// of one filename and the start of another.
|
||||
import _ from 'lodash'
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
export function filenameContainsFilter(
|
||||
rowValue,
|
||||
|
@ -31,8 +32,8 @@ export function genericContainsFilter(
|
|||
if (humanReadableRowValue == null) {
|
||||
return false
|
||||
}
|
||||
humanReadableRowValue = humanReadableRowValue.toString().toLowerCase().trim()
|
||||
filterValue = filterValue.toString().toLowerCase().trim()
|
||||
humanReadableRowValue = String(humanReadableRowValue).toLowerCase().trim()
|
||||
filterValue = String(filterValue).toLowerCase().trim()
|
||||
|
||||
return humanReadableRowValue.includes(filterValue)
|
||||
}
|
||||
|
@ -45,8 +46,8 @@ export function genericContainsWordFilter(
|
|||
if (humanReadableRowValue == null) {
|
||||
return false
|
||||
}
|
||||
humanReadableRowValue = humanReadableRowValue.toString().toLowerCase().trim()
|
||||
filterValue = filterValue.toString().toLowerCase().trim()
|
||||
humanReadableRowValue = String(humanReadableRowValue).toLowerCase().trim()
|
||||
filterValue = String(filterValue).toLowerCase().trim()
|
||||
// check using regex to match whole words
|
||||
// make sure to escape the filterValue as it may contain regex special characters
|
||||
filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
|
||||
|
@ -100,10 +101,10 @@ export function genericHasValueContainsFilter(cellValue, filterValue) {
|
|||
return false
|
||||
}
|
||||
|
||||
filterValue = filterValue.toString().toLowerCase().trim()
|
||||
filterValue = String(filterValue).toLowerCase().trim()
|
||||
|
||||
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)) {
|
||||
return true
|
||||
|
@ -118,14 +119,14 @@ export function genericHasValueContainsWordFilter(cellValue, filterValue) {
|
|||
return false
|
||||
}
|
||||
|
||||
filterValue = filterValue.toString().toLowerCase().trim()
|
||||
filterValue = String(filterValue).toLowerCase().trim()
|
||||
filterValue = filterValue.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&')
|
||||
|
||||
for (let i = 0; i < cellValue.length; i++) {
|
||||
if (cellValue[i].value == null) {
|
||||
continue
|
||||
}
|
||||
const value = cellValue[i].value.toString().toLowerCase().trim()
|
||||
const value = String(cellValue[i].value).toLowerCase().trim()
|
||||
if (value.match(new RegExp(`\\b${filterValue}\\b`))) {
|
||||
return true
|
||||
}
|
||||
|
@ -143,7 +144,7 @@ export function genericHasValueLengthLowerThanFilter(cellValue, filterValue) {
|
|||
if (cellValue[i].value == null) {
|
||||
continue
|
||||
}
|
||||
const valueLength = cellValue[i].value.toString().length
|
||||
const valueLength = String(cellValue[i].value).length
|
||||
if (valueLength < filterValue) {
|
||||
return true
|
||||
}
|
||||
|
@ -151,3 +152,53 @@ export function genericHasValueLengthLowerThanFilter(cellValue, filterValue) {
|
|||
|
||||
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: '.',
|
||||
}
|
||||
|
||||
const NUMBER_MAX_DECIMAL_PLACES = 10
|
||||
|
||||
const DEFAULT_THOUSAND_SEPARATOR = THOUSAND_SEPARATORS.NONE
|
||||
const DEFAULT_DECIMAL_SEPARATOR = DECIMAL_SEPARATORS.PERIOD
|
||||
|
||||
|
@ -57,7 +59,7 @@ export const getNumberFormatOptions = (field) => {
|
|||
|
||||
const numberPrefix = field.number_prefix ?? ''
|
||||
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
|
||||
|
||||
return {
|
||||
|
@ -79,7 +81,8 @@ export const getNumberFormatOptions = (field) => {
|
|||
export const formatNumberValue = (
|
||||
field,
|
||||
value,
|
||||
withThousandSeparator = true
|
||||
withThousandSeparator = true,
|
||||
roundDecimals = true
|
||||
) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return ''
|
||||
|
@ -95,7 +98,9 @@ export const formatNumberValue = (
|
|||
|
||||
// Parse the input value if it's a string
|
||||
let numericValue =
|
||||
typeof value === 'string' ? parseNumberValue(field, value) : value
|
||||
typeof value === 'string'
|
||||
? parseNumberValue(field, value, roundDecimals)
|
||||
: value
|
||||
|
||||
if (numericValue === null) {
|
||||
return null
|
||||
|
@ -116,9 +121,13 @@ export const formatNumberValue = (
|
|||
locale = 'en-US'
|
||||
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, {
|
||||
minimumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: roundDecimals
|
||||
? decimalPlaces
|
||||
: NUMBER_MAX_DECIMAL_PLACES,
|
||||
useGrouping: true,
|
||||
})
|
||||
let formatted = formatter.format(numericValue)
|
||||
|
@ -139,18 +148,22 @@ export const formatNumberValue = (
|
|||
return `${sign}${numberPrefix}${formatted}${numberSuffix}`.trim()
|
||||
}
|
||||
|
||||
export const parseNumberValue = (field, value) => {
|
||||
export const parseNumberValue = (field, value, roundDecimals = true) => {
|
||||
const { numberPrefix, numberSuffix, decimalSeparator } =
|
||||
getNumberFormatOptions(field)
|
||||
|
||||
if (value === null || value === undefined || value === '') {
|
||||
if (value == null || value === '') {
|
||||
return null
|
||||
}
|
||||
|
||||
const toBigNumber = (val) => {
|
||||
return new BigNumber(
|
||||
new BigNumber(val).toFixed(field.number_decimal_places)
|
||||
)
|
||||
let rounded = val
|
||||
if (roundDecimals) {
|
||||
rounded = new BigNumber(val).decimalPlaces(
|
||||
field.number_decimal_places ?? 0
|
||||
)
|
||||
}
|
||||
return new BigNumber(rounded)
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || BigNumber.isBigNumber(value)) {
|
||||
|
@ -192,5 +205,5 @@ export const parseNumberValue = (field, value) => {
|
|||
}
|
||||
|
||||
const parsedNumber = toBigNumber(result)
|
||||
return isNaN(parsedNumber) ? null : isNegative ? -parsedNumber : parsedNumber
|
||||
return parsedNumber.isNaN() ? null : isNegative ? -parsedNumber : parsedNumber
|
||||
}
|
||||
|
|
|
@ -10,8 +10,15 @@ import {
|
|||
HasNotEmptyValueViewFilterType,
|
||||
HasValueLengthIsLowerThanViewFilterType,
|
||||
HasAllValuesEqualViewFilterType,
|
||||
HasValueHigherThanViewFilterType,
|
||||
HasValueHigherThanOrEqualViewFilterType,
|
||||
HasNotValueHigherThanViewFilterType,
|
||||
HasNotValueHigherThanOrEqualViewFilterType,
|
||||
} from '@baserow/modules/database/arrayViewFilters'
|
||||
import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
|
||||
import {
|
||||
FormulaFieldType,
|
||||
LookupFieldType,
|
||||
} from '@baserow/modules/database/fieldTypes'
|
||||
import {
|
||||
EmptyViewFilterType,
|
||||
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