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

Resolve "Introduce “greater than or equal” (>=) and “less than or equal” (<=) filter types"

This commit is contained in:
Eimantas Stonys 2024-04-05 15:43:08 +00:00
parent 1bccbb31e2
commit 5664304685
9 changed files with 612 additions and 79 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database/view
changelog/entries/unreleased/feature
web-frontend
locales
modules/database
test/unit/database

View file

@ -294,6 +294,7 @@ class DatabaseConfig(AppConfig):
FilenameContainsViewFilterType,
FilesLowerThanViewFilterType,
HasFileTypeViewFilterType,
HigherThanOrEqualViewFilterType,
HigherThanViewFilterType,
IsEvenAndWholeViewFilterType,
LengthIsLowerThanViewFilterType,
@ -301,6 +302,7 @@ class DatabaseConfig(AppConfig):
LinkRowHasNotViewFilterType,
LinkRowHasViewFilterType,
LinkRowNotContainsViewFilterType,
LowerThanOrEqualViewFilterType,
LowerThanViewFilterType,
MultipleCollaboratorsHasNotViewFilterType,
MultipleCollaboratorsHasViewFilterType,
@ -327,7 +329,9 @@ class DatabaseConfig(AppConfig):
view_filter_type_registry.register(DoesntContainWordViewFilterType())
view_filter_type_registry.register(LengthIsLowerThanViewFilterType())
view_filter_type_registry.register(HigherThanViewFilterType())
view_filter_type_registry.register(HigherThanOrEqualViewFilterType())
view_filter_type_registry.register(LowerThanViewFilterType())
view_filter_type_registry.register(LowerThanOrEqualViewFilterType())
view_filter_type_registry.register(IsEvenAndWholeViewFilterType())
view_filter_type_registry.register(DateEqualViewFilterType())
view_filter_type_registry.register(DateBeforeViewFilterType())

View file

@ -311,43 +311,6 @@ class LengthIsLowerThanViewFilterType(ViewFilterType):
return self.default_filter_on_exception()
class HigherThanViewFilterType(ViewFilterType):
"""
The higher than filter checks if the field value is higher than the filter value.
It only works if a numeric number is provided. It is at compatible with
models.IntegerField and models.DecimalField.
"""
type = "higher_than"
compatible_field_types = [
NumberFieldType.type,
RatingFieldType.type,
AutonumberFieldType.type,
DurationFieldType.type,
FormulaFieldType.compatible_with_formula_types(
BaserowFormulaNumberType.type,
),
]
def get_filter(self, field_name, value, model_field, field):
value = value.strip()
# If an empty value has been provided we do not want to filter at all.
if value == "":
return Q()
if isinstance(model_field, IntegerField) and value.find(".") != -1:
decimal = Decimal(value)
value = floor(decimal)
# Check if the model_field accepts the value.
try:
value = model_field.get_prep_value(value)
return Q(**{f"{field_name}__gt": value})
except Exception:
return self.default_filter_on_exception()
class IsEvenAndWholeViewFilterType(ViewFilterType):
"""
The is even and whole filter checks if the field value is an even number
@ -372,14 +335,14 @@ class IsEvenAndWholeViewFilterType(ViewFilterType):
)
class LowerThanViewFilterType(ViewFilterType):
class NumericComparisonViewFilterType(ViewFilterType):
"""
The lower than filter checks if the field value is lower than the filter value.
It only works if a numeric number is provided. It is at compatible with
models.IntegerField and models.DecimalField.
Base filter type for basic numeric comparisons. It defines common logic for
'lower than', 'lower than or equal', 'higher than' and 'higher than or equal'
view filter types.
"""
type = "lower_than"
operator = None
compatible_field_types = [
NumberFieldType.type,
RatingFieldType.type,
@ -390,6 +353,9 @@ class LowerThanViewFilterType(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()
@ -397,18 +363,64 @@ class LowerThanViewFilterType(ViewFilterType):
if value == "":
return Q()
if isinstance(model_field, IntegerField) and value.find(".") != -1:
decimal = Decimal(value)
value = ceil(decimal)
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.
try:
value = model_field.get_prep_value(value)
return Q(**{f"{field_name}__lt": value})
return Q(**{f"{field_name}__{self.operator}": value})
except Exception:
return self.default_filter_on_exception()
class LowerThanViewFilterType(NumericComparisonViewFilterType):
"""
The lower than filter checks if the field value is lower than the filter value.
It only works if a numeric number is provided.
"""
type = "lower_than"
operator = "lt"
rounding_func = floor
class LowerThanOrEqualViewFilterType(NumericComparisonViewFilterType):
"""
The lower than or equal filter checks if the field value is lower or if it
equals to the filter value.
It only works if a numeric number is provided.
"""
type = "lower_than_or_equal"
operator = "lte"
rounding_func = floor
class HigherThanViewFilterType(NumericComparisonViewFilterType):
"""
The higher than filter checks if the field value is higher than the filter value.
It only works if a numeric number is provided.
"""
type = "higher_than"
operator = "gt"
rounding_func = ceil
class HigherThanOrEqualViewFilterType(NumericComparisonViewFilterType):
"""
The higher than or equal filter checks if the field value is higher than or
if it equals to the filter value.
It only works if a numeric number is provided.
"""
type = "higher_than_or_equal"
operator = "gte"
rounding_func = ceil
class TimezoneAwareDateViewFilterType(ViewFilterType):
compatible_field_types = [
DateFieldType.type,

View file

@ -1494,6 +1494,115 @@ def test_higher_than_filter_type(data_fixture):
assert len(ids) == 0
@pytest.mark.django_db
def test_higher_than_or_equal_filter_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
grid_view = data_fixture.create_grid_view(table=table)
integer_field = data_fixture.create_number_field(table=table, number_negative=True)
decimal_field = data_fixture.create_number_field(
table=table,
number_decimal_places=2,
number_negative=True,
)
handler = ViewHandler()
model = table.get_model()
row = model.objects.create(
**{
f"field_{integer_field.id}": 10,
f"field_{decimal_field.id}": 20.20,
}
)
model.objects.create(
**{
f"field_{integer_field.id}": None,
f"field_{decimal_field.id}": None,
}
)
row_3 = model.objects.create(
**{
f"field_{integer_field.id}": 99,
f"field_{decimal_field.id}": 99.99,
}
)
row_4 = model.objects.create(
**{
f"field_{integer_field.id}": -10,
f"field_{decimal_field.id}": -30.33,
}
)
view_filter = data_fixture.create_view_filter(
view=grid_view, field=integer_field, type="higher_than_or_equal", value="1"
)
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert (
len(ids) == 2
) # Only rows with values 10 and 99 are equal to or greater than 1
assert row.id in ids
assert row_3.id in ids
view_filter.value = "10"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 2 # Rows with 10 and 99 are equal to or greater than 10
assert row.id in ids
assert row_3.id in ids
view_filter.value = "99"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 1 # Only row_3 matches because it's equal to or greater than 99
assert row_3.id in ids
view_filter.value = "100"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 0 # No rows match
view_filter.value = "not_number"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 0
view_filter.value = "0"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 2 # Rows with 10 and 99 are equal to or greater than 0
view_filter.value = "-10"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert (
len(ids) == 3
) # Includes row, row_3, and row_4 because it's equal to or greater than -10
view_filter.field = decimal_field
view_filter.value = "20.20"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert (
len(ids) == 2
) # Matches row and row_3 with values equal to or greater than 20.20
view_filter.value = "99.99"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 1 # Only row_3 matches
view_filter.value = "100"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 0
view_filter.value = "not_number"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 0
@pytest.mark.django_db
def test_lower_than_filter_type(data_fixture):
user = data_fixture.create_user()
@ -1646,6 +1755,75 @@ def test_lower_than_filter_type(data_fixture):
assert len(ids) == 0
@pytest.mark.django_db
def test_lower_than_or_equal_filter_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
grid_view = data_fixture.create_grid_view(table=table)
integer_field = data_fixture.create_number_field(table=table, number_negative=True)
decimal_field = data_fixture.create_number_field(
table=table,
number_decimal_places=2,
number_negative=True,
)
handler = ViewHandler()
model = table.get_model()
row = model.objects.create(
**{
f"field_{integer_field.id}": 10,
f"field_{decimal_field.id}": 20.20,
}
)
model.objects.create(
**{
f"field_{integer_field.id}": None,
f"field_{decimal_field.id}": None,
}
)
row_3 = model.objects.create(
**{
f"field_{integer_field.id}": 99,
f"field_{decimal_field.id}": 99.99,
}
)
row_4 = model.objects.create(
**{
f"field_{integer_field.id}": -10,
f"field_{decimal_field.id}": -30.33,
}
)
view_filter = data_fixture.create_view_filter(
view=grid_view, field=integer_field, type="lower_than_or_equal", value="1"
)
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 1
assert row_4.id in ids
view_filter.value = "100"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 3 # Includes row, row_3, row_4;
view_filter.value = "-10"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 1 # Only row_4 matches
view_filter.value = "9"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 1 # Only row_4 matches
view_filter.field = decimal_field
view_filter.value = "20.20"
view_filter.save()
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
assert len(ids) == 2 # Includes row and row_4;
@pytest.mark.django_db
def test_is_even_and_whole_number_filter_type(data_fixture):
"""

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Introduce 'greater than or equal to' and 'lower than or equal to' filter types for number fields",
"issue_number": 1988,
"bullet_points": [],
"created_at": "2024-03-29"
}

View file

@ -179,6 +179,7 @@
"has": "has",
"hasNot": "doesn't have",
"higherThan": "higher than",
"higherThanOrEqual": "higher than or equal",
"is": "is",
"isNot": "is not",
"isEmpty": "is empty",
@ -204,6 +205,7 @@
"isWithinWeeks": "is within weeks",
"isWithinMonths": "is within months",
"lowerThan": "lower than",
"lowerThanOrEqual": "lower than or equal",
"isEvenAndWhole": "is even and whole",
"lengthIsLowerThan": "length is lower than",
"hasFileType": "has file type",

View file

@ -45,7 +45,9 @@ import {
ContainsNotViewFilterType,
LengthIsLowerThanViewFilterType,
HigherThanViewFilterType,
HigherThanOrEqualViewFilterType,
LowerThanViewFilterType,
LowerThanOrEqualViewFilterType,
IsEvenAndWholeViewFilterType,
SingleSelectEqualViewFilterType,
SingleSelectNotEqualViewFilterType,
@ -398,7 +400,15 @@ export default (context) => {
new LengthIsLowerThanViewFilterType(context)
)
app.$registry.register('viewFilter', new HigherThanViewFilterType(context))
app.$registry.register(
'viewFilter',
new HigherThanOrEqualViewFilterType(context)
)
app.$registry.register('viewFilter', new LowerThanViewFilterType(context))
app.$registry.register(
'viewFilter',
new LowerThanOrEqualViewFilterType(context)
)
app.$registry.register(
'viewFilter',
new IsEvenAndWholeViewFilterType(context)

View file

@ -1326,16 +1326,10 @@ export class DateEqualsDayOfMonthViewFilterType extends LocalizedDateViewFilterT
}
}
export class HigherThanViewFilterType extends ViewFilterType {
static getType() {
return 'higher_than'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.higherThan')
}
// Base filter type for basic numeric comparisons. It defines common logic for
// 'lower than', 'lower than or equal', 'higher than' and 'higher than or equal'
// view filter types.
export class NumericComparisonViewFilterType extends ViewFilterType {
getExample() {
return '100'
}
@ -1358,6 +1352,22 @@ export class HigherThanViewFilterType extends ViewFilterType {
]
}
// This method should be implemented by subclasses to define their comparison logic.
matches(rowValue, filterValue, field, fieldType) {
throw new Error('matches method must be implemented by subclasses')
}
}
export class HigherThanViewFilterType extends NumericComparisonViewFilterType {
static getType() {
return 'higher_than'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.higherThan')
}
matches(rowValue, filterValue, field, fieldType) {
if (filterValue === '') {
return true
@ -1369,7 +1379,30 @@ export class HigherThanViewFilterType extends ViewFilterType {
}
}
export class LowerThanViewFilterType extends ViewFilterType {
export class HigherThanOrEqualViewFilterType extends NumericComparisonViewFilterType {
static getType() {
return 'higher_than_or_equal'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.higherThanOrEqual')
}
matches(rowValue, filterValue, field, fieldType) {
if (filterValue === '') {
return true
}
const rowVal = fieldType.parseInputValue(field, rowValue)
const fltVal = fieldType.parseInputValue(field, filterValue)
return (
Number.isFinite(rowVal) && Number.isFinite(fltVal) && rowVal >= fltVal
)
}
}
export class LowerThanViewFilterType extends NumericComparisonViewFilterType {
static getType() {
return 'lower_than'
}
@ -1379,28 +1412,6 @@ export class LowerThanViewFilterType extends ViewFilterType {
return i18n.t('viewFilter.lowerThan')
}
getExample() {
return '100'
}
getInputComponent(field) {
const inputComponent = {
[RatingFieldType.getType()]: ViewFilterTypeRating,
[DurationFieldType.getType()]: ViewFilterTypeDuration,
}
return inputComponent[field?.type] || ViewFilterTypeNumber
}
getCompatibleFieldTypes() {
return [
'number',
'rating',
'autonumber',
'duration',
FormulaFieldType.compatibleWithFormulaTypes('number'),
]
}
matches(rowValue, filterValue, field, fieldType) {
if (filterValue === '') {
return true
@ -1412,6 +1423,29 @@ export class LowerThanViewFilterType extends ViewFilterType {
}
}
export class LowerThanOrEqualViewFilterType extends NumericComparisonViewFilterType {
static getType() {
return 'lower_than_or_equal'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.lowerThanOrEqual')
}
matches(rowValue, filterValue, field, fieldType) {
if (filterValue === '') {
return true
}
const rowVal = fieldType.parseInputValue(field, rowValue)
const fltVal = fieldType.parseInputValue(field, filterValue)
return (
Number.isFinite(rowVal) && Number.isFinite(fltVal) && rowVal <= fltVal
)
}
}
export class IsEvenAndWholeViewFilterType extends ViewFilterType {
static getType() {
return 'is_even_and_whole'

View file

@ -998,6 +998,36 @@ exports[`ViewFilterForm component Full view filter component 1`] = `
<!---->
<span
class="select__item-name-text"
title="viewFilter.higherThanOrEqual"
>
viewFilter.higherThanOrEqual
</span>
</div>
<!---->
</a>
<i
class="select__item-active-icon iconoir-check"
/>
</li>
<li
class="select__item select__item--no-options"
>
<a
class="select__item-link"
>
<div
class="select__item-name"
>
<!---->
<!---->
<!---->
<span
class="select__item-name-text"
title="viewFilter.lowerThan"
@ -1009,6 +1039,36 @@ exports[`ViewFilterForm component Full view filter component 1`] = `
<!---->
</a>
<i
class="select__item-active-icon iconoir-check"
/>
</li>
<li
class="select__item select__item--no-options"
>
<a
class="select__item-link"
>
<div
class="select__item-name"
>
<!---->
<!---->
<!---->
<span
class="select__item-name-text"
title="viewFilter.lowerThanOrEqual"
>
viewFilter.lowerThanOrEqual
</span>
</div>
<!---->
</a>
<i
class="select__item-active-icon iconoir-check"
/>
@ -1438,6 +1498,36 @@ exports[`ViewFilterForm component Test rating filter 1`] = `
<!---->
<span
class="select__item-name-text"
title="viewFilter.higherThanOrEqual"
>
viewFilter.higherThanOrEqual
</span>
</div>
<!---->
</a>
<i
class="select__item-active-icon iconoir-check"
/>
</li>
<li
class="select__item select__item--no-options"
>
<a
class="select__item-link"
>
<div
class="select__item-name"
>
<!---->
<!---->
<!---->
<span
class="select__item-name-text"
title="viewFilter.lowerThan"
@ -1449,6 +1539,36 @@ exports[`ViewFilterForm component Test rating filter 1`] = `
<!---->
</a>
<i
class="select__item-active-icon iconoir-check"
/>
</li>
<li
class="select__item select__item--no-options"
>
<a
class="select__item-link"
>
<div
class="select__item-name"
>
<!---->
<!---->
<!---->
<span
class="select__item-name-text"
title="viewFilter.lowerThanOrEqual"
>
viewFilter.lowerThanOrEqual
</span>
</div>
<!---->
</a>
<i
class="select__item-active-icon iconoir-check"
/>
@ -1878,6 +1998,36 @@ exports[`ViewFilterForm component Test rating filter 2`] = `
<!---->
<span
class="select__item-name-text"
title="viewFilter.higherThanOrEqual"
>
viewFilter.higherThanOrEqual
</span>
</div>
<!---->
</a>
<i
class="select__item-active-icon iconoir-check"
/>
</li>
<li
class="select__item select__item--no-options"
>
<a
class="select__item-link"
>
<div
class="select__item-name"
>
<!---->
<!---->
<!---->
<span
class="select__item-name-text"
title="viewFilter.lowerThan"
@ -1889,6 +2039,36 @@ exports[`ViewFilterForm component Test rating filter 2`] = `
<!---->
</a>
<i
class="select__item-active-icon iconoir-check"
/>
</li>
<li
class="select__item select__item--no-options"
>
<a
class="select__item-link"
>
<div
class="select__item-name"
>
<!---->
<!---->
<!---->
<span
class="select__item-name-text"
title="viewFilter.lowerThanOrEqual"
>
viewFilter.lowerThanOrEqual
</span>
</div>
<!---->
</a>
<i
class="select__item-active-icon iconoir-check"
/>

View file

@ -29,7 +29,9 @@ import {
DateEqualsCurrentYearViewFilterType,
IsEvenAndWholeViewFilterType,
HigherThanViewFilterType,
HigherThanOrEqualViewFilterType,
LowerThanViewFilterType,
LowerThanOrEqualViewFilterType,
SingleSelectIsAnyOfViewFilterType,
SingleSelectIsNoneOfViewFilterType,
} from '@baserow/modules/database/viewFilters'
@ -1225,6 +1227,44 @@ const numberValueIsHigherThanCases = [
},
]
const numberValueIsHigherThanOrEqualCases = [
{
rowValue: 2,
filterValue: 3,
expected: false,
},
{
rowValue: 2,
filterValue: 0,
expected: true,
},
{
rowValue: null,
filterValue: 0,
expected: false,
},
{
rowValue: 1,
filterValue: '-1',
expected: true,
},
{
rowValue: 0,
filterValue: '0',
expected: true,
},
{
rowValue: -1,
filterValue: '-1',
expected: true,
},
{
rowValue: -1,
filterValue: '0',
expected: false,
},
]
const numberValueIsLowerThanCases = [
{
rowValue: 1,
@ -1243,6 +1283,44 @@ const numberValueIsLowerThanCases = [
},
]
const numberValueIsLowerThanOrEqualCases = [
{
rowValue: 2,
filterValue: 3,
expected: true,
},
{
rowValue: 2,
filterValue: 0,
expected: false,
},
{
rowValue: null,
filterValue: 0,
expected: false,
},
{
rowValue: 1,
filterValue: '-1',
expected: false,
},
{
rowValue: 0,
filterValue: '0',
expected: true,
},
{
rowValue: -1,
filterValue: '-1',
expected: true,
},
{
rowValue: -1,
filterValue: '0',
expected: true,
},
]
describe('All Tests', () => {
let testApp = null
@ -1566,6 +1644,20 @@ describe('All Tests', () => {
}
)
test.each(numberValueIsHigherThanOrEqualCases)(
'NumberHigherThanOrEqualFilterType',
(values) => {
const app = testApp.getApp()
const result = new HigherThanOrEqualViewFilterType({ app }).matches(
values.rowValue,
values.filterValue,
{ type: 'number' },
new NumberFieldType({ app })
)
expect(result).toBe(values.expected)
}
)
test.each(numberValueIsHigherThanCases)(
'FormulaNumberHigherThanFilterType',
(values) => {
@ -1594,6 +1686,20 @@ describe('All Tests', () => {
}
)
test.each(numberValueIsLowerThanOrEqualCases)(
'NumberLowerThanOrEqualFilterType',
(values) => {
const app = testApp.getApp()
const result = new LowerThanOrEqualViewFilterType({ app }).matches(
values.rowValue,
values.filterValue,
{ type: 'number' },
new NumberFieldType({ app })
)
expect(result).toBe(values.expected)
}
)
test.each(numberValueIsLowerThanCases)(
'FormulaNumberLowerThanFilterType',
(values) => {